通过案例读 vue2 源码01

121 阅读4分钟

案例

<!DOCTYPE html>
<body>
  <div id="app"></div>
</body>
<script src="vue.js"></script>
<script>

let ButtonCounter = {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>',
  beforeCreate() {
    console.log('child beforeCreate')
  },
  created() {
    console.log('child created')
  },
  beforeMount() {
    console.log('child beforeMount')
  },
  mounted() {
    console.log('child mounted')
  },
  beforeUpdate() {
    console.log('child beforeUpdate')
  },
  updated() {
    console.log('child updated')
  },
  beforeDestroy() {
    console.log('child beforeDestroy')
  },
  destroyed() {
    console.log('child destroyed')
  },
}

let childComp = {
  template: '<div>{{msg}}<button-counter/></div>',
  components: {
    'button-counter': ButtonCounter
  },
  data() {
    return {
      msg: 'QAAA'
    }
  },
  beforeCreate() {
    console.log('parent beforeCreate')
  },
  created() {
    console.log('parent created')
  },
  beforeMount() {
    console.log('parent beforeMount')
  },
  mounted() {
    console.log('parent mounted')
  },
  beforeUpdate() {
    console.log('parent beforeUpdate')
  },
  updated() {
    console.log('parent updated')
  },
  beforeDestroy() {
    console.log('parent beforeDestroy')
  },
  destroyed() {
    console.log('parent destroyed')
  },
}

let app = new Vue({
  el: '#app',
  render: h => h(childComp),
  beforeCreate() {
    console.log('init beforeCreate')
  },
  created() {
    console.log('init created')
  },
  beforeMount() {
    console.log('init beforeMount')
  },
  mounted() {
    console.log('init mounted')
  },
  beforeUpdate() {
    console.log('init beforeUpdate')
  },
  updated() {
    console.log('init updated')
  },
  beforeDestroy() {
    console.log('init beforeDestroy')
  },
  destroyed() {
    console.log('init destroyed')
  },
})
</script>
init beforeCreate
init created
init beforeMount
parent beforeCreate
parent created
parent beforeMount
child beforeCreate
child created
child beforeMount
child mounted
parent mounted
init mounted

分析

初始化 Vue

new Vue()

  1. 通过外部 new Vue() 创建 vue 实例,进入 _init() 方法中

通过 new 创建实例会进行以下四步:

  1. 创建一个新对象
  2. 将 this 指针指向这个新对象
  3. 执行构造函数中的代码
  4. 返回新对象
  1. 所以在 _init 方法的第一步,我们就将 vm = this,此时的 this 就是指向 vue 实例的。
  2. 接着在 vm 上添加了很多属性。
  3. 此时传入的 options 没有 _isComponent 属性,所以使用 mergeOptions 进行配置合并,并赋值给 vm.$options 属性。
  4. 然后初始化生命周期,事件总线和 render 方法,接着调用 callHook 执行 beforeCreated 这个生命周期的事件。

init beforeCreate

我们浅看一下 callHook 中的操作:

var prev = currentInstance   // 开始时 currentInstance 为 null
setCurrentInstance(vm)       // 设置 currentInstance 为 当前的 vue 实例
// ... 执行 vm.$options[hook]
setCurrentInstance(prev)     // 重置 currentInstance 为 null
  1. 再通过 initInjections,initProvide,还有 initState 初始化 data,props,methods 等数据,在这之后,调用 callHook 执行 created 这个生命周期的事件。

init created

  1. 由于传入的 options 中有 el: '#app' 属性,所以进入 vm.$mount(vm.$options.el) 执行挂载的过程。一般只有 new Vue() 的时候会传入 el,组件初始化的时候不会走到这。

vm.$mounted

  1. 根据 el 找到 html 中的真实 Dom
<div id="app"></div>

一旦写了 render,这里写的 el,将没有任何意义,在最终生成的 dom 中不会存在。

  1. $mount 开始的时候,有 render 用手写的 render,如 h => h(childComp),没有 render 则将 el 或者 template 属性转换成 render, 如 _c('button-counter')_c('div')。而此案例中传入的本来就是 render
 h => h(childComp)
  1. 从而直接进入 mountComponent 方法,设置 vm.$el 为真实的 Dom 节点 <div id="app"></div>,紧接着调用 callHook 执行 beforeMount 这个生命周期的事件。

init beforeMount

  1. 后面创建了一个监听,先不用看这个 watcher,我们只需要看到 updateComponent 方法,在初始化渲染的时候就执行了这个方法, vm._update(vm._render(), undefined) 中使用 _render 方法将 render 函数转换为 vnode,在 _update 方法中将 vnode 转换为真实 dom
  2. 当执行完 updateComponent 方法之后,会判断 vm.$vnode 是否为 null,如果为空会设置 vm._isMounted = true,并执行 mounted 生命周期。这块内容我们稍后再看,先进入 updateComponent 方法看看。

vm._render()

  1. _render 方法中,将 vm.$vnode 赋值为 vm.$options._parentVnode,当前实例的父虚拟节点当然为 undefined

  2. 设置 currentInstance=vm,设置成了当前的 vue 实例。

  3. 然后执行 render,生成 vnode:

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

render 方法为 h=>h(childComp),使用 call 方法调用,即 hvm.$createElement 方法。 4. 即执行 vm.$createElement(childComp),也就是传入的 tag 参数为 childComp 对象,由于是手写的 render,所以 alwaysNormalizetrue,随后调用 _createElement 方法。

  • 如果传入的 tag 是对象,则直接执行 createComponent 方法,将传入的 tag 对象继续往下传,如 h => h(childComp)
  • 如果传入的 tag 是字符串,且是保留的标签,那么直接通过 new Vnode 创建出对应的 vnode,如 _c('div')
  • 如果传入的 tag 是字符串,但是不是保留标签,那么会在组件的 components 属性中查找,找到对应标签名的组件对象再传入 createComponent 执行,如 _c('button-counter')

