vue-basic2 案例源码解析

100 阅读4分钟

案例

<body>
  <div id="app"></div>
</body>
<script src="vue.js"></script>
<script>
var App = {
  data: function () {
    return { message: 'Hello Vue!' }
  },
  template: '<div>{{ message }}</div>'
}
var app = new Vue({
  el: '#app',
  // 这里的 h 是 createElement 方法
  render: h => h(App)
})
</script>

设置 vm.$options 为:

{
    "components": {},
    "directives": {},
    "filters": {},
    "el": "#app""render": h => h(App),
    "_base": ƒ Vue(options)  // 入口的 vue 方法
    ...
}

前面的步骤和 demo-basic1 一致,只是通过 $mount 方法获取的 render 不再是生成的,而是手写的:

h => h(App)

然后看下这种情况下核心的 2 个方法:vm._render 和 vm._update 又有哪些不同。

vm._render

render 是手写的时候,调用的不再是 _c 方法而是 vm.$createElement 生成 vnode,传入的参数为 APP:

// 传入 `render` 函数的参数为 `vm.$createElement`,也就是前面的 `h` 表示的就是 `vm.$createElement` 方法。
   vnode = render.call(vm._renderProxy, vm.$createElement);

initRender 方法中定义了两个将 render 方法转换为 vnode 的方法:

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

vm._c 是被模板编译成的 render 函数使用,而 vm.$createElement 是用户手写 render 方法使用的, 它们支持的参数相同,并且内部都调用了 createElement$1 方法,而 createElement$1 方法的最后又调用了 _createElement 方法。

由于此时传入的最后一个参数为 true,所以在调用 _createElement 时会将 normalizationType 设置为 ALWAYS_NORMALIZE。传入的 tag 参数为 App 这个对象:

{
  data: function () {
    return { message: 'Hello Vue!' }
  },
  template: '<div>{{ message }}</div>'
}
if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE;
}
function _createElement(context, tag, data, children, normalizationType) { ... }

_createElement

if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children);
 }
if (typeof tag === 'string') { ... } else {
    vnode = createComponent(tag, data, context, children);
}
  1. 手写的 render 在这里会触发 normalizeChildren 方法,由于当前 childrenundefined,暂时不考虑。
  2. 由于传入的 tag 是一个对象,所以会触发 createComponent 方法。

createComponent

构造子类构造函数

var baseCtor = context.$options._base;
if (isObject(Ctor)) {
   Ctor = baseCtor.extend(Ctor);
}

baseCtor 就是我们入口的 Vue 函数。

function Vue(options) {
   if (!(this instanceof Vue)) {
      warn$2('Vue is a constructor and should be called with the `new` keyword');
   }
   this._init(options);
}

当我们使用局部注册的方式注册组件的时候会走到 isObject(Ctor) 中的逻辑,从而执行 baseCtor.extend(Ctor) 方法,而 baseCtor 是一个 vue 实例,所以这里使用的是 Vue.extend 方法。

Vue.extend = function (extendOptions) {
          extendOptions = extendOptions || {};
          var Super = this;
          var SuperId = Super.cid;
          var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
          // 判断缓存
          if (cachedCtors[SuperId]) {
              return cachedCtors[SuperId];
          }
          ...
          var 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;
          ...
          // 缓存 Sub
          cachedCtors[SuperId] = Sub;
          return Sub;
      };
  1. Vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回。 image.png 传入的 baseCtor 就是 SuperCtor 就是 Sub,经过继承,它们的关系如图所示。
  2. Sub 定义为一个 VueComponent 函数,也就是 Vue 中的组件都是一个 VueComponent 实例,然后在 Sub 的原型上添加了很多 Vue 实例的数据。
  3. 将传入的 extendOptionsVue 的属性进行合并,存储在 Suboptions 属性中。
  4. 最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

实现继承后,局部注册时的 Ctoroptions 属性长这样:

{
    data: () { return { count: 0 } }
    template: "<button v-on:click=\"count++\">You clicked me {{ count }} times.</button>"
    _Ctor: {0: VueComponent(options) { this._init(options);}}
}

 installComponentHooks

就是把 componentVNodeHooks 的钩子函数(destroy,init,insert,prepatch)合并到 data.hook 中,VNode 执行 patch 的过程中(将 vnode 转换为真实 Dom )执行相关的钩子函数。这里要注意的是合并策略,在合并过程中,如果某个时机的钩子已经存在 data.hook 中,那么通过执行 mergeHook 函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可。

