Vue2.0源码学习(1) – 数据和模板的渲染(下)

  • A+
所属分类:Web前端
摘要

上述updateComponent方法调用是运行了一个函数:其中会先运行vm._render函数,那么vm._render函数又是从哪里定义的呢?我们回到srccoreinstanceindex.js,这里一开始会运行renderMixin方法,而renderMixin函数具体定义在srccoreinstancerender.js中,去到函数中可以清晰看到,我们心心念念的vm._render就定义在此处了。


vm._render是怎么实现的

上述updateComponent方法调用是运行了一个函数:

// srccoreinstancelifecycle.js updateComponent = () => {   vm._update(vm._render(), hydrating) }  

其中会先运行vm._render函数,那么vm._render函数又是从哪里定义的呢?我们回到srccoreinstanceindex.js,这里一开始会运行renderMixin方法,而renderMixin函数具体定义在srccoreinstancerender.js中,去到函数中可以清晰看到,我们心心念念的vm._render就定义在此处了。

// srccoreinstancerender.js export function renderMixin (Vue: Class<Component>) {   ...   Vue.prototype._render = function (): VNode {     const vm: Component = this     const { render, _parentVnode } = vm.$options     ...       let vnode     try {       // 暂时只关注这部分代码       vnode = render.call(vm._renderProxy, vm.$createElement)     } catch (e) {       ...     }     ...     return vnode   } } 

render函数中其他的先不做详细解读,先把目光聚焦在render.call(vm._renderProxy, vm.$createElement)。
render函数可以在配置中自己写,也可以是生成的,在上节Vue实例挂载中有具体分析过$mount是如何生成render函数的大概流程,忘了的可以回顾一下,往下会以自写的render去跑一遍代码流程。
剖析点①:
vm._renderProxy:在_init函数中定义。

