摘要:通过的合并策略合并添加项到新的构造器上缓存父构造器处理和相关响应式配置项在新的构造器上挂上的工具方法缓存组件构造器在上总的来说是返回了一个带有附加配置相的新的的构造器。在函数中,构造器叫做,等待时候初始化。
身为原来的jquery,angular使用者。后面接触了react和vue。渐渐的喜欢上了vue。抱着学习的态度呀。看看源码。果然菜要付出代价。一步步单步调试。头好疼。看到哪里记到哪里。来一点点心得。错误的地方请帮我指出来。谢谢。最近看的是vue component部分。
先上一段最简单的代码,来剖析component机制:
{{a}} 111{{a}}
我们按照浏览器的思维逐行来。执行到脚本时。首先执行了
Vue.component("my-component", { name: "my-component", template: "A custom component!", components: { Child:Child }, created(){ console.log(this); }, mounted(){ console.log(this); } })
我们来看看这个函数经历了什么:
vue.js初始化的时候。调用了initGlobalAPI(vue),为vue挂上了工具函数vue.component
经过initGlobalAPI(vue)中的initAssetRegisters (Vue) 后。变为
vue.component = function ( id, definition ) { if (!definition) { return this.options[type + "s"][id] } else { /* istanbul ignore if */ { if (type === "component" && config.isReservedTag(id)) { warn( "Do not use built-in or reserved HTML elements as component " + "id: " + id ); } } if (type === "component" && isPlainObject(definition)) { definition.name = definition.name || id; definition = this.options._base.extend(definition); } if (type === "directive" && typeof definition === "function") { definition = { bind: definition, update: definition }; } this.options[type + "s"][id] = definition;//全局的组件或者指令和过滤器。统一挂在vue.options上。等待init的时候利用策略合并侵入实例。供实例使用 return definition } };
this.options._base在initGlobalAPI(vue)中为Vue.options._base = Vue;
so vue.component调用了vue.extend。找到了源头。我们来好好看看这个vue.extend这个function。代码如下:
Vue.extend = function (extendOptions) { extendOptions = extendOptions || {}; var Super = this; var SuperId = Super.cid; var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}); if (cachedCtors[SuperId]) { return cachedCtors[SuperId]//如果组件已经被缓存在extendOptions上则直接取出 } var name = extendOptions.name || Super.options.name; { if (!/^[a-zA-Z][w-]*$/.test(name)) { warn( "Invalid component name: "" + name + "". Component names " + "can only contain alphanumeric characters and the hyphen, " + "and must start with a letter."//校验组件名 ); } } var Sub = function VueComponent (options) { this._init(options); }; Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub;//将vue上原型的方法挂在Sub.prototype中,Sub的实例同时也继承了vue.prototype上的所有属性和方法。 Sub.cid = cid++; Sub.options = mergeOptions( Super.options, extendOptions//通过vue的合并策略合并添加项到新的构造器上 ); Sub["super"] = Super;缓存父构造器 // For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps$1(Sub); } if (Sub.options.computed) { //处理props和computed相关响应式配置项 initComputed$1(Sub); } // allow further extension/mixin/plugin usage Sub.extend = Super.extend; Sub.mixin = Super.mixin; Sub.use = Super.use; // create asset registers, so extended classes // can have their private assets too. //在新的构造器上挂上vue的工具方法 ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type]; }); // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub; } // keep a reference to the super options at extension time. // later at instantiation we can check if Super"s options have // been updated. Sub.superOptions = Super.options; Sub.extendOptions = extendOptions; Sub.sealedOptions = extend({}, Sub.options); // cache constructor cachedCtors[SuperId] = Sub;//缓存组件构造器在extendOptions上 return Sub }; } function initProps$1 (Comp) { var props = Comp.options.props; for (var key in props) { proxy(Comp.prototype, "_props", key); } } function initComputed$1 (Comp) { var computed = Comp.options.computed; for (var key in computed) { defineComputed(Comp.prototype, key, computed[key]); } }
总的来说vue.extend是返回了一个带有附加配置相的新的vue的构造器。在函数中,构造器叫做Sub,等待render时候初始化。
经过vue.component的调用。vue增加了一个全局组件my-component;此时vue.options.component如下图:
前三个是vue内置的三个组件,在initgloabalapi的时候初始化。
至此全局组件创建完成。全局组件放置在最底层。在以后的策略合并里会在子组件中的component项的__proto__中。
上图:
vue官方的生命周期图,其实也就是vue组件的构成的生命周期。沿着new Vue()我们来大概看看这些生命周期在什么阶段
Vue.prototype._init = function (options) { var vm = this; // a uid vm._uid = uid$1++; var startTag, endTag; /* istanbul ignore if */ if ("development" !== "production" && config.performance && mark) { startTag = "vue-perf-init:" + (vm._uid); endTag = "vue-perf-end:" + (vm._uid); mark(startTag); } // a flag to avoid this being observed vm._isVue = true; // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options);//内部组件调用此快捷方法 } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor),//策略合并,每项属性都有对应的合并规则 options || {}, vm ); } /* istanbul ignore else */ { initProxy(vm);//属性代理,即vm.xx = vm.data.xx } // expose real self vm._self = vm; initLifecycle(vm);//初始化生命周期状态变量,建立子父关系初始值,如$children,$parent. initEvents(vm);// 初始化事件 initRender(vm);//初始化render核心函数_$createElement和$slots $scopedSlots等 callHook(vm, "beforeCreate"); initInjections(vm); // resolve injections before data/props initState(vm);//利用数据劫持做响应式 initProvide(vm); //resolve provide after data/props callHook(vm, "created"); /* istanbul ignore if */ if ("development" !== "production" && config.performance && mark) { vm._name = formatComponentName(vm, false); mark(endTag); measure(((vm._name) + " init"), startTag, endTag); } if (vm.$options.el) { vm.$mount(vm.$options.el);//如果有el配置相则主动挂载。触发之后的compile.render } };
介绍了大概的_init函数,我们继续往下看程序的运行。完成了vue.component()之后。开始执行new vue(),创建实例。
对照_init函数。我们知道它分别进行了对传入参数的合并。初始化实例参数。创建响应的响应式。最后挂载:vm.$mount(vm.$options.el);
简单说说挂载。好吧。我们还是往方法里面看,挂载的时候发生了什么:
// public mount method Vue$3.prototype.$mount = function ( el, hydrating ) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) }; var mount = Vue$3.prototype.$mount//缓存mount,用来触发render Vue$3.prototype.$mount = function (//核心mount用来构建render函数 el, hydrating ) { el = el && query(el); /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { "development" !== "production" && warn( "Do not mount Vue to or- mount to normal elements instead."//检测,排除不可挂载的元素 ); return this } var options = this.$options; // resolve template/el and convert to render function if (!options.render) { var template = options.template;//假如输入的是template模版时。 if (template) { if (typeof template === "string") { if (template.charAt(0) === "#") { template = idToTemplate(template); /* istanbul ignore if */ if ("development" !== "production" && !template) { warn( ("Template element not found or is empty: " + (options.template)), this ); } } } else if (template.nodeType) { template = template.innerHTML;//输入的是dom节点时 } else { { warn("invalid template option:" + template, this); } return this } } else if (el) { template = getOuterHTML(el);//如果是一个id,如此次初始化挂载的id=app,会取到id=app的html } if (template) { /* istanbul ignore if */ if ("development" !== "production" && config.performance && mark) { mark("compile"); } var ref = compileToFunctions(template, { shouldDecodeNewlines: shouldDecodeNewlines,//核心compile函数。用于生成render函数。这里不细说 delimiters: options.delimiters }, this); var render = ref.render; var staticRenderFns = ref.staticRenderFns; options.render = render;//挂载render到实例options中。待调用 options.staticRenderFns = staticRenderFns;//静态的元素区分开。提升性能,后续虚拟dom树比较时,不会比较静态节点 /* istanbul ignore if */ if ("development" !== "production" && config.performance && mark) { mark("compile end"); measure(((this._name) + " compile"), "compile", "compile end"); } } } return mount.call(this, el, hydrating)//利用缓存的mount调用准备好的render }; $mount方法的核心其实就是准备好组件的render函数。这里最核心的一个方法就是:
var ref = compileToFunctions(template, { shouldDecodeNewlines: shouldDecodeNewlines,//核心compile函数。用于生成render函数。这里不细说 delimiters: options.delimiters }, this);compileToFunctions这个函数中主要做了两件事:
1:对模版进行compile(按标签解析,生成ast(抽象语法树)
2:利用generate(ast, options),生成render函数语法我们来看看最后实例生成的render函数:
没有错就是这个样子,很有感觉。生成的render函数保存在options中,等待调用
好吧。开始调用吧。
mount.call(this, el, hydrating)=》mountComponent(this, el, hydrating)=》updateComponent = function () { vm._update(vm._render(), hydrating); };=》vm._watcher = new Watcher(vm, updateComponent, noop);new Watcher中会主动调用updateComponent去touch依赖(给页面中引用过的data中的变量假如监听)正式调用render函数。既然都说了。那就来看看render函数:
Vue.prototype._render = function () { var vm = this; var ref = vm.$options; var render = ref.render; var staticRenderFns = ref.staticRenderFns; var _parentVnode = ref._parentVnode; if (vm._isMounted) { // clone slot nodes on re-renders for (var key in vm.$slots) { vm.$slots[key] = cloneVNodes(vm.$slots[key]); } } vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject; if (staticRenderFns && !vm._staticTrees) { vm._staticTrees = []; } // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode; // render self var vnode; try { vnode = render.call(vm._renderProxy, vm.$createElement);//核心函数,调用render } catch (e) { handleError(e, vm, "render function"); // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ { vnode = vm.$options.renderError ? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) : vm._vnode; } } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if ("development" !== "production" && Array.isArray(vnode)) { warn( "Multiple root nodes returned from render function. Render function " + "should return a single root node.", vm ); } vnode = createEmptyVNode(); } // set parent vnode.parent = _parentVnode; return vnode };_render()间接调用了vnode = render.call(vm._renderProxy, vm.$createElement);
然后结合render函数。看看发生了什么。vm.$createElement是核心的创建虚拟dom的函数。
继续看看核心构建虚拟dom函数:
function createElement ( context, tag, data, children,//children是该元素下的所有子元素 normalizationType, alwaysNormalize ) { if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children; children = data; data = undefined; } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE; } return _createElement(context, tag, data, children, normalizationType) } function _createElement ( context, tag, data, children, normalizationType ) { if (isDef(data) && isDef((data).__ob__)) { "development" !== "production" && warn( "Avoid using observed data object as vnode data: " + (JSON.stringify(data)) + " " + "Always create fresh vnode data objects in each render!", context ); return createEmptyVNode() } if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === "function") { data = data || {}; data.scopedSlots = { default: children[0] }; children.length = 0; } if (normalizationType === ALWAYS_NORMALIZE) { children = normalizeChildren(children); } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children); } var vnode, ns; if (typeof tag === "string") { var Ctor; ns = config.getTagNamespace(tag); if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ); } else if (isDef(Ctor = resolveAsset(context.$options, "components", tag))) {假如是组件则从上下文中取出组件的构造相关参数 // component vnode = createComponent(Ctor, data, context, children, tag); } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ); } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children); } if (vnode !== undefined) { if (ns) { applyNS(vnode, ns); } return vnode } else { return createEmptyVNode() } }这里其实我们不难看出vue在构造虚拟dom时。递归的去调用createElement去生成虚拟dom树。当children是组件或者是普通元素时。做不同的处理。这里我们关注的是。当子元素是组建时。这里调用了
vnode = createComponent(tag, data, context, children);
细心的人可以在去看看这个函数做了什么。简单来说这个函数将组件的构造参数取出来,放置在元素的componentOptions上。供后续创建真实dom时。标记该元素是组件。递归初始化。
跳过这些沉重的。我们直接看看我们的这个html生成的最终的虚拟dom长什么样。如下:我们在来看看我们的my-component组件长什么样子:
componentOptios上存着初始化组件需要的参数。
构建好虚拟dom后。vue进入update阶段:
这个阶段vue会判断先前有无该元素。是否为第一次渲染。假如是第一次。那么直接创建。如果不是有先前的ovnode,则比较差异。最小化更新。看看具体函数:nction lifecycleMixin (Vue) { Vue.prototype._update = function (vnode, hydrating) { var vm = this; if (vm._isMounted) { callHook(vm, "beforeUpdate"); } var prevEl = vm.$el; var prevVnode = vm._vnode;//取出缓存的久的虚拟dom var prevActiveInstance = activeInstance; activeInstance = vm; vm._vnode = vnode;//缓存当前vnode,供下次更新使用 // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) {//假如第一次渲染。直接创建 // initial render vm.$el = vm.__patch__( vm.$el, vnode, hydrating, false /* removeOnly */, vm.$options._parentElm, vm.$options._refElm ); } else { // updates vm.$el = vm.__patch__(prevVnode, vnode);//更新时。会比较差异 } activeInstance = prevActiveInstance; // update __vue__ reference if (prevEl) { prevEl.__vue__ = null; } if (vm.$el) { vm.$el.__vue__ = vm; } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el; } // updated hook is called by the scheduler to ensure that children are // updated in a parent"s updated hook. };__patch__函数我们就不细看了。算了看一下:
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { if (isUndef(vnode)) { if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); } return } var isInitialPatch = false; var insertedVnodeQueue = []; if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true;//假如第一次渲染。直接创建 createElm(vnode, insertedVnodeQueue, parentElm, refElm); } else { var isRealElement = isDef(oldVnode.nodeType); if (!isRealElement && sameVnode(oldVnode, vnode)) {//假如更新并且前后虚拟dom相似,这里相似有自己的一个算法。比如tag,key必需一致。才会去diff比较 // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly); } else { if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR); hydrating = true; } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true); return oldVnode } else { warn( "The client-side rendered virtual DOM tree is not matching " + "server-rendered content. This is likely caused by incorrect " + "HTML markup, for example nesting block-level elements inside " + ", or missing
. Bailing hydration and performing " + "full client-side render." ); } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode); } // replacing existing element var oldElm = oldVnode.elm; var parentElm$1 = nodeOps.parentNode(oldElm); createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm$1, nodeOps.nextSibling(oldElm) ); if (isDef(vnode.parent)) { // component root element replaced. // update parent placeholder node element, recursively var ancestor = vnode.parent; while (ancestor) { ancestor.elm = vnode.elm; ancestor = ancestor.parent; } if (isPatchable(vnode)) { for (var i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode.parent); } } } if (isDef(parentElm$1)) { removeVnodes(parentElm$1, [oldVnode], 0, 0); } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode); } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm }patch方法中核心的是createElm:看懂这个函数非常重要代码如下
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) { vnode.isRootInsert = !nested; // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {//根据之前保存的componentoptions来识别是否为组件。若是。则进这个逻辑 return } var data = vnode.data; var children = vnode.children; var tag = vnode.tag; if (isDef(tag)) { { if (data && data.pre) { inPre++; } if ( !inPre && !vnode.ns && !(config.ignoredElements.length && config.ignoredElements.indexOf(tag) > -1) && config.isUnknownElement(tag) ) { warn( "Unknown custom element: <" + tag + "> - did you " + "register the component correctly? For recursive components, " + "make sure to provide the "name" option.", vnode.context ); } } vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode); setScope(vnode); /* istanbul ignore if */ { createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue); } insert(parentElm, vnode.elm, refElm); } if ("development" !== "production" && data && data.pre) { inPre--; } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text); insert(parentElm, vnode.elm, refElm); } else { vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); } }我们这边还是先关注自己的组件部分。当children是组件元素时,很显然调用了createComponent(vnode, insertedVnodeQueue, parentElm, refElm);
var componentVNodeHooks = { init: function init ( vnode, hydrating, parentElm, refElm ) { if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) { var child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance, parentElm,//调用了组件内部的_init方法递归创建子组件。正式进入子组件的生命周期 refElm ); child.$mount(hydrating ? vnode.elm : undefined, hydrating);触发子组件的挂载。出发子组件的编译和render。又重新来一遍/直到子组件完全渲染好。再开始creelem下一个child } else if (vnode.data.keepAlive) { // kept-alive components, treat as a patch var mountedNode = vnode; // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode); } },这里就是递归创建子组件的核心部分.
总结: 第一次写这个vue。失败了。切模块切的不够细。组件机制感觉用了好多东西。这个面太大了。自己讲的时候也不知道该细讲还是。。。
总的来说:vue在创建虚拟dom的时候,如果元素是组件。则准备好组件的构造参数。包括模版和数据等等。组件中的元素如slot,和child放在组件元素的children下。供后面的内容分发用组件中的元素也是在父组件的作用域内编译的。看—_render()函数就知道。然后在vue需要将虚拟dom变为真实dom时。遇到组件元素时。开始递归初始化。直到把组件compile,render构建完后。开始构建下一个元素。最后添加到真实id=app上。并且把旧的删了。哈哈。随便写了
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/87027.html
相关文章
Vue 中 extend / component / mixins / extends 的区别
摘要:父组件在前,子组件在后。,,首先会在子组件里查找,如果没有,才会沿着原型链向上,找父组件中对应的属性。 在segmentfault上看到了一个问题vue.extend, vue.component 区别,突然有些方,好歹也写了几个小vue项目,自己都弄不清楚这些东西... new Vue()、component 首先我们来约定一个选项对象 baseOptions,后面的代码会用到. ...
Vue源码解读(1)--src/core/index.js 入口文件
摘要:生产版本设为可以启用检查。只适用于开发模式和支持的浏览器上指定组件的渲染和观察期间未捕获错误的处理函数为的运行时警告赋予一个自定义处理函数。注意这只会在开发者环境下生效,在生产环境下它会被忽略。 Vue源码主入口:src/core/index.js import Vue from ./instance/index // 引用Vue构造器 import { initGlobalAPI }...
(解析)vue源码解读
摘要:前言为什么要做源码解读我们新到一个环境,第一件事情就是熟悉环境熟悉项目,这个很考验阅读源码的能力以及耐心。构造函数拉到最下面,导出的是一个构造函数。 前言 A: 为什么要做源码解读?Q: 我们新到一个环境,第一件事情就是熟悉环境熟悉项目,这个很考验阅读源码的能力以及耐心。vue是个很好的库,知名度高,对js的学习具有向上性,所以搞清楚逻辑是有好处的。A: 阅读源码的程度?Q: 我们完全...
来一打前端博客压压惊
前言 本文所有内容全部发布再个人博客主页 https://github.com/muwoo/blogs欢迎订阅。不过最近因为事情比较多,有一段时间没有更新了,后面打算继续不断学习更新,欢迎小伙伴一起沟通交流~ 最近更新 前端单测的那些事 基于virtual dom 的canvas渲染 js Event loop 机制简介 axios 核心源码实现原理 JS 数据类型、赋值、深拷贝和浅拷贝 j...
发表评论
0条评论
1treeS
男|高级讲师
TA的文章
阅读更多
什么是 Python 编程语言?
阅读 3626·2021-11-24 09:39
C语言 位运算符详解 (使用二进制实例深入学习理解位运算符使用原理)
阅读 1230·2021-09-30 09:48
Bitwarden免费密码管理软件实现跨平台在线密码管理(云端密码存储)
阅读 3140·2021-09-09 11:51
RAKsmart:九月秒杀服务器$30/月起,洛杉矶/圣何塞/香港/日本站群特价,全场VPS五折
阅读 2843·2021-09-08 10:41
用CSS3实现钟表效果
阅读 1267·2019-08-30 14:06
CSS详细解读定位
阅读 2770·2019-08-30 14:01
css-浮动
阅读 842·2019-08-29 17:11
前端面试题(4)JavaScript
阅读 3143·2019-08-29 15:37
<阅读需要支付1元查看