前言

上回我们提到,在子组件存在的情况下,父组件在执行完created钩子函数之后生成子组件的实例,子组件执行created钩子函数,同时也检查是否也有子组件,有则重复父组件的步骤,否则子组件的dom元素渲染

深入了解vnode

在上一篇文章中其实我们提到一个函数 —— createComponentInstanceForVnode????

function createComponentInstanceForVnode (  vnode, // we know it's MountedComponentVNode but flow doesn't  parent // activeInstance in lifecycle state) {  var options = {    _isComponent: true,    _parentVnode: vnode,    parent: parent  };  // check inline-template render functions  var inlineTemplate = vnode.data.inlineTemplate;  if (isDef(inlineTemplate)) {    options.render = inlineTemplate.render;    options.staticRenderFns = inlineTemplate.staticRenderFns;  }  return new vnode.componentOptions.Ctor(options)}

与之相关的代码????

...var child = vnode.componentInstance = createComponentInstanceForVnode(    vnode,    activeInstance);child.$mount(hydrating ? vnode.elm : undefined, hydrating);

从中我们可以得知:

  • 子组件的实例是由createComponentInstanceForVnode生成的
  • 上面的结论与vnode.componentOptions.Ctor(options)有关

VNode

通过全局检索componentOptions,可知存在如下代码????

var VNode = function VNode (  tag,  data,  children,  text,  elm,  context,  componentOptions,  asyncFactory) {  this.tag = tag;  this.data = data;  this.children = children;  this.text = text;  this.elm = elm;  ...}

实际上,在beforeMount钩子和mounted钩子之间,有段奇怪的代码????

new Watcher(vm, updateComponent, noop, {    before: function before () {      if (vm._isMounted && !vm._isDestroyed) {        callHook(vm, 'beforeUpdate');      }    }  }, true /* isRenderWatcher */);

看过前面的文章的你其实已经知道Watcher的执行逻辑:

  1. 初始化相关属性,其中包括getter属性
  2. value赋值的同时执行getter

updateComponent实现:

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

这意味着函数 updateComponent 将被执行,同时存在这样的调用顺序(从上往下执行):

  • vm._render
  • vm_update

同时dom元素肯定也是在这两个函数调用时渲染

vm._render

Vue.prototype._render = function () {    var vm = this;    var ref = vm.$options;    var render = ref.render;    var _parentVnode = ref._parentVnode;    if (_parentVnode) {      vm.$scopedSlots = normalizeScopedSlots(        _parentVnode.data.scopedSlots,        vm.$slots,        vm.$scopedSlots      );    }    // 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 {      // There's no need to maintain a stack becaues all render fns are called      // separately from one another. Nested component's render fns are called      // when parent component is patched.      currentRenderingInstance = vm;      vnode = render.call(vm._renderProxy, vm.$createElement);    } catch (e) {      handleError(e, vm, "render");      // return error render result,      // or previous vnode to prevent render error causing blank component      /* istanbul ignore else */      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {        try {          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e);        } catch (e) {          handleError(e, vm, "renderError");          vnode = vm._vnode;        }      } else {        vnode = vm._vnode;      }    } finally {      currentRenderingInstance = null;    }    // if the returned array contains only a single node, allow it    if (Array.isArray(vnode) && vnode.length === 1) {      vnode = vnode[0];    }    // return empty vnode in case the render function errored out    if (!(vnode instanceof VNode)) {      if (process.env.NODE_ENV !== '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 的执行过程(从上往下):

  • _parentVnode(父组件的 vnode) 赋值给 vm.$vnode
  • 执行 normallizeScopedSlots,将父子组件的 $slots$scopedSlots 合并
  • 执行 render 函数并赋值给 vnode(即得到现有的 vnode
  • 如果 vnode 为空则执行 createEmptyVNode 函数
  • 返回 vnode

这里我们优先把断点打入 render 函数,理所当然的会得到以下执行过程:

  • render
  • vm.$createElement

由于最先执行的是 new Vue({...}),所以看上去断点好像停在了「奇怪的地方」????

new Vue({  render: h => h(App),}).$mount('#app')

render 函数

细心的同学会注意到这样一行代码????

vnode = render.call(vm._renderProxy, vm.$createElement);

步进之后断点马上跳到了这里????

new Vue({    render: h => h(App),    ...}).$mount('#app')

其实,这里将 vm._renderProxy 作为了 render 函数的上下文对象,而 vm.$createElement 返回一个闭包函数作为 render 函数的参数传入

相关代码:

vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };

总结

总结下生成 vnode 的完整逻辑:

  • 执行 $mount 函数
  • 判断是否在浏览器环境下,是则获取 dom 元素并赋值给 el 变量,否则 el 变量取值 undefined
  • 执行 mountComponent 函数
  • 执行 new Watcher(vm, updateComponent, noop, ...)
  • 由于 Watcher 的「特性」(传入的 updateComponent 赋值给 getter 之后执行),_render 函数在这之后会被触发
  • 执行 $createElement
  • 执行 createElement
  • 执行 _createElement
  • 判断参数 datadata.is 是否不为空,是则将 data.is 赋值给 tag
  • 如果 tag 为空,那么认为这是一个空白节点,此时调用 createEmptyVNode 创建一个「空白节点」,并把 isComment 标记为 true
  • 判断 tag 是否是「保留字」,是则属于 HTML 标签,生成对应的 vnode,否则调用 createComponent 函数生成对应的 vnode
  • 最后返回 vnode

相关函数

createEmptyVNode

var createEmptyVNode = function (text) {  if ( text === void 0 ) text = '';  var node = new VNode();  node.text = text;  node.isComment = true;  return node};