实例化 vnode

      var name = getComponentName(Ctor.options) || tag;
      var vnode = new VNode("vue-component-".concat(Ctor.cid).concat(name ? "-".concat(name) : ''), 
             data, undefined, undefined, undefined, context,
             { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, asyncFactory);
      return vnode;

最后一步通过 new VNode 实例化一个 vnode 并返回。需要注意的是和普通元素节点的 vnode 不同,组件的 vnode 是没有 children 的,这点很关键。这里创建的 vnode 为:

{
  tag: 'vue-component-1',
  componentOptions : { 
      Ctor: ƒ VueComponent(options),
      ...
  },
  ...
}

在生成的 vnode 中的 componentOptions\color{blue}{`componentOptions`} 属性中的 Ctor,就是我们在前面得到的组件实例 VueComponent

组件的 vnode 没有 children,且具有 componentOptions 属性,其中的 Ctor就是组件的初始化方法。

vm._update

作用是把 VNode 渲染成真实的 DOM

这个方法中的第一步是将前一个 vnode 存储为 preVnode,再将 vm._vnode 赋值为当前生成的 vnode,在 render 阶段还设置了 vm.$vnode ,这两个属性的关系就是一种父子关系,用代码表达就是 vm._vnode.parent === vm.$vnode

 var prevEl = vm.$el;         // <div id="app"></div>
 var prevVnode = vm._vnode;     // null
 var restoreActiveInstance = setActiveInstance(vm);
 vm._vnode = vnode;         // vue-component-1
 
 ...__patch__...
 
 restoreActiveInstance();

这个 setActiveInstance 作用就是保持当前上下文的 Vue 实例,我们可以看到这个方法,就是先将当前的活动对象存储起来,将现在的实例设为活动对象,执行完 patch 操作之后,再将以前的活动对象重新设置回去。这样就保证了 createComponentInstanceForVnode 整个深度遍历过程中,我们在实例化子组件的时候能传入当前子组件的父 Vue 实例,并在 _init 的过程中,通过 vm.$parent 把这个父子关系保留。

function setActiveInstance(vm) {
      var prevActiveInstance = activeInstance;
      activeInstance = vm;
      return function () {
          activeInstance = prevActiveInstance;
      };
  }

然后进入 patch 方法。

patch

按照目前使用的例子来说,传入 patch 方法的 oldVnode<div id="app"></div>,传入的 vnodevue-component-1 这个 vnode

由于我们传入的 oldVnode 实际上是一个 DOM container,所以 isRealElement 为 true,接下来又通过 emptyNodeAt 方法把 oldVnode 转换成一个空的 VNode 对象

{
  children: [],
  data: {},
  elm: div#app,
  tag: "div",
  ...
}

然后得到节点的父元素 parentElmbody 元素。

 var oldElm = oldVnode.elm;
 var parentElm = nodeOps.parentNode(oldElm);

接下来会调用 createElm 方法,这个方法作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。

 createElm(
   vnode,             // vue-component-1 这个 vnode
   insertedVnodeQueue,       // []
   parentElm,            // html 中的 body 元素
   nodeOps.nextSibling(oldElm)      // oldElm 为 <div id="app"></div>,这里取到的它的下一个兄弟节点为换行文本节点
 );

createElm

  1. 这里的 APP 就是一个组件,所以这里我们进入 createComponent,看看里面的实现。

createComponent

if (isDef((i = i.hook)) && isDef((i = i.init))) {
    i(vnode, false /* hydrating */);
}

这个 i 最后取到的是 i.hook.init 方法,我们可以在 componentVNodeHooks 组件虚拟节点钩子变量中找到这个 init 方法:

init: function (vnode, hydrating) {
    if (vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive) {
        var mountedNode = vnode; 
        componentVNodeHooks.prepatch(mountedNode, mountedNode);
     } else {
        var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance));
         child.$mount(hydrating ? vnode.elm : undefined, hydrating);
     }
 },

创建组件实例的 createComponentInstanceForVnode 方法中会调用 vnodecomponentOptions\color{blue}{`componentOptions`} 属性,这个属性是我们在 rendercreateComponent 方法中添加的(在 renderpatch 过程中都各自有一个同名的 createComponent 方法),其中的 Ctor 方法会触发组件的 _init() 方法,所以子组件的实例化实际上就是在这个时机执行的。

return new vnode.componentOptions.Ctor(options)

传入的 options 为:

{
    parent: Vue_isComponent: true_parentVnode: VNode    // vue-component-1
}

但是在组件的 _init 方法中,由于没有 el ,所以不会执行里面的 $mount 方法,而是执行 child.$mount 方法成组件的 render:

ƒ anonymous(
) {
with(this){return _c('div',[_v(_s(message))])}
}

vnode:

{
  tag:'div',
  parent:{ tag: 'vue-component-1', ... },
  children:{{tag:undefined, text: 'Hello Vue!',children:undefined,...}}
}

真实 dom:

<div>Hello Vue!</div>
  1. createComponent 的最后会执行
insert(parentElm, vnode.elm, refElm);  // 将组件的真实dom插入body中

最后得到的真实 dom

  <div id="app"></div>
  <div>Hello Vue!</div>

然后销毁 oldVnode