_createElement 方法中由于 childrenundefined,所以不需要看 normalizeChildren 方法。由于传入的 tagchildComp 对象,会进入 createComponent 方法。

总结一下,当手写 render 的时候,传入的如果是对象或者未知标签,那么就会被当作组件处理。当是由 template 生成的 render,那么在 template 中的未知标签的元素会被当作组件处理。

  1. 执行 createComponent 生成组件的 vnode
vnode = createComponent(tag, data, context, children);
function createComponent(Ctor, data, context, children, tag){...}

createComponent 接收了一个参数 Ctor,就是我们传入的 tag 对象,如 childComp 对象,

  • 首先我们将这个 Ctor 对象进行包装,将其设置为一个 VueComponent 方法对象,继承自 Vue 对象,并将传入的 childComp 对象 mergeVueComponent 对象的 extendOptionsoptions 属性上。
  • 然后使用 installComponentHooks 生成包含 hook 还有 on 属性的 data 对象,hook 中包含 initprepatchinsertdestroy 四个方法。
  • 最后通过 Ctordata 生成的 vnode,其 tagvue-component-1,组件的 vnode 没有 children
  1. 生成的 childComp 组件的 vnode 后,返回 _render 方法中,将组件的 vnodeparent 属性设置为 _parentVnode,当前为 null。然后将 _render 方法生成的 vnode,传入 _update 方法生成真实的 Dom

  2. 最终生成的 vnode 为:

{
  tag: "vue-component-1",
  data: { on: undefined, hook: { init: f, prepatch: f, insert: f, destroy: f,} },
  children: undefined,       // 组件的 vnode 没有 children
  parent: undefined,
  componentOptions: {
    // Ctor 是一个 VueComponent 方法
    Ctor: {
      cid: 1,
      options: {},  // childComp 对象的属性都 merge 到了上面
      ...
    }
  },
  ...
}

vm._update(vnode)

  1. _update 方法中,将 preEl = vm.$el(<div id="app"></div>),prevVnode = vm._vnode,当前的 vm._vnodenull
  2. 将当前 vm(Vue) 设置为了全局变量 activeInstance,且将 vm 保存在了局部作用域中的 prevActiveInstance 变量中,在执行完 __patch__ 方法后会将保存的 vm 重新设置回 activeInstance 变量中。
  3. vm._vnode 设置为了刚生成的 vue-component-1 组件的 vnode。到这里我们总结一下,vm._vnoode 中存储的是这个 vm 实例对应的虚拟节点,而 vm.$vnode 中存储的是父节点的虚拟节点
  4. 然后执行 __patch__ 方法,传入了 vm.$el 和生成的 vnode
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
function patch(oldVnode, vnode, hydrating, removeOnly){ ... }

patch 方法中:

  • 根据传入的 oldVnodenodeType 判断其是否是一个真实元素。

在 Dom 中有很多 nodeType,比如元素节点的 nodeType 为 1,属性节点的 nodeType 为 2,文本节点的 nodeType 为 3,注释节点的 nodeType 为 8。。。

当前的 oldVnode<div id="app"></div>,所以其为 nodeType 为 1 的真实元素。

  • 如果 oldVnode 是真实节点,那么需要进行转换为虚拟节点,最终得到的 oldVnode 就是根据真实节点生成的 tagdiv 的虚拟节点,其中的 elm 属性保存了真实节点。
  • oldVnode.elm 也就是传过来的 vm.$el 存入 oldElm 变量中,找到 vm.$el 的父元素存入 parentElm 变量中,当前为 body 元素。oldVnode 是有 elm 属性的,但是 vnodeelm 属性还是 undefined
  • 最后进入 createElm 方法,传入通过 _render 生成的 vnode (vue-component-1),oldVnode (div) 的父节点 body 和兄弟节点等。
  1. 调用 createElm 方法 在这个方法里会调用和 render 过程同名createComponent 方法将 vnode 转换为真实的 dom 节点,为了清晰表示,以后 render 阶段的我们称为 RcreateComponentupdate 阶段的我们称为 UcreateComponent,。

  2. 调用 UcreateComponent 方法

  • UcreateComponent 方法中创建组件的时候,先查找 vnodedata.hook.init 方法,有的话就去执行,这个方法是我们在 RcreateComponent 方法里就存入的,也就是只有组件 vnodedata.hook.init 方法,因为只有组件才会进入这个方法,非组件直接执行的 new Vnode()
  • 调用 init 方法,传入 vnodefalse
  • init 方法中因为 vnode.componentInstance 不存在,所以调用了 createComponentInstanceForVnode 方法创建 vnode.componentInstance,传入 vnodeactiveInstance(vm)。
var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance))
  • 执行 createComponentInstanceForVnode 方法,这个方法中又执行了我们存入 vnodecomponentOptions 属性中的 Ctor 方法,也就是 extend 期间创建的 Sub 这个 VueComponent 方法,可以对照前面生成的 vnode 来看。
var options = {
    _isComponent: true,
    _parentVnode: vnode,
    parent: parent
};
new vnode.componentOptions.Ctor(options)  // new VueComponent(options)

最后的 new VueComponent(options) 就进入子组件了,不要忘记,等子组件渲染完我们要回到这个地方继续执行的。