Vue.prototype._init = function (options?: Object) {   ...   if (process.env.NODE_ENV !== 'production') {  //非生产环境       initProxy(vm)     } else {       vm._renderProxy = vm     }     ... } 

代码中可知生产环境其实就是vm或者说this本身,而非生产环境时则是在initProxy函数中定义vm._renderProxy。我们往下看看定义在srccoreinstanceproxy.js里的initProxy方法。

// srccoreinstanceproxy.js let initProxy ... const hasProxy = typeof Proxy !== 'undefined' && isNative(Proxy) // 浏览器是否支持proxy if (hasProxy) {   const isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact')   config.keyCodes = new Proxy(config.keyCodes, {     set (target, key, value) {       if (isBuiltInModifier(key)) {         warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)         return false       } else {         target[key] = value         return true       }     }   }) } const hasHandler = {   has (target, key) {     const has = key in target     const isAllowed = allowedGlobals(key) ||       (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))     if (!has && !isAllowed) {       if (key in target.$data) warnReservedPrefix(target, key)       else warnNonPresent(target, key)     }     return has || !isAllowed   } } const getHandler = {   get (target, key) {     if (typeof key === 'string' && !(key in target)) {       if (key in target.$data) warnReservedPrefix(target, key)       else warnNonPresent(target, key)     }     return target[key]   } } initProxy = function initProxy (vm) {   if (hasProxy) {     // determine which proxy handler to use     const options = vm.$options     const handlers = options.render && options.render._withStripped       ? getHandler       : hasHandler     vm._renderProxy = new Proxy(vm, handlers)   } else {     vm._renderProxy = vm   } } export { initProxy } 

initProxy方法首先用hasProxy判断浏览器是否支持proxy,不支持情况下vm._renderProxy就是vm,支持的情况则是用Proxy对vm做一个数据劫持。

vm._renderProxy就告一段落了,下面看vm.$createElement

vm.$createElement:把render函数转换成Vnode,它的定义需要回到我们开始说的vue._init方法,其中运行了initRender方法,而vm.$createElement就定义在initRender函数中,下面我们看看initRender函数。

// srccoreinstancerender.js export function initRender (vm: Component) {   ...   // bind the createElement fn to this instance   // (将createElement fn绑定到此实例)   // so that we get proper render context inside it.   // (以便在其中获得适当的渲染上下文)   // args order: tag, data, children, normalizationType, alwaysNormalize(参数标注)   // internal version is used by render functions compiled from templates   vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)   // normalization is always applied for the public version, used in   // user-written render functions.   vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)   ... } 

其中vm._c是生产render所调用的,而vm.$createElement是自写render调用的,下面我们自写个render来做调试实验。

new Vue({   el:'#app',   render(createElement){     return createElement('div',{       attrs:{         id:"app1"       }     },this.msg)   },   data(){     return{       msg:"niccc"     }   } }) 

还记得前面的Vue.prototype._render方法不,里面有段代码render.call(vm._renderProxy, vm.$createElement),上面代码块中的render函数中的createElement参数,其实就是vm.$createElement方法。
注:官方render函数文档 https://cn.vuejs.org/v2/api/#render

createElement函数的实现

不知道各位是否还记得之前分析的render函数,其中的vm._c和vm.$createElement都是调用了createElement函数。下面我们来看看该函数

//srccorevdomcreate-element.js const SIMPLE_NORMALIZE = 1 const ALWAYS_NORMALIZE = 2 export function createElement (   context: Component,      tag: any,   data: any,   children: any,   normalizationType: any,   alwaysNormalize: boolean ): VNode | Array<VNode> {   //判断data参数是否为空,空时自动补全   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) } 

createElement 方法实际上是对 _createElement 方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElement,由于函数体较大,我们进行分段解读。

//srccorevdomcreate-element.js // part 1 export function _createElement (   context: Component,   //上下文环境,一般就是vm   tag?: string | Class<Component> | Function | Object,  //标签(element)   data?: VNodeData,     //VNode数据,VnodeData类型,详见flowvnode.js   children?: any,       //Vnode子节点   normalizationType?: number    //子节点规范类型 ): VNode | Array<VNode> {     if (isDef(data) && isDef((data: any).__ob__)) {       process.env.NODE_ENV !== '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()     }     // object syntax in v-bind     if (isDef(data) && isDef(data.is)) {       tag = data.is     }     if (!tag) {       // in case of component :is set to falsy value       return createEmptyVNode()     }     // warn against non-primitive key     if (process.env.NODE_ENV !== 'production' &&       isDef(data) && isDef(data.key) && !isPrimitive(data.key)     ) {       if (!__WEEX__ || !('@binding' in data.key)) {         warn(           'Avoid using non-primitive value as key, ' +           'use string/number value instead.',           context         )       }     }     ... } 

我们先分析一下入参:
  context:上下文环境,一般就是vm;
  tag:标签;
  data:VNode数据,VnodeData类型,详见flowvnode.js;
  children:Vnode子节点;
  normalizationType:子节点规范类型;
往下看其实就是对data数据的判断,看是否需要跑createEmptyVNode函数,即创建注释函数。下面我们继续看part 2。

//srccorevdomcreate-element.js // part 2 export function _createElement (   context: Component,   //上下文环境,一般就是vm   tag?: string | Class<Component> | Function | Object,  //标签(element)   data?: VNodeData,     //VNode数据,VnodeData类型,详见flowvnode.js   children?: any,       //Vnode子节点   normalizationType?: number    //子节点规范类型 ): VNode | Array<VNode> {     ...     if (normalizationType === ALWAYS_NORMALIZE) {         children = normalizeChildren(children)     } else if (normalizationType === SIMPLE_NORMALIZE) {         children = simpleNormalizeChildren(children)     }     ... } 

children最终形态是Vnode的节点,但是入参中的children却是any类型,所以我们需要对它进行转换,normalizeChildren和simpleNormalizeChildren就是做了这项任务。下面我们看看这两个函数

// srccorevdomhelpersnormalize-children.js  // 其中simpleNormalizeChildren是拍扁数组,成为一维素组 export function simpleNormalizeChildren (children: any) {   for (let i = 0; i < children.length; i++) {     if (Array.isArray(children[i])) {       return Array.prototype.concat.apply([], children)     }   }   return children } 

simpleNormalizeChildren方法其实就是为了拍扁为一维数组,具体是什么场景下进行的,后续再研究过来填坑。

// srccorevdomhelpersnormalize-children.js export function normalizeChildren (children: any): ?Array<VNode> {   return isPrimitive(children)  //判断是否为基础类型     ? [createTextVNode(children)]   //是,创建一个vnode节点     : Array.isArray(children)       //否,判断是否为数组       ? normalizeArrayChildren(children)    //详见下文       : undefined } 

normalizeChildren其实最终也是返回一个一维数组,它分了三个情况:
①:isPrimitive判断是基础类型,返回一个用一维数组包裹着的文本Vnode;
②:Array.isArray判断是数组类型,调用normalizeArrayChildren函数;
③:啥都都不是,返回undefined;
然后我们看看第②个情况中,normalizeArrayChildren做了什么。

// srccorevdomhelpersnormalize-children.js function normalizeArrayChildren (children: any, nestedIndex?: string): Array<VNode> {   const res = []   let i, c, lastIndex, last   for (i = 0; i < children.length; i++) {     c = children[i]     if (isUndef(c) || typeof c === 'boolean') continue     lastIndex = res.length - 1     last = res[lastIndex]     // 注①,如果是数组类型,则继续调用normalizeArrayChildren递归,递归后于父数组apply为一个一维数组。     if (Array.isArray(c)) {       if (c.length > 0) {         // 递归chilrend         c = normalizeArrayChildren(c, `${nestedIndex || ''}_${i}`)         // merge adjacent text nodes(合并相邻的文本节点)         if (isTextNode(c[0]) && isTextNode(last)) {           res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)           c.shift()         }         res.push.apply(res, c)       }     // 注②,如果是基础类型,则创建文本Vnode然后push到res数组中     } else if (isPrimitive(c)) {       if (isTextNode(last)) {         // merge adjacent text nodes         // this is necessary for SSR hydration because text nodes are         // essentially merged when rendered to HTML strings         res[lastIndex] = createTextVNode(last.text + c)       } else if (c !== '') {         // convert primitive to vnode         res.push(createTextVNode(c))       }     // 注③,再否则其实就是一个正常的Vnode,然后回对一些v-for做一些处理,然后也是push到res数组中     } else {       if (isTextNode(c) && isTextNode(last)) {         // merge adjacent text nodes         res[lastIndex] = createTextVNode(last.text + c.text)       } else {         // default key for nested array children (likely generated by v-for)         if (isTrue(children._isVList) &&           isDef(c.tag) &&           isUndef(c.key) &&           isDef(nestedIndex)) {           c.key = `__vlist${nestedIndex}_${i}__`         }         res.push(c)       }     }   }   return res } 

normalizeArrayChildren做的其实就是利用递归把多维数组apply合并成一维数组。
注①:如果是数组类型,则继续调用normalizeArrayChildren递归,递归后于父数组apply为一个一维数组。
注②:如果是基础类型,则创建文本Vnode然后push到res数组中;
注③:再否则其实就是一个正常的Vnode,然后回对一些v-for做一些处理,然后也是push到res数组中;

//srccorevdomcreate-element.js // part 3 export function _createElement (   context: Component,   //上下文环境,一般就是vm   tag?: string | Class<Component> | Function | Object,  //标签(element)   data?: VNodeData,     //VNode数据,VnodeData类型,详见flowvnode.js   children?: any,       //Vnode子节点   normalizationType?: number    //子节点规范类型 ): VNode | Array<VNode> {   ...   let vnode, ns   //这个tag就是'string'   if (typeof tag === 'string') {     let Ctor     ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)     //是否HTML原生标签     if (config.isReservedTag(tag)) {       // platform built-in elements       vnode = new VNode(         //config.parsePlatformTagName方法其实就是传什么返回什么         config.parsePlatformTagName(tag), data, children,         undefined, undefined, context       )     } else if ((!data || !data.pre) && 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 (Array.isArray(vnode)) {     return vnode   } else if (isDef(vnode)) {     if (isDef(ns)) applyNS(vnode, ns)     if (isDef(data)) registerDeepBindings(data)     return vnode   } else {     return createEmptyVNode()   } } 

首先会判断tag的类型,tag可以是字符串,也可以是组件,组件我们往后再细说,这里先关注String类型。config.isReservedTag判断是否HTML原生标签,(该方法定义在srcplatformswebutilelement.js),config.parsePlatformTagName方法其实就是传什么返回什么,然后new Vnode创建一个Vnode实例。
这个最终生成好的vnode,会经过几个方法return到_render函数。
①:return到createElement函数;
②:return到vm.$createElement;
③:return到vue._render中的vnode = render.call(vm._renderProxy, vm.$createElement)。
自此_render函数就告一段落了,我们接下来开始分析vm._update(vm._render(), hydrating)的vm._update函数。

_update函数的实现

_update是实例的一个私有方法,主要是吧Vnode渲染成真实的dom。调用的情况有两种,一是首次渲染,二是数据更新的时候。_update方法定义在srccoreinstancelifecycle.js,接下来我们看看具体的方法。

// srccoreinstancelifecycle.js export function lifecycleMixin (Vue: Class<Component>) {   Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {       ...       // 主要方法patch       vm.$el = vm.__patch__(prevVnode, vnode)       ...   } } 

其中核心其实就是vm.__patch__函数,其定义在 srcplatformswebruntimeindex.js

// srcplatformswebruntimeindex.js import { patch } from './patch' // 判断是否浏览器环境 Vue.prototype.__patch__ = inBrowser ? patch : noop 

__patch__有个三元表达式,inBrowser,用于判断是否为浏览器环境,是则为patch方法,否则就是noop空函数(服务器渲染时用到的)。下面我们看看patch方法,patch方法定义在srcplatformswebruntimepatch.js

// srcplatformswebruntimepatch.js import * as nodeOps from 'web/runtime/node-ops' import { createPatchFunction } from 'core/vdom/patch' import baseModules from 'core/vdom/modules/index' import platformModules from 'web/runtime/modules/index'  // 合并两个集合 const modules = platformModules.concat(baseModules)  export const patch: Function = createPatchFunction({ nodeOps, modules }) // createPatchFunction方法创建了许多辅助函数,最后返回patch函数(函数柯里化) 

可以看出,patch方法其实就是createPatchFunction方法调用后的return值,createPatchFunction入参分别是nodeOps和modules。
nodeOps其实就是封装一些dom原生操作的方法,有兴趣的可以到srcplatformswebruntimenode-ops.js看看;
modules是baseModules和platformModules的合集,主要是一些模块的钩子函数,主要用于生成dom,这里暂时不多赘述。
我们把关注点放到createPatchFunction函数上,由于createPatchFunction函数比较复杂,下面会分段分析。

// srccorevdompatch.js const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] export function createPatchFunction (backend) {   ...   return function patch (oldVnode, vnode, hydrating, removeOnly) {     ...   } } 

createPatchFunction很复杂,看源码是可以看出它其中定义了很多的辅助函数,然后最终返回了一个patch函数,这个也就是patch的最终方法,为什么要这么处理呢?其实这里是运用了函数柯里化的技巧,主要目的是让其有更高的灵活度和可复用性。接下来我们来详细看patch方法。

// srccorevdompatch.js return function patch (oldVnode, vnode, hydrating, removeOnly) {     ...     // oldVnode有值所以进入 else 阶段     if (isUndef(oldVnode)) {       ...     } else {       const isRealElement = isDef(oldVnode.nodeType)       // isRealElement为true,进入else阶段       if (!isRealElement && sameVnode(oldVnode, vnode)) {         ...       } else {         // isRealElement为true         if (isRealElement) {           ...           // either not server-rendered, or hydration failed.           // create an empty node and replace it           // emptyNodeAt函数作用是把真实dom转换成虚拟dom           oldVnode = emptyNodeAt(oldVnode)         }          // replacing existing element         const oldElm = oldVnode.elm   //提取真实dom对象(div#app)         const parentElm = nodeOps.parentNode(oldElm)  //真实dom的父级(body)          // create new node         // 下面会单独分析         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 : parent Elm,           nodeOps.nextSibling(oldElm)         )         ...       }     }     //插入钩子函数,后续再细看     invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)     return vnode.elm   } 

因为patch的高复用性,因此patch函数有多个条件判断语句,建议调试看着代码去理解patch。
现在我们以vue-cli默认模板来去分析,其中省略了部分没运行到的代码,上述代码注释中有注释解析,这边就不多赘述了,就简单说一下入参。oldVnode其实就是真实dom对象;vnode是虚拟dom;hydrating和removeOnly其实就是false。
一套流程下来去到了createElm函数,它是定义在createPatchFunction中的,下面我们来具体分析一下这个函数。

// srccorevdompatch.js   function createElm (     vnode,     insertedVnodeQueue,     parentElm,     refElm,     nested,     ownerArray,     index   ) {     ...     // 标注①     if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {       return     }      const data = vnode.data     const children = vnode.children     const tag = vnode.tag     if (isDef(tag)) {       ...       // 标注②       vnode.elm = vnode.ns         ? nodeOps.createElementNS(vnode.ns, tag)         : nodeOps.createElement(tag, vnode)       setScope(vnode)        /* istanbul ignore if */       if (__WEEX__) {         ...       } else {         // 标注③         createChildren(vnode, children, insertedVnodeQueue)         if (isDef(data)) {           // 创建一些钩子,后续再研究           invokeCreateHooks(vnode, insertedVnodeQueue)         }         // 标注④         insert(parentElm, vnode.elm, refElm)       }        if (process.env.NODE_ENV !== 'production' && data && data.pre) {         creatingElmInVPre--       }     } 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)     }   } 

createElm的作用其实就是通过vnode创建真实dom并插入到父节点中。
下面我们来分析上述代码块中标注的代码:
①:这里会尝试创建一个组件,当然我们现在创建的是页面,返回的是undefined,后续讲解组件化的时候会再详细聊createComponent函数;
②:vnode.ns为undefined,因此运行的是nodeOps.createElement(tag, vnode),createElement其实就是就是调用了原生的document.createElement方法去创建一个dom,源码在srcplatformswebruntimenode-ops.js中;
③:children是否有子节点,有的话创建子节点;createChildren函数其实其实就是递归createElm方法,把子节点插入进来;
④:insert其作用就是根据判断插入dom(insertBefore/appendChild),用到的地方很多,是插入真实dom的主要方法;

总结

自此,数据和模板是如何渲染到dom的过程我们已经分析完毕,当然中间还有很多细节的东西我们没有去探讨,我们首先把思维流程架构起来,然后再慢慢发散分支,这样会更有助我们去把源码读懂,下面再附上一张数据驱动流程图。
Vue2.0源码学习(1) - 数据和模板的渲染(下)