Vue源码解读(一)初始化流程

178 阅读2分钟

v2-9995d3018a2160dbebbd9e58ada2803f_1440w.jpeg

前言

因最近闲来无事,从本文开始重新学习vue源码,如果有错误或遗漏或不对的地方,请在评论区指出,非常感谢各位大佬。

正文

相信小伙伴们在面试的时候,或多或少都有碰到过被面试官问到vue初始化过程,今天我们就通过源码来解析下new Vue(options)到底发生了什么。

初始化源码入口

找源码入口最简单的办法,首先我们需要一个例子实例化一下vue,通过打断点就可以很明确的知道代码的执行流程。还有一个办法就是通过全局搜vue的构造函数在那声明的也可以找到入口,这篇文章我们以编写代码示例为基础找入口。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue源码解析</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    message: {{ message }}
  </div>
</body>
<script>
  debugger
  const app = new Vue({
    el: '#app',
    data: {
      message: 'Hello Vue!'
    }
  })
</script>
</html>

image.png 通过debugger我们可以看到vue的构造函数在/vue/src/core/instance/index.ts文件,下面我们看下构造函数都做了些什么。

源码解读

入口文件/vue/src/core/instance/index.ts

import { initMixin } from './init'

function Vue(options) {
  // instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // Vue.prototype._init 方法在initMixin声明
  this._init(options)
}

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

通过这段代码我们可以知道初始化是_init方法,我们来看下_init方法都有什么

Vue.prototype._init

文件位置:vue/src/core/instance/init.ts

export function initMixin (Vue: Class<Component>) {
  // 负责 Vue 的初始化过程
  Vue.prototype._init = function (options?: Object) {
    // vue 实例
    const vm: Component = this
    // 每个 vue 实例都有一个 _uid,并且是依次递增的
    vm._uid = uid++
    
    // 这段代码是用来检测性能,使用需要借助谷歌插件Vue Performance Devtool
    let startTag, endTag
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // avoid instances from being observed
    vm.__v_skip = true
    // effect scope
    vm._scope = new EffectScope(true /* detached */)
    // 处理组件配置项
    if (options && options._isComponent) {
      /**
       * 每个子组件初始化时走这里,这里只做了一些性能优化
       * 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
       */
      initInternalComponent(vm, options)
    } else {
      /**
       * 初始化根组件时走这里,合并 Vue 的全局配置到根组件的局部配置,比如 Vue.component 注册的全局组件会合并到 根实例的 components 选项中
       * 至于每个子组件的选项合并则发生在两个地方:
       *   1、Vue.component 方法注册的全局组件在注册时做了选项合并
       *   2、{ components: { xx } } 方式注册的局部组件在执行编译器生成的 render 函数时做了选项合并,包括根组件中的 components 配置
       */
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      // 设置代理,将 vm 实例上的属性代理到 vm._renderProxy
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    
    /* istanbul ignore else */
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    
    // expose real self
    vm._self = vm
    // 初始化组件实例关系属性,比如 $parent、$children、$root、$refs 等
    initLifecycle(vm)
    /**
     * 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,
     * 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
     */
    initEvents(vm)
    // 解析组件的插槽信息,得到 vm.$slot,处理渲染函数,得到 vm.$createElement 方法,即 h 函数
    initRender(vm)
    // 调用 beforeCreate 钩子函数
    callHook(vm, 'beforeCreate')
    // 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
    initInjections(vm) // resolve injections before data/props
    // 数据响应式的重点,处理 props、methods、data、computed、watch
    initState(vm)
    // 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
    initProvide(vm) // resolve provide after data/props
    // 调用 created 钩子函数
    callHook(vm, 'created')
    
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount,反之,没有 el 则必须手动调用 $mount
    if (vm.$options.el) {
      // 调用 $mount 方法,进入挂载阶段
      vm.$mount(vm.$options.el)
    }
  }
}

这里体现一个面试问题,实例创建前,有事件生命周期开始,对应的el没有绑定到实例,data获取不到数据,也获取不到method方法。

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
}

总结

现在回到开始的问题,Vue 的初始化过程new Vue(options)都做了什么?

  1. 处理组件配置项
  2. 初始化组件实例的关系属性,比如 parentparent、children、rootroot、refs 等
  3. 处理自定义事件
  4. 调用 beforeCreate 钩子函数
  5. 初始化组件的 inject 配置项,得到 ret[key]= val 形式的配置对象,然后对该配置对象进行浅层的响应式处理(只处理了对象第一层数据),并代理每个 key 到 vm 实例上
  6. (响应式原理的核心)数据响应式,处理 props、methods、data、computed、watch 等选项(顺序也是按照这个执行)
  7. 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
  8. 调用 created 钩子函数
  9. 如果发现配置项上有 el 选项,则自动调用 mount方法,也就是说有了el选项,就不需要再手动调用mount 方法,也就是说有了 el 选项,就不需要再手动调用 mount 方法,反之,没提供 el 选项则必须调用 $mount
  10. 进入挂载阶段