内容简介: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源码解读
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
产品经理修炼之道
费杰 / 机械工业出版社华章公司 / 2012-7-30 / 59.00元
本书由资深产品经理、中国最大的产品经理沙龙Pmcaff创始人费杰亲自执笔,微软、腾讯、百度、新浪、搜狐、奇虎、阿里云、Evernote等国内外20余家大型互联网企业资深产品经理和技术专家联袂推荐。用系统化的方法论和丰富的实战案例解读了优秀产品经理所必须修炼的产品规划能力、产品设计能力、产品执行能力,以及思考、分析和解决问题的能力和方法,旨在为互联网产品经理打造核心竞争力提供实践指导。 全书一......一起来看看 《产品经理修炼之道》 这本书的介绍吧!
RGB CMYK 转换工具
RGB CMYK 互转工具
HSV CMYK 转换工具
HSV CMYK互换工具