内容简介:vue源码解读-component机制
身为原来的jquery,angular使用者。后面接触了react和vue。渐渐的喜欢上了vue。抱着学习的态度呀。看看源码。果然菜要付出代价。一步步单步调试。头好疼。看到哪里记到哪里。来一点点心得。错误的地方请帮我指出来。谢谢。最近看的是vue component部分。
先上一段最简单的代码,来剖析component机制:
<div id="app"> <div>{{a}}</div> <input v-model="heihei"> <button v-on:click="click1"> click1 </button> <my-component> <div slot='dudu'>111</div> <Child>{{a}}</Child> </my-component> <button @click.stop="click2"> click2 </button> </div> </body> <script src="vue.js"></script> <script type="text/javascript"> var Child = { template: '<div>A custom component!</div>' } Vue.component('my-component', { name: 'my-component', template: '<div>A custom component!<Child></Child><slot></slot></div>', components: { Child:Child }, created(){ console.log(this); }, mounted(){ console.log(this); } }) new Vue({ el: '#app', data: function () { return { heihei:{name:3333}, a:1 } }, components: { Child:Child }, created(){ }, methods: { click1: function(){ console.log(this); }, click2: function(){ alert('click2') } } }) </script>
我们按照浏览器的思维逐行来。执行到脚本时。首先执行了
Vue.component('my-component', { name: 'my-component', template: '<div>A custom component!<Child></Child><slot></slot></div>', 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如何巧妙构建应用)
上图:
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 <html> or <body> - 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)) + "\n" + '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 ' + '<p>, or missing <tbody>. 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在comp创建虚拟dom的时候,如果元素是组件。则准备好组件的构造参数。包括模版和数据等等。组件中的元素如slot,和child放在组件元素的children下。供后面的内容分发用组件中的元素也是在父组件的作用域内编译的。看—_render()函数就知道。然后在vue需要将虚拟dom变为真实dom时。遇到组件元素时。开始递归初始化。直到把组件compile,render构建完后。开始构建下一个元素。最后添加到真实id=app上。并且把旧的删了。哈哈。随便写了
以上所述就是小编给大家介绍的《vue源码解读-component机制》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Phoenix解读 | Phoenix源码解读之索引
- Phoenix解读 | Phoenix源码解读之SQL
- Redux 源码解读 —— 从源码开始学 Redux
- AQS源码详细解读
- SDWebImage源码解读《一》
- MJExtension源码解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Stylin' with CSS
Wyke-Smith, Charles / 2012-10 / $ 50.84
In this completely revised edition of his bestselling Stylin' with CSS, veteran designer and programmer Charles Wyke-Smith guides you through a comprehensive overview of designing Web pages with CSS, ......一起来看看 《Stylin' with CSS》 这本书的介绍吧!