【源码学习】Vue 初始化过程 (附思维导图)

我心飞翔 分类:vue

声明

🔊 本文是开始学习 Vue 源码的第二篇笔记,当前的版本是 2.6.14 。如果对你有一点点帮助,请点赞鼓励一下,如果有错误或者遗漏,请在评论区指出,非常感谢各位大佬。

🔊 代码基本上是逐行注释,由于本人的能力有限,很多基础知识也进行了注释和讲解。由于源码过长,文章不会贴出完整代码,所以基本上都是贴出部分伪代码然后进行分析。

🔊 从本篇文章开始,可能会出现暂时看不懂的地方,是因为还没有学习前置知识,不必惊慌,只需知道存在这样一个知识点,接着向下看,看完了前置知识,回过头来再看这里就一目了然了。

初始化

构造函数

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)
//stateMixin主要定义了$data,$props,$set,$delete,$watch,并且$data,$props是只读属性。
stateMixin(Vue)
//初始化事件中心
eventsMixin(Vue)
//初始化生命周期,调用声明周期钩子函数
lifecycleMixin(Vue)
//初始化渲染
renderMixin(Vue)

代码解读

Vue 实际上就是一个用 Function 实现的类,我们只能通过 new Vue 去实例化它,然后会调用 this._init 方法。

⭐ 为何 Vue 不用 ES6Class 去实现呢?可以看到构造函数的下方执行了很多 xxxMixin 的函数调用,并把 Vue 当参数传入,它们的功能都是给 Vueprototype 上扩展一些方法,Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有,这种方式是用 Class 难以实现的。这么做的好处是非常方便代码的维护和管理,这种编程技巧也非常值得我们去学习。

最后我们用一张思维导图总结一下

【源码学习】Vue 初始化过程 (附思维导图)

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<Component>) {

  /**
   * 给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 &amp;&amp; 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.$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<Component>) {
  //从实例构造函数上获取配置 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<Component>): ?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 中。vm.options 的属性来自两个方面,一个是 Vue 的构造函数 vm.constructor 预先定义的,一个是 new Vue 时传入的入参对象。

第三部分

initProxy / vm._renderProxy 在非生产环境下执行了 initProxy 函数,参数是实例;在生产环境下设置了实例的 _renderProxy 属性为实例自身。

⭐ 设置了实例的 _self 属性为实例自身。

⭐ initLifecycle 初始化组件实例关系属性 , 比如 parent、children、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.$mount 方法挂载 vm ,挂载的目标就是把模板渲染成最终的 DOM

⭐ 不存在 el 的时候不挂载 , 需要手动挂载。

最后我们用一张思维导图总结一下

【源码学习】Vue 初始化过程 (附思维导图)

参考

Vue.js 技术揭秘

精通 Vue 技术栈的源码原理

本文由 李永宁 教程结合自己的想法整理而来,在此特别感谢前辈。

回复

我来回复
  • 暂无回复内容