vue组件的渲染流程

102 阅读3分钟

一. 创建组件类型VNode

上一章主要分析了普通节点的渲染流程,普通节点的渲染流程猛戳这里,本篇文章来探讨一下组件节点的渲染流程。

由于挂载过程中会实例化render watcher,此时会先执行vm._render,执行vm._render时会去执行编译好的render函数,并向外传递vm.$createElement方法(以下简称 h 函数),用户调用h函数创建 Vnode节点,我们来看一下h函数内部做了哪些事情:

const App = {name: 'app',template: '<div id="app">我是app组件</div>'}
new Vue({ el: '#app', render: h => h(App)})

我们调用h函数,传入了App组件对象,Vue 内部会先执行 createElement 方法先把参数序列化,再执行 _createElement 生成不同类型的 Vnode,代码如下:

function createElement (
  context, // Vue实例
  tag, // App对象
) {
  return _createElement(context, tag)
}

function _createElement (
  context,
  tag
) {
  var vnode
  if (typeof tag === 'string') { // 普通节点的VNode
     vnode = new VNode(tag, context);
  } else { // 组件节点的VNode
     vnode = createComponent(tag, context);
  }
  return vnode
}

createComponent 会生成组件类型的Vnode,它接收组件对象作为第一个参数。让我们来看看内部的执行逻辑:

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

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  context: Component,
  data: ?VNodeData,
): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) {
    return
  }

  const baseCtor = context.$options._base // Vue

  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor) // 1.构造子类构造函数
  }

  data = data || {}

  installComponentHooks(data) // 2.安装组件钩子函数

  const name = Ctor.options.name || tag
  const vnode = new VNode( // 3.实例化Vnode
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }
  )

  return vnode
}

其实createComponent函数内部做了3件事:

1.构造子类构造函数

const baseCtor = context.$options._base // Vue
if (isObject(Ctor)) {
  Ctor = baseCtor.extend(Ctor)
}

extend 代码位置: src/core/global-api/extend.js

Vue.extend = function (extendOptions: Object): Function {
  extendOptions = extendOptions || {}
  const Super = this
  const SuperId = Super.cid
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }

  const name = extendOptions.name || Super.options.name

  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }

  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use

  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })

  if (name) {
    Sub.options.components[name] = Sub
  }

  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)

  cachedCtors[SuperId] = Sub   // cache constructor
  return Sub
}

Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换成一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

这样当我们去实例化 Sub 的时候,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑,之后会分析实例化子组件的逻辑。

2. 安装组件钩子函数

installComponentHooks(data)

const componentVNodeHooks = {
  init: function init (vnode, hydrating) {
    var child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    );
    child.$mount(undefined, false);
  },
  prepatch: function prepatch (oldVnode, vnode) {},
  insert: function insert (vnode) {},
  destroy: function destroy (vnode) {}
};

var hooksToMerge = Object.keys(componentVNodeHooks);

function installComponentHooks (data) {
  var hooks = data.hook || (data.hook = {});
  for (var i = 0; i < hooksToMerge.length; i++) {
    var key = hooksToMerge[i];
    var toMerge = componentVNodeHooks[key];
    hooks[key] = toMerge;
  }
}

Vue.js 在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数,整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数,后面会有介绍。

执行完成后 data 变成了如下结构: 

\

3. 实例化 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 },
)
return vnode

生成的组件**VNode**如下图所示: 

手绘流程图如下: 

至此,创建组件类型 VNode流程结束。

二. 组件的渲染流程

和普通节点相比,组件的渲染还是比较复杂的,下面我们来介绍一下流程:

  1. 当执行createElm方法创建node节点时,这里会判断 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值,如果为 true 则直接结束
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
) {
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
}

2.createComponent会执行 vnode.data.hook.init 方法,并把vnode作为参数传入。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  var i = vnode.data;
  if (isDef(i)) {
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false);
    }
    
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue);
      insert(parentElm, vnode.elm, refElm);
      return true
    }
  }
}
  1. init 钩子函数执行也很简单,它是通过 createComponentInstanceForVnode 创建一个 Vue 的实例,然后调用 $mount 方法挂载子组件, 先来看一下 createComponentInstanceForVnode 的实现:
function init (vnode, hydrating) {
  var child = vnode.componentInstance = createComponentInstanceForVnode(
    vnode,
    activeInstance
  );
  child.$mount(undefined, false);
}

