vue普通节点的渲染流程

110 阅读2分钟

上次我们说到 vue 的初始化渲染会实例化一个渲染watcher,内部会执行**updateComponent方法,执行updateComponent方法的时候,会先执行实例上的_render方法生成vnode,在执行_update方法patch成真实dom**,下面我们来分析一下这两块的原理: 

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

Virtual Dom(VNode)

Vue2.0引入了**virtual dom,用一个原生的 JS 对象去描述一个 DOM节点,相比于操作真实DOM 代价更小。其实VNode**是对真实 DOM 的一种抽象描述,我们看一下它的组成:

代码位置: src/core/vdom/vnode.js

export default class VNode {  
   constructor (tag, data, children, text, elm, context, componentOptions) {
    this.tag = tag 
    this.data = data 
    this.children = children    
    this.text = text
    this.elm = elm
    this.context = context
    this.componentOptions = componentOptions 
    this.componentInstance = undefined
  }
}

VNode 核心定义无非就几个关键属性,标签名、数据、子节点、文本、真实元素、组件属性等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。

tip: Vue.js 的**virtual dom** 借鉴了开源组件库**snabbdom**的实现,然后加入了一些Vue.js自身需要的属性。

_render方法实现: 

代码位置:  src/core/instance/render.js

1. **_render方法是在renderMixin函数中添加到Vue Prototype上的,内部会执行编译好的render**函数。

Vue.prototype._render = function () {
  const vm = this  
  const { render } = vm.$option
  let vnode  
  try {          
    vnode = render.call(vm._renderProxy, vm.$createElement) // 执行render函数   
  }
}

执行**render函数的时候,会传入vm.$createElement**函数(以下简称h函数)。

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

2.用户调用h函数,会传入一系列配置项,如下:

new Vue({      
 el: '#app',      
 render(h) {        
  return h('div', {          
   attrs: {            
    id: 'app'          
   }        
  }, this.message)      
 },      
 data: {        
   message: 'Hello Vue!'      
 }    
})

3. **Vue拿到用户传递进来的配置项,会执行内部的createElement**方法。

代码位置: src/core/vdom/dom/create-element.js

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2
function createElement (  
  context,
  tag,  
  data,
  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)
}

执行**createElement函数,序列化用户传入的参数,然后执行__createElement**内部方法。

function _createElement (  context,  tag,  data,  children,  normalizationType){
   // 1.规范化children
   if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)   
   } else if (normalizationType === SIMPLE_NORMALIZE) {   
    children = simpleNormalizeChildren(children)   
   }   
   // 2. 生成不同类型的VNode(普通标签、组件类型)
   let vnode
   if (typeof tag === 'string') {
    let Ctor
    if (config.isReservedTag(tag)) {
     vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
     )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
     vnode = createComponent(Ctor, data, context, children, tag)
    } else {
     vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
    }
   } else {
    vnode = createComponent(tag, data, context, children)
   }
}

手绘流程图如下: 

到此为止**_render**函数执行完成,此时会生成不同类型的VNode。

_update方法实现:

**_update**入口:  src/core/instance/lifecycle.js

Vue.prototype._update = function (vnode, hydrating) {
  const vm = this  if (!prevVnode) {     
  vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
}

**__patch__**函数位置:  src/platforms/web/runtime/index.js

Vue.prototype.__patch__ = inBrowser ? patch : noop

**patch**函数位置: src/platforms/web/runtime/patch.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 = createPatchFunction({ nodeOps, modules })

**createPatchFunction**函数位置:  src/core/vdom/patch.js

function createPatchFunction (backend) {
  const { modules, nodeOps } = backend    
  return function patch (oldVnode, vnode, hydrating, removeOnly) {    
   const isRealElement = isDef(oldVnode.nodeType)    
   if (isRealElement) {
    oldVnode = emptyNodeAt(oldVnode)    
   }    
   const oldElm = oldVnode.elm    
   const parentElm = nodeOps.parentNode(oldElm) 
   createElm(      
     vnode,      
     insertedVnodeQueue,      
      oldElm._leaveCb ? null : parentElm,      
      nodeOps.nextSibling(oldElm)    
    )
    if (isDef(parentElm)) {      
      removeVnodes([oldVnode], 0, 0)    
    }  
   } 
}

**`createElm`**函数位置: **`src/core/vdom/patch.js`**

function createElm (    
  vnode,    
  insertedVnodeQueue,    
  parentElm,     
  refElm,    
  nested,    
  ownerArray,    
  index
) {
   vnode.isRootInsert = !nested   
   const data = vnode.data   
   const children = vnode.children
   const tag = vnode.tag    
   if (isDef(tag)) {     
     vnode.elm = nodeOps.createElement(tag, vnode)     
     createChildren(vnode, children, insertedVnodeQueue) // 递归调用createElm方法挂载子VNode
     insert(parentElm, vnode.elm, refElm)   
   } 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)    
  }
}

**createChildren**函数实现: **`src/core/vdom/patch.js`**

function createChildren (vnode, children, insertedVnodeQueue) {  
  if (Array.isArray(children)) {    
    for (let i = 0; i < children.length; ++i) {      
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)    
    }  
  } else if (isPrimitive(vnode.text)) {    
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))  
  }
}

手绘流程图如下: 

总结: 执行**patch函数的过程中,会执行createElm**创建节点并挂载(根据vnode的tag来区分,如果vnode有children,会遍历children递归调用createElm挂载子节点)。这是一个深度优先的过程,所以节点的挂载顺序是 子 -> 父