根据调试工具看Vue源码之虚拟dom(二)

栏目: JavaScript · 发布时间: 6年前

内容简介:上回我们提到,在子组件存在的情况下,父组件在执行完在上一篇与之相关的代码:point_down:

前言

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

深入了解 vnode

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

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

与之相关的代码:point_down:

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

从中我们可以得知:

createComponentInstanceForVnode
vnode.componentOptions.Ctor(options)

VNode

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

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 钩子之间,有段奇怪的代码:sweat_smile:

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({...}) ,所以看上去断点好像停在了「奇怪的地方」:point_down:

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

render 函数

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

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

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

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

About Face 3

About Face 3

Alan Cooper、Robert Reimann、David Cronin / John Wiley & Sons / 2007-5-15 / GBP 28.99

* The return of the authoritative bestseller includes all new content relevant to the popularization of how About Face maintains its relevance to new Web technologies such as AJAX and mobile platforms......一起来看看 《About Face 3》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具