function createComponentInstanceForVnode (vnode, parent) {
  var options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent: parent
  };
  return new vnode.componentOptions.Ctor(options)
}
  1. createComponentInstanceForVnode 函数会构造一个 options 参数,然后执行 new vnode.componentOptions.Ctor(options)。这里的 vnode.componentOptions.Ctor 对应的就是子组件的构造函数,我们在上面分析了它实际上是继承于 Vue 的一个构造器 Sub,相当于 new Sub(options) 这里有几个关键参数要注意几个点,_isComponent 为 true 表示它是一个组件,parent 表示当前激活的组件实例。
  2. 所以子组件的实例化实际上就是在这个时机执行的,并且它会执行实例的 _init 方法,这个过程有一些和之前不同的地方需要挑出来说:
Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // merge options
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  } 
}
  1. 这里首先是合并 options 的过程有变化,_isComponent 为 true,所以走到了 initInternalComponent 过程,这个函数的实现也简单看一下:
function initInternalComponent (vm: Component, options) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}
  1. 这个过程我们重点记住以下几个点即可:opts.parent = options.parentopts._parentVnode = parentVnode

  2. 再来看一下 _init 函数最后执行的代码:

if (vm.$options.el) {
   vm.$mount(vm.$options.el)
}

由于子组件初始化的时候是不传 el 的,因此组件是自己接管了 $mount 的过程,这个过程的主要流程在上一章介绍过了,回到组件 init 的过程,componentVNodeHooks 的 init 钩子函数,在完成实例化的 _init 后,接着会执行 child.$mount(undefined, false),它最终会调用 mountComponent 方法,进而执行 vm._render() 方法:

function mountComponent (
  vm,
  el,
  hydrating
) {
  vm.$el = el //由于el是undefined,所以vm.$el也是undefined
  ...
})

Vue.prototype._render = function (): VNode {
  const vm: Component = this
  let vnode
  try {
    vnode = render.call(vm._renderProxy, vm.$createElement)
  } catch (e) {
  }
  return vnode
}
  1. 我们知道在执行完 vm._render 生成子组件 VNode 后,接下来就要执行 vm._update 去渲染 VNode 了。来看一下组件渲染的过程中有哪些需要注意的,
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  var vm = this;
  var prevEl = vm.$el; // undefined
  var prevVnode = vm._vnode; // undefined
  vm._vnode = vnode; // 执行_render生成的vnode。
  
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false) // undefined, nvode
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}
  1. 最后就是调用 patch 渲染 VNode 了。
function patch (oldVnode, vnode, hydrating, removeOnly) { // undefined, vnode
  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
  }
}

这里又回到了渲染流程开始的位置,还是会执行的函数 createElm,注意这里我们只传了 2 个参数,所以对应的 parentElm 是 undefined。我们再来看看它的定义:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
) {
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  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)
    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)
  }
}

function createChildren (vnode, children, insertedVnodeQueue) {
  if (Array.isArray(children)) {
    for (var i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
    }
  }
}

注意,这里我们传入的 vnode 是子组件渲染的 vnode,也就是我们之前说的 vm._vnode,如果组件的根节点是个普通元素,那么 vm._vnode 也是普通的 vnode,这里 createComponent(vnode, insertedVnodeQueue, parentElm, refElm) 的返回值是 false。接下来的过程就和创建普通元素的过程一样,先创建一个父节点占位符,然后再遍历所有子 VNode 递归调用 createElm,在遍历的过程中,如果遇到子 VNode 是一个组件的 VNode,则重复执行createComponent,这样通过一个递归的方式就可以完整地构建了整个组件树。

  1. 由于我们这个时候传入的 parentElm 是空,所以对组件的插入,在 createComponent 有这么一段逻辑:
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    if (isDef(vnode.componentInstance)) { 
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm) // 在这个地方执行插入流程
      return true
    }
  }
}

function initComponent (vnode, insertedVnodeQueue) {
  vnode.elm = vnode.componentInstance.$el;
}
  1. 在完成组件的整个 patch 过程后,最后执行 insert(parentElm, vnode.elm, refElm) 完成组件的 DOM 插入,如果组件 patch 过程中又创建了子组件,那么DOM 的插入顺序是先子后父。

手绘流程图如下: 

16.png