从创建实例到模板编译前Vue都做了些什么?

写在前面

Vue构造函数的创建过程 一文中介绍了 Vue构造函数 的创建过程,其中第一个对 Vue构造函数 进行成员添加的就是 initMixin(Vue),该调用内创建了 _init 方法。可以说,Vue实例 的大门就是 _init 方法,因此,我们从这个方法入手,一步一步剖析 vm 是如何生成的。

/*

{{msg}}

*/
// Vue.extend 手动挂载组件 const ExtendUse = Vue.extend({ props: ['msg'], template: `

ExtendUse said: {{msg}}

`
, }) const ChildExtend = new ExtendUse({ propsData: { msg: 'hello Extend' } }).$mount('#extendUse') // Vue.component 自动挂载组件 const ChildComponent = Vue.extend({ props: ['msg'], template: `

child's father said: {{msg}}

`
, }) Vue.component('child-component', ChildComponent) // Vue 实例 const vm = new Vue({ el: '#app', data: { msg: 'hello Vue', } })

按照惯例,我先将 _init 的源码简写一下,并且划分一下步骤:

Vue.prototype._init = function (options) {
  // 步骤 - 1
  const vm = this
  vm._uid = uid++
  vm._isVue = true
  // 步骤 - 2
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  // 步骤 - 3
  if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
  } else {
    vm._renderProxy = vm
  }
  vm._self = vm
  // 步骤 - 4
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm)
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
  // 步骤 - 5
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

步骤 - 1

在这段代码中,我们首要了解的是 vm 指向了谁?
从它的调用 function Vue(options) { this._init(options) } 中我们可以了解到,Vue 是一个构造函数,而 _init 方法又不是该构造函数的静态方法,因此 this 指向了实例,所以 const vm = this 这一步其实就是将实例本身赋值给了 vm。

再具体点?const app = new Vue(options) 这里的 app 就是 vm 的值。

接着,在实例身上添加上 _uid 和 _isVue 的标识,记录当前实例是第 (_uid + 1) 个 Vue 实例对象。

步骤 - 2

这段代码是一个条件判断,判断的是是否是一个组件。那怎样的存在算是一个组件呢?Vue.extend 手动挂载上去的算不算一个组件呢?

我就先兜个底,只有存在于 vm.options.components 中的才算组件,且能进入 if 判断之中。这就意味着,要么 Vue.component(id, definition) 定义的对象,要么 new Vue({ components: { id: definition } }) 定义的对象,其他方式都不具备 _isComponent 属性。因此,即使是 Vue.component 方法内同样也调用了的 Vue.extend 方法,在手动挂载时也不算作组件,不具备 _isComponent 属性。

那这段代码的意义是什么呢?简而言之,就是将 Vue构造函数 中的成员变量 options 和我们传入的 options 合并然后挂载到实例的 $options 属性上。

initInternalComponent

不知道大家想没想过,组件为什么会进到 _init 中来,哪儿定义了它也可以进来进行初始化操作?

如果你没有忘记 详解Vue.component和Vue.extend 一文中讲过,Vue.component 中会调用一次 Vue.extend,将生成的实例存入 vm.options.components 之中。而在 Vue.extend 中我们曾定义过一个 VueComponent构造函数,这个构造函数继承了 Vue构造函数,因此也有 _init 方法,在 VueComponent构造函数 的 constructor 中又恰好调用了 this._init(options),所以套了一层又一层,剥丝抽茧后你应该就能明白为什么组件可以进来了吧。

那么,这个方法内做了些什么?当你进入函数体你会发现,options 中哪来的这么多属性???我丢,见都没见过啊,一脸懵逼逐渐变成N脸懵逼。所以我们先按下不表,因为其中涉及到模板编译部分,扯得太远回不来就麻烦了。

resolveConstructorOptions

既然不讲组件的 options 合并,那总得讲讲 new (Vue.extend(extendOptions))(options) / new Vue(options) 的 options 合并啊,讲讲讲,这不就来了么。先声明一下,以下 vm 指的是 Vue构造函数 的实例,componentVM 指的是 Vue.extend 返回的构造函数的实例。

这个方法本质上是为了得到 Vue构造函数 身上的 options。

如果传入的 constructor 没有 super 属性,则说明当前实例是 vm,直接返回 vm.constructor.options。

如果传入的 constructor 拥有 super 属性,则说明当前实例是 componentVM,那么当前的 vm.constructor.options 就是 VueComponent.options。如果 VueComponent.superOptions 和 VueComponent.super.options 不相等时会将 VueComponent.superOptions 更新,得到最新的 VueComponent.options 然后返回,否则直接返回 VueComponent.options。

但是,明明 superOptions 和 super.options 都是指向的 Vue构造函数,什么时候会不相等呢?大家可以亲自调试一下以下代码:

const Score = Vue.extend({
  template: '

{{firstName}} {{lastName}}: {{score}}

'
}) Vue.mixin({ data() { return { firstName: 'Walter', lastName: 'White' } } }) Score.mixin({ data: function () { return { score: '99' } } }) new Score().$mount('#app')

mergeOptions

首先,我们需要了解这个方法做了什么。

function mergeOptions (parent, child, vm) {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }
  if (typeof child === 'function') {
    child = child.options
  }
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
checkComponents

判断 (vm | componentVM).options.components: { id: definition } 的 id 是否等于 slot / component 或其他原生 HTML 标签,如果是则报错。

normalizeProps

当 (vm | componentVM) 中传入的 options 中存在 props 时激活该方法,目的是为了格式化 props 中的属性。

如果 options.props 是一个数组,则将 options.props 的值统一成 { item: { type: null } }。

如果 options.props 是一个对象,则将 options.props 的值统一成 { key: { type: value } }。

当 key 是以短横线命名法( name-space )命名时将其转换成驼峰命名法( nameSpace )

// 如果 options.props 是一个数组
new Vue({
  props: ['msg', 'aeo-rus']
})
/*
props: {
  msg: {
    type: null
  },
  aeoRus: {
    type: null
  }
}
*/
// 如果 options.props 是一个对象
new Vue({
  props: {
    msg: {
      type: String,
      default: 'hello Vue'
    },
    'aeo-rus': {
      default: 'aeorus'
    },
  }
})
/*
props: {
  msg: {
    type: String,
    default: 'hello Vue'
  },
  aeoRus: {
    default: 'aeorus'
  }
}
*/
normalizeInject

当 (vm | componentVM) 中传入的 options 中存在 inject 时激活该方法,目的是为了格式化 inject 中的属性。

如果 options.inject 是一个数组,则将 options.inject 的值统一成 { item: { from: item } }。

如果 options.inject 是一个对象,则将 options.inject 的值统一成 { key: { from: value } } ( 当 value 也是对象时则统一成 { from: key, value.k: value.v } )。

new Vue({
  inject: ['onload', 'reload']
})
/*
inject: {
  onload: {
    from: 'onload'
  },
  reload: {
    from: 'reload'
  }
}
*/
new Vue({
  inject: {
    onload: {
      from: 'onlaunch',
      default: 'onload'
    },
    reload: 'reload'
  }
})
/*
inject: {
  onload: {
    from: 'onlaunch',
    default: 'onload'
  },
  reload: {
    from: 'reload'
  }
}
*/
normalizeDirectives

当 (vm | componentVM) 中传入的 options 中存在 directives 时激活该方法,目的是为了格式化 directives 中的指令。

将 options.directives 的值统一成 { key: { bind: value, update: value } } ( 有可能 value 是一个方法 ) 。

new Vue({
  directives: {
    focus: {
      inserted(el) {
        el.focus()
      }
    }
  }
})
/*
directives: {
  focus: {
    inserted: el => {
      el.focus()
    }
  }
}
*/
new Vue({
  directives: {
    focus: el => {
      el.focus()
    }
  }
})
/*
directives: {
  focus: {
    bind: el => {
      el.focus()
    },
    update: el => {
      el.focus()
    }
  }
}
*/
extends / mixins

当 (vm | componentVM) 中传入的 options 中存在 extends / mixins 时进入条件判断,递归 mergeOptions 方法,将 extends / mixins 的对象 ( 实质上就是 options ) 与 (vm | componentVM) 进行合并。

mergeField
const options = {}
let key
for (key in parent) {
  mergeField(key)
}
for (key in child) {
  if (!hasOwn(parent, key)) {
    mergeField(key)
  }
}
function mergeField (key) { 
  const strat = strats[key] || defaultStrat
  options[key] = strat(parent[key], child[key], vm, key)
}

这个方法极其复杂,本质上就是合并了构造函数身上的属性和 options 上的属性。根据 key 的不同判断是返回 options 上的属性抑或以构造函数身上的属性的值为原型创造的新的拥有 options 上的属性的值的对象。

// 比如当 key 为 components 时返回
options = {
  components: {
    ChildComponent,
    __proto__: {
      components: {
        KeepAlive,
        Transition,
        TransitionGroup,
      },
    },
  },
}

步骤 - 3

我们可以看到在这段代码中多了一个新的属性 _renderProxy,从字面意义上来看应该称之为渲染代理对象,事实上它确实参与了渲染流程,在调用 render 方法获取 vnode 时会将 render 方法的内部指针指向它。

但随之而来产生了两个问题: 1.为什么要有渲染代理对象?2.为什么开发环境和生产环境要用不同的渲染代理对象?

其实并不复杂,我们通过以下代码来进行解答:

/*
目录: core/instance/render.js
vnode = render.call(vm._renderProxy, vm.$createElement)
*/
render(h) {
  h('div', 'hello vue')
}

我们通过以上代码可以发现,h函数 其实就是 vm.$createElement,但是因为调用的时候是直接使用 h() 来调用的,因此它内部的指针应该指向 window 而不是当前实例,所以我们需要通过 call 的方式将内部指针指向当前实例,如此就可以使用 this 获取到该实例身上 options 中其他的属性了。

那么开发环境和生产环境为什么要用不同的渲染代理对象呢?我们可以发现在开发环境中其实是调用了 initProxy 方法创建的渲染代理对象,其中判断了是否存在 Proxy 对象,通过 Proxy 对象拦截 this.xxx 中的 xxx 是否是一个非法的属性,有利于我们开发时的操作不谨慎。但是生产环境时就没必要了,毕竟我们不会将错误保留到线上。

步骤 - 4

这又是一连串的调用,像极了 core/global-api/index.js 中的操作,但是请不要忽略入参,core/global-api/index.js 中传入的是 Vue,而这里传入的是 vm。这意味着,core/global-api/index.js 中是对 Vue构造函数 进行成员的添加,而这里是对 vm 进行属性的添加。

initLifecycle

初始化一系列属性,如果当前实例是组件则将 $parent / $root 绑定上组件所归属的 vm。

vm = {
  __proto__: {
    ...Vue,
  },
  _uid,
  _isVue,
  $options,
  _renderProxy,
  _self,
  /* new add start */
  $parent,
  $root,
  $children: [],
  $refs: {},
  _watcher: null,
  _inactive: null,
  _directInactive: false,
  _isMounted: false,
  _isDestroyed: false,
  _isBeingDestroyed: false,
  /* new add end */
}

initEvents

初始化事件中心,如果当前实例是组件则会对父组件的事件监听进行重新绑定。

vm = {
  __proto__: {
    ...Vue,
  },
  _uid,
  _isVue,
  $options,
  _renderProxy,
  _self,
  $parent,
  $root,
  $children: [],
  $refs: {},
  _watcher: null,
  _inactive: null,
  _directInactive: false,
  _isMounted: false,
  _isDestroyed: false,
  _isBeingDestroyed: false,
  /* new add start */
  _events: {},
  _hasHookEvent: {},
  /* new add end */
}

initRender

初始化存放和生成 虚拟DOM 的属性和方法。

如果当前实例不是组件,则将 $attrs / $listeners 设置为空对象添加到实例上。

如果当前实例是组件,则会将父组件 虚拟DOM 上的 attrs 添加响应式后放到自身实例的 $attrs 属性上;再将父组件的事件监听添加响应式后放到自身实例的 $listeners 属性上。

vm = {
  __proto__: {
    ...Vue,
  },
  _uid,
  _isVue,
  $options,
  _renderProxy,
  _self,
  $parent,
  $root,
  $children: [],
  $refs: {},
  _watcher: null,
  _inactive: null,
  _directInactive: false,
  _isMounted: false,
  _isDestroyed: false,
  _isBeingDestroyed: false,
  _events: {},
  _hasHookEvent: {},
  /* new add start */
  $vnode: null,
  _vnode: null,
  $slots,
  $scopedSlots,
  _c() {},
  $createElement() {},
  $attrs,
  $listeners,
  /* new add end */
}

beforeCreate

调用 beforeCreate 生命周期。

initInjections

由于 步骤 - 2 -> resolveConstructorOptions -> mergeOptions -> normalizeInject 这一过程中在实例的 $options 中挂载了 inject 这个属性的缘故,这个方法中就不需要再进行添加,只是单纯地为 inject 中的对象添加了响应式。

vm = {
  __proto__: {
    ...Vue,
  },
  _uid,
  _isVue,
  $options: {
    /* update start */
    inject: {}
    /* update end */
  },
  _renderProxy,
  _self,
  $parent,
  $root,
  $children: [],
  $refs: {},
  _watcher: null,
  _inactive: null,
  _directInactive: false,
  _isMounted: false,
  _isDestroyed: false,
  _isBeingDestroyed: false,
  _events: {},
  _hasHookEvent: {},
  $vnode: null,
  _vnode: null,
  $slots,
  $scopedSlots,
  _c() {},
  $createElement() {},
  $attrs,
  $listeners,
}

initState

这一步大家肯定熟,只要在网上看过 Vue 源码解析啊响应式原理啊之类视频的应该都了解,这里就是网传的 Vue 的 constructor 中的内容。

即初始化 props / methods / data / computed / watch。

vm = {
  __proto__: {
    ...Vue,
  },
  _uid,
  _isVue,
  $options: {
    inject: {}
  },
  _renderProxy,
  _self,
  $parent,
  $root,
  $children: [],
  $refs: {},
  _watcher: null,
  _inactive: null,
  _directInactive: false,
  _isMounted: false,
  _isDestroyed: false,
  _isBeingDestroyed: false,
  _events: {},
  _hasHookEvent: {},
  $vnode: null,
  _vnode: null,
  $slots,
  $scopedSlots,
  _c() {},
  $createElement() {},
  $attrs,
  $listeners,
  /* new add start */
  _watchers,
  _props, // 如果 options 上有 props
  ...options.methods, // 如果 options 上有 methods
  ...options.data, // 如果 options 上有 data
  _data: {
    ...options.data
  }, // 如果 options 上有 data
  ...options.computed, // 如果 options 上有 computed
  /* new add end */
}

initProvide

vm = {
  __proto__: {
    ...Vue,
  },
  _uid,
  _isVue,
  $options: {
    inject: {}
  },
  _renderProxy,
  _self,
  $parent,
  $root,
  $children: [],
  $refs: {},
  _watcher: null,
  _inactive: null,
  _directInactive: false,
  _isMounted: false,
  _isDestroyed: false,
  _isBeingDestroyed: false,
  _events: {},
  _hasHookEvent: {},
  $vnode: null,
  _vnode: null,
  $slots,
  $scopedSlots,
  _c() {},
  $createElement() {},
  $attrs,
  $listeners,
  _watchers: [], // 如果 options 上有 watch 则会存在 Watcher 的实例
  _props, // 如果 options 上有 props
  ...options.methods, // 如果 options 上有 methods
  ...options.data, // 如果 options 上有 data
  _data: {
    ...options.data
  }, // 如果 options 上有 data
  ...options.computed, // 如果 options 上有 computed
  /* new add start */
  _provided, // 如果 options 上有 provide
  /* new add end */
}

created

调用 created 生命周期。

步骤 - 5

到目前为止,我们都只是初始化的工作,大部分都是挂载某某属性,要说真的做了什么业务相关的事情,那大概就是 initState 这部分了,其中为数据添加了响应式,当有计算属性和侦听器时还顺便做了依赖收集。

但是,我们是否还没看到对于 DOM 的处理?对了,这就是为什么在 beforeCreate 和 created 生命周期里无法获取 this.$refs 的原因,我们还没有进入模板编译,那么 DOM 自然就还没有生成,没有生成的东西怎么可能在这些钩子里被获取呢?

自此,_init 就告一段落了,接下来我们即将进入下一个流程 ———— 模板编译 -> ゲットスタート ( get start )。

你可能感兴趣的:(不当切图仔,vue.js,javascript,node.js)