源码分析 - Vue 初始化过程 (附思维导图)

208 阅读7分钟

初始化 构造函数 vue 的本质是一个 构造函数 ,我们 new Vue 的时候,肯定是通过它的构造函数,所以我们先找到它所在的目录 \vue-dev\src\core\instance\index.js 。

\vue-dev\src\core\instance\index.js

/*

  • @Author: 一尾流莺
  • @Description: Vue实际上就是一个用 Function 实现的类,我们只能通过 new Vue 去实例化它。
  • @Date: 2021-07-07 17:46:27
  • @LastEditTime: 2021-07-09 19:08:26
  • @FilePath: \vue-dev\src\core\instance\index.js */

// Vue 构造函数 Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法 function Vue (options) { if (process.env.NODE_ENV !== 'production' && // instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。 // Vue必须是new实例化出来的 es5实现class的方式(通过函数) !(this instanceof Vue) ) { // 如果不是Vue的实例走这里 warn('Vue is a constructor and should be called with the new keyword') } //Vue.prototype._init方法 该方法是在 initMixin 中定义的,其入参options就是我们定义的对象时传入的参数对象 this._init(options) }

/**

  • 执行xxxMixin方法,初始化相关的功能定义,这里仅仅是定义函数,后面实际用到再分析
  • 每一个Mixin都是向Vue的原型上添加一些属性或者方法 */

//合并配置 initMixin(Vue) //初始化 data、props、computed、watcher stateMixin(Vue) //初始化事件中心 eventsMixin(Vue) //初始化生命周期,调用声明周期钩子函数 lifecycleMixin(Vue) //初始化渲染 renderMixin(Vue)

复制代码 代码解读 ⭐ Vue 实际上就是一个用 Function 实现的类,我们只能通过 new Vue 去实例化它,然后会调用 this._init 方法。 ⭐ 为何 Vue 不用 ES6 的 Class 去实现呢?可以看到构造函数的下方执行了很多 xxxMixin 的函数调用,并把 Vue 当参数传入,它们的功能都是给 Vue 的 prototype 上扩展一些方法,Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有,这种方式是用 Class 难以实现的。这么做的好处是非常方便代码的维护和管理,这种编程技巧也非常值得我们去学习。 最后我们用一张思维导图总结一下

init的过程 接下来我们看一下 this._init(options) 发生了什么,_init 方法是在 initMixin 中向 Vue 的原型中添加的。

\vue-dev\src\core\instance\init.js

