Vue源码翻译之渲染逻辑链
本篇文章主要要记录说明的是,Vue在Vdom的创建上的相关细节。这也是描绘了Vue在界面的创建上的一个逻辑顺序,同时我也非常拜服作者编码的逻辑性,当然或许这么庞大复杂的编码不是一次性铸就的,我想应该也是基于多次的需求变动而不断完善至现在如此庞大的结构和复杂度。
首先我们回顾 上一篇文章 中,讲到了Vue实例initMixin,就是实例初始化,但是,我们在看Vue的源码时,经常会遇到某个变量或方法,好像还没定义,怎么就用上了。那是因为,其实我们在使用Vue,即 new Vue(options) 的时候,其实Vue的整个类,已经在我们js的头部import时,就已经完全定义好了,Vue的类因为过于庞大,内部复杂,并且还有抽象分层,所以类的整个写法,会比较分散,但是当你在用它的时候(new Vue()),其实它已经完全初始化完毕,整个类的装配已经齐全,所以我们在看源码时,是根据工程目录来看,但Vue是建立在文本pack上,所以最终这些工程目录是会整合到一个文件里,所以我们遇到没看到的变量,不要感到困惑,你只要知道,它一定是在其他的某个地方初始化过。
So,我们这次要说的,是整个Vue再界面的绘制逻辑。
整个Vue组件的绘制过程,是这样一个方法链条:
vm.$mount() -> mountComponent -> new Watcher()的构造函数 -> watcher.get() -> vm._update -> vm.__patch__()-> patch.createElm -> patch.createComponent -> componentVNodeHooks.init() -> createComponentInstanceForVnode -> child.$mount
好了,从vm.$mount() -----> child.$mount,我相信大家应该看出个名堂来了,其实这很像递归调用。在执行createComponentInstanceForVnode的时候,就把创建好的Vnode与父级Vnode进行关联,通过这么一长串的递归调用去创建整个Vnode Tree,然后在整个树创建完了以后呢,在patch那部分的代码,会继续后续逻辑,后续逻辑自然就是把这个创建好的局部Vnode树,替换掉对应的旧的Vnode节点,相当于更新了局部的页面内容。但这只是执行界面绘制的动作链条,要理解整个过程,要区分一下,区分成执行,和初始化两个步骤。我们来看看定义是从哪里开始的。
首先要看的肯定是上一篇文章中讲到的 vm._c 以及 vm.$createElement ,这个函数的定义,是整个界面绘制逻辑的入口,但是并不是动作触发的入口,就像这个函数的名字一样,initRender,初始化绘制方法,实际上,就是对绘制动作进行了定义,但是并不是从这里执行。
InitRender
path:src/core/instance/render.js
1 export function initRender (vm: Component) { 2 vm._vnode = null // the root of the child tree 3 vm._staticTrees = null // v-once cached trees 4 const options = vm.$options 5 const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 6 const renderContext = parentVnode && parentVnode.context 7 vm.$slots = resolveSlots(options._renderChildren, renderContext) 8 vm.$scopedSlots = emptyObject 9 // bind the createElement fn to this instance 10 // so that we get proper render context inside it. 11 // args order: tag, data, children, normalizationType, alwaysNormalize 12 // internal version is used by render functions compiled from templates 13 vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) 14 // normalization is always applied for the public version, used in 15 // user-written render functions. 16 vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 17 18 // $attrs & $listeners are exposed for easier HOC creation. 19 // they need to be reactive so that HOCs using them are always updated 20 const parentData = parentVnode && parentVnode.data 21 22 /* istanbul ignore else */ 23 if (process.env.NODE_ENV !== 'production') { 24 defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => { 25 !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm) 26 }, true) 27 defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => { 28 !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm) 29 }, true) 30 } else { 31 defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) 32 defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true) 33 } 34 }
由此,我们再去查看createElement,这是一个又一个代码的封装,整个方法链的调用是这样子:
createElement -> _createElement -> createComponent
最终返回Vnode对象或Vnode对象数组(应该是在v-for的情况下返回数组)。中间的片段包含着一些校验逻辑,我就不说了,不是什么特别难理解的地方,我们直接看createComponent的方法
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void { if (isUndef(Ctor)) { return } const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { // Vue.extend(Component) Ctor = baseCtor.extend(Ctor) } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) if (Ctor === undefined) { // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } data = data || {} // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } // merge component management hooks onto the placeholder node mergeHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) // Weex specific: invoke recycle-list optimized @render function for // extracting cell-slot template. // https://github.com/Hanks10100/weex-native-directive/tree/master/component /* istanbul ignore if */ if (__WEEX__ && isRecyclableComponent(vnode)) { return renderRecyclableComponentTemplate(vnode) } return vnode }
首先,说明一下,入参Ctor是什么。其实这个Ctor,就是你平时写Vue文件时,components 对象里的那些东西,就是你写的单个Component对象。
这个可以从上层_createElement方法中得知,如下图:
其中调用的resolveAsset方法,就是从你的options,即你写的Component中,获取components属性,并且同时验证一下,与对应的tag是否存在于你定义的文件中,这个tag,是标签,是html标签,我们在使用自定义Vue组件的时候,都是自定义标签或<div is='componentName'></div> 这样的方式。而这个tag就是要吗是is的值,要吗是你使用的html标签。
再来。回到createComponent方法中,可以看到,代码一开始会去判断你这个组件对象是否是undefind,如果是undefind,那就直接退出。再往下看,有一行其实我们很熟悉,但可能有点懵逼的代码,就是 Ctor = baseCtor.extend(Ctor) ,这里怎么感觉有点熟悉,是的,这里其实就是我们经常在文档中看到的 Vue.extend(Component) 这么一个方法。这个baseCtor可以看到是从contentx.$options._base来的,这个contex 上级方法追溯就可以知道是一个vm对象,但是这个_base从何而来?不要着急,前面说了,遇到这种好像没看过的,它一定是在某处已经初始化过了,我们不用怀疑它,只需要找到他。
其实它在 src/core/global-api/index.js文件中,initGlobalAPI方法中就定义了,并且他指的就是Vue对象。
然后我们再回到 createComponent 方法这个主线任务中,继续往下打怪,我们会发现遇到一个函数是mergeHooks,
1 function mergeHooks (data: VNodeData) { 2 if (!data.hook) { 3 data.hook = {} 4 } 5 for (let i = 0; i < hooksToMerge.length; i++) { 6 const key = hooksToMerge[i] 7 const fromParent = data.hook[key] 8 const ours = componentVNodeHooks[key] 9 data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours 10 } 11 }
所谓hook,就是钩子,那再Vue中,这个钩子自然就是在代码中的某处可能会执行的方法,类似Vue实例的生命周期钩子一样。细看这个方法,它涉及到了一个对象,就是componentVNodeHooks对象,这个方法其实就是把这个对象里的init、prepath、insert、destory方法存进data.hook这个对象中罢了,那你回头要问,这个data又是从哪里来?一直追溯你会发现,这个是$createElement函数上的参数,咦?好像线索就断了= =?这个时候如果想要简单理解,只需要查找 Vue文档——深入data对象 你大概就知道这个data是神马了。
而此处正定义了,最开头说的界面渲染的执行动作链条中的递归调用创建子节点的部分。但是大家可能会觉得,奇怪,这个函数最终是走到了$createElement,可是跟先前提到的那个动作链条似乎没有相关,就算定义了data.hook,让动作链条就有componentVNodeHooks.init() 这个方法,可是什么地方触发这个定义呢?最开始的动作链条似乎没有涉及定义这部分呀?没地方触发这些定义的方法呀?
大家稍安勿躁,所以我说真的是很绕,不可能没定义,否则到执行data.hook.init的时候就undefind了。
我们要回头看一下,在Vue进行初始化装配的时候,有执行这么一个方法 renderMixin(Vue) :
1 export function renderMixin (Vue: Class<Component>) { 2 // install runtime convenience helpers 3 installRenderHelpers(Vue.prototype) 4 5 Vue.prototype.$nextTick = function (fn: Function) { 6 return nextTick(fn, this) 7 } 8 9 Vue.prototype._render = function (): VNode { 10 const vm: Component = this 11 const { render, _parentVnode } = vm.$options 12 13 // reset _rendered flag on slots for duplicate slot check 14 if (process.env.NODE_ENV !== 'production') { 15 for (const key in vm.$slots) { 16 // $flow-disable-line 17 vm.$slots[key]._rendered = false 18 } 19 } 20 21 if (_parentVnode) { 22 vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject 23 } 24 25 // set parent vnode. this allows render functions to have access 26 // to the data on the placeholder node. 27 vm.$vnode = _parentVnode 28 // render self 29 let vnode 30 try { 31 vnode = render.call(vm._renderProxy, vm.$createElement) 32 } catch (e) { 33 handleError(e, vm, `render`) 34 // return error render result, 35 // or previous vnode to prevent render error causing blank component 36 /* istanbul ignore else */ 37 if (process.env.NODE_ENV !== 'production') { 38 if (vm.$options.renderError) { 39 try { 40 vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) 41 } catch (e) { 42 handleError(e, vm, `renderError`) 43 vnode = vm._vnode 44 } 45 } else { 46 vnode = vm._vnode 47 } 48 } else { 49 vnode = vm._vnode 50 } 51 } 52 // return empty vnode in case the render function errored out 53 if (!(vnode instanceof VNode)) { 54 if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { 55 warn( 56 'Multiple root nodes returned from render function. Render function ' + 57 'should return a single root node.', 58 vm 59 ) 60 } 61 vnode = createEmptyVNode() 62 } 63 // set parent 64 vnode.parent = _parentVnode 65 return vnode 66 } 67 }
可能有的人就看明白了,我们看看,我们平时写组件的时候,如果你有用到render的方式来写组件样式,那是如何工作的。在Vue.prototype._render这个方法体内,你会看到render从vm.$options中取出(vm.$options就是你写的Component内的那些data、props等等的属性),然后再看上面截出的代码的第31行,render.call(vm._renderProxy,vm.$createElement),然后返回一个vnode,所以说,$createElemment在此处就会被调用,然后进行上面说的那些乱七八糟的代码。但是你可能又会问:render.call?我平时写Component的时候从来没用render函数来做界面绘制呀!这个render又是在什么时候被定义在$options的呢?否则直接从$options中取出肯定是会报错的呀。还是我刚才那句话,不是没定义,只是没找到,实际上是定义了,定义在哪儿了?定义在mountComonent的最开始的部分了。
然后你可能又会想,那按照代码的执行顺序,能确保在使用前就定义了吗?答案自然是肯定的。我们刚才看到$createElement这个方法,是被定义在vm._render当中,别忘了我们还有一个很重要的任务,就是找到$createElement是在哪里被执行的,那也就是说,vm._render()是在哪里被执行的。其实它就在mountComponent当中执行的,而且还一定是在render被定义之后才执行的。
其实这段代码不是简单地从上至下执行那么容易理解,你可以看到updateComponent的写法,其实它只是被定义了,而且在定义的时候,vm._update实际上是没有执行的,并且vm._render()也是没有被执行的,他们实际上是到了下面new Watcher()的构造函数当中才被执行,同时我们也可以看到,整个定义和动作执行两个过程中,在watcher的构造函数里,执行updateComponent方法时,vm._render()一定先执行然后返回一个vnode,然后才是到了vm._update开始执行,也就是说,此时data.hook已经被装填了init等函数,所以在最开始的执行链不会因为属性尚未定义而报出undefind被打断。
哈哈,真的很绕。说实在话,看了良久才看明白这绕来绕去的逻辑。
另外,在我研读这份源码时,我才发现(额,我并木有什么偏见),src/platforms 包下,除了web,多了一个weex。然后我就又回过头理解了一圈,发现vue是把vm.$mount以及相关界面的模块整个都抽出来单独写,然后在不同的平台,就可以使用不同的渲染方式,然后我们在使用webpack打包时,只修要针对自己想要的平台打包对应的模块。如此将界面渲染层分开写,真的是增加了Vue的扩展性,整个工程就很好扩展和管理。拜服大神的设计。