initMixin / _init /**

  • @description: 定义 Vue.prototype._init 方法
  • @param {*} Vue Vue 构造函数 */ export function initMixin (Vue: Class) {

/**

  • 给Vue的原型上挂载一个_init方法
  • 负责 Vue 的初始化过程 */ Vue.prototype._init = function (options?: Object) {

// ```````````````````````````````````````````````````第一部分````````````````````````````````````````````````` // 获取 vue 实例 const vm: Component = this // 每个 vue 实例都有一个 _uid,并且是依次递增的,确保唯一性 vm._uid = uid++ // vue实例不应该是一个响应式的,做个标记 vm._isVue = true

// ```````````````````````````````````````````````````第二部分`````````````````````````````````````````````````

/**
 * 处理组件配置项 
 * 对options进行合并,vue会将相关的属性和方法都统一放到vm.$options中,为后续的调用做准备工作。
 * vm.$option的属性来自两个方面,一个是Vue的构造函数(vm.constructor)预先定义的,一个是new Vue时传入的入参对象
 */
if (options && options._isComponent) {
/**
 * 如果是子组件初始化时走这里,这里只做了一些性能优化
 * 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
 */
  initInternalComponent(vm, options)
} else {
  /**
   * 合并配置项
   * 如果是根组件初始化走这里,,合并 Vue 的全局配置到根组件的局部配置,比如 Vue.component 注册的全局组件会合并到 根实例的 components 选项中
   * 至于每个子组件的选项合并则发生在两个地方:
   *   1、Vue.component 方法注册的全局组件在注册时做了选项合并 (全局API)
   *   2、{ components: { xx } } 方式注册的局部组件在执行编译器生成的 render 函数时做了选项合并,包括根组件中的 components 配置  (编译器)
   */
  vm.$options = mergeOptions(
    // 这里是取到之前的默认配置,组件 指令 过滤器等 也就是构造函数的options
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

// ```````````````````````````````````````````````````第三部分`````````````````````````````````````````````````

//在非生产环境下执行了initProxy函数,参数是实例;在生产环境下设置了实例的_renderProxy属性为实例自身
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}

//设置了实例的_self属性为实例自身
vm._self = vm
// 初始化组件实例关系属性, 比如 $parent、$children、$root、$refs 等  不是组件生命周期mounted,created...
initLifecycle(vm)
/**
 * 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,
 * 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
 */
initEvents(vm)
// render初始化 初始化插槽, 获取 this.slots , 定义this._c ,也就是createElement方法,平时使用的 h 函数
initRender(vm)
// 调用创建之前的钩子函数  执行 beforeCreate 生命周期函数
callHook(vm, 'beforeCreate')
// 注入初始化  初始化  inject 选项  得到 {key:val} 形式的配置对象  并对解析结果做响应式处理 ,并代理每个 key 到 vm 实例
initInjections(vm) // resolve injections before data/props
// 数据初始化  响应式原理的核心,处理 props methods computed data watch 等
initState(vm)
// 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
initProvide(vm) // resolve provide after data/props
// 调用创建完成的钩子函数  执行 created 生命周期函数
callHook(vm, 'created') 
//通过_init() 可以知道 beforeCreate 生命周期不可以访问数据  因为还没有初始化 但是可以拿到关系属性,插槽,自定义事件

// ```````````````````````````````````````````````````第四部分`````````````````````````````````````````````````

/**
 * 判断vm.$options有没有el  如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM
 * 存在el则默认挂载到el上 不存在的时候不挂载  需要手动挂载
 */
if (vm.$options.el) {
   // 调用 $mount 方法,进入挂载阶段
  vm.$mount(vm.$options.el)
}

} } 复制代码 initInternalComponent /**

  • @description: 性能优化 把组件传进来的一些配置赋值到vm.$options上 打平配置对象上的属性 减少运行时原型链的查找,提高执行效率
  • @param {*} vm 组件实例
  • @param {*} options 传递进来的配置 */

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {

//基于组件构造函数上的配置对象 创建vm.optionsconstopts=vm.options const opts = vm.options = Object.create(vm.constructor.options)

//```````````````把组件传进来的一些配置赋值到vm.$options上````````````````````∧ const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode

const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag

//```````````````把组件传进来的一些配置赋值到vm.$options上````````````````````∨

//如果有 render 函数, 将其赋值到vm.$options if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } } 复制代码 resolveConstructorOptions /**

  • @description: 解析实例constructor上的options属性,并合并基类选项
  • @param {*} Ctor 实例构造函数
  • @return {*} options 配置选项 */

export function resolveConstructorOptions (Ctor: Class) { //从实例构造函数上获取配置 options let options = Ctor.options if (Ctor.super) { /** * Ctor.super是通过Vue.extend构造子类的时候。Vue.extend方法会为Ctor添加一个super属性,指向其父类构造器 * 如果构造函数上有super 说明Ctor是Vue.extend构建的子类 换句话说就是检查是否有父级组件 * 然后再用递归的方式获取基类上的配置选项,也就是获取所有上级的options合集 */ const superOptions = resolveConstructorOptions(Ctor.super)

// Ctor.superOptions:父级组件的options  Vue构造函数上的options,如directives,filters,....
const cachedSuperOptions = Ctor.superOptions

if (superOptions !== cachedSuperOptions) {
  // 如果父级组件被改变过,更新superOption
  Ctor.superOptions = superOptions

  // 检查 Ctor.options 上是否有任何后期修改/附加选项
  const modifiedOptions = resolveModifiedOptions(Ctor)

  if (modifiedOptions) {
    //如果存在被修改或增加的选项,则合并两个选项
    extend(Ctor.extendOptions, modifiedOptions)
  }

  // 选项合并,将合并结果赋值为 Ctor.options
  options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
  
  if (options.name) {
    options.components[options.name] = Ctor
  }
}

} //当Ctor.super不存在时,如通过new关键字来新建Vue构造函数的实例 直接返回基础构造器的options return options }

复制代码 resolveModifiedOptions /**

  • @description: 检查是否有任何后期修改/附加选项
  • @param {*} Ctor 实例构造函数
  • @return {*} modified */

function resolveModifiedOptions (Ctor: Class): ?Object { // 声明修改项 let modified // 获取构造函数选项 const latest = Ctor.options // 密封的构造函数选项,备份 const sealed = Ctor.sealedOptions // 对比两个选项,记录不一致的选项 for (const key in latest) { if (latest[key] !== sealed[key]) { if (!modified) modified = {} modified[key] = latest[key] } } //返回修改项 return modified }

复制代码 代码解读 通过代码,我们把 _init 的过程分成四个部分进行分析。这里暂时先知道干了这么些事情,具体的代码后面会详细分析。 第一部分 ⭐ 每个 vue 实例都有一个 _uid,并且是依次递增的,确保唯一性。 ⭐ vue 实例不应该是一个响应式的,做个标记。 第二部分 ⭐ 如果是子组件,将组件配置对象上的一些深层次属性放到 vm.options选项中,以提高代码的执行效率。⭐如果是根组件,对options进行合并,vue会将相关的属性和方法都统一放到vm.options 选项中,以提高代码的执行效率。 ⭐ 如果是根组件,对 options 进行合并,vue 会将相关的属性和方法都统一放到 vm.options 中。vm.options的属性来自两个方面,一个是Vue的构造函数vm.constructor预先定义的,一个是newVue时传入的入参对象。第三部分⭐initProxy/vm.renderProxy在非生产环境下执行了initProxy函数,参数是实例;在生产环境下设置了实例renderProxy属性为实例自身。⭐设置了实例self属性为实例自身。⭐initLifecycle初始化组件实例关系属性,比如options 的属性来自两个方面,一个是 Vue 的构造函数 vm.constructor 预先定义的,一个是 new Vue 时传入的入参对象。 第三部分 ⭐ initProxy / vm._renderProxy 在非生产环境下执行了 initProxy 函数,参数是实例;在生产环境下设置了实例的 _renderProxy 属性为实例自身。 ⭐ 设置了实例的 _self 属性为实例自身。 ⭐ initLifecycle 初始化组件实例关系属性 , 比如 parent、childrenchildren、root、refs等(不是组件生命周期mounted,created...)⭐initEvents初始化自定义事件。⭐initRender初始化插槽,获取this.slots,定义this.c,也就是createElement方法,平时使用的h函数。⭐callHook执行beforeCreate生命周期函数。⭐initInjections初始化inject选项⭐initState响应式原理的核心,处理propsmethodscomputeddatawatch等。⭐initProvide解析组件配置项上的provide对象,将其挂载到vm.provided属性上。⭐callHook执行created生命周期函数。第四部分⭐如果有el属性,则调用vm.refs 等 (不是组件生命周期 mounted , created...) ⭐ initEvents 初始化自定义事件。 ⭐ initRender 初始化插槽 , 获取 this.slots , 定义 this._c , 也就是 createElement 方法 , 平时使用的 h 函数。 ⭐ callHook 执行 beforeCreate 生命周期函数。 ⭐ initInjections 初始化 inject 选项 ⭐ initState 响应式原理的核心 , 处理 props、methods、computed、data、watch 等。 ⭐ initProvide 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上。 ⭐ callHook 执行 created 生命周期函数。 第四部分 ⭐ 如果有 el 属性,则调用 vm.mount 方法挂载 vm ,挂载的目标就是把模板渲染成最终的 DOM。 ⭐ 不存在 el 的时候不挂载 , 需要手动挂载。 最后我们用一张思维导图总结一下

参考 Vue.js 技术揭秘 精通 Vue 技术栈的源码原理 本文由 李永宁 教程结合自己的想法整理而来,在此特别感谢前辈。

作者:一尾流莺 链接:juejin.cn/post/698458… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。