Vue2 源码解析 - 初始化过程

308 阅读7分钟

本篇文章分享来自小伙伴「liuxin」的一次学习总结分享。 为了进一步熟悉 Vue 框架,对其源码进行解析,记录此文档,方便以后回顾、达到加强记忆和强化学习效果。源码不是看一遍两遍就会了,需要多看才能熟练掌握甚至精通。

收益

  1. 阅读 Vue 源码,可以让自己更加熟悉框架,能更快速的解决工作中遇到的问题;
  2. 阅读 Vue 源码,学习大佬的思路,在项目遇到问题时可以有更多的思路,也提升自己编码的思路,培养“造轮子”的能力;
  3. 阅读 Vue 源码,可以学习怎么写出规范又好维护的代码;
  4. 提升自己解读源码的能力,读源码本身就是一个很好的学习方式,掌握了如何阅读源码,将来在学习其他框架或者是接手新项目的时候,都可以通过阅读源码的方式快速上手。

准备

Vue2 最新稳定版本:2.7.10

github

下载 Vue2 源码

git clone https://github.com/vuejs/vue.git

安装依赖

npm i

添加 sourcemap

在 package.json - scripts 中的 dev,添加 sourcemap 方便调试时查看当前行在源码中的位置

image.png

开发调试

npm run dev

image.png

找初始化的位置

编写调试代码,打断点,快速查找 Vue 构造函数声明位置。

  1. 在 /examples 目录下创建 text.html,引入 vue.js,并创建实例。
<!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="../dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <h2>{{ title }}</h2>
  </div>
  <script>
    debugger
    const vm = new Vue({
      el: '#app',
      data: {
        title: 'Vue 初始化过程解析'
      }
    });
  </script>
</body>
</html>
  1. 在浏览器打开 text.html,debugger 进入 Vue 构造函数,然后如下图,找到 Vue 构造函数所在文件。

image.png

得到 Vue 构造函数在文件 src/core/instance/index.js 中,下面就开始进入源码解读,刚开始解读肯定会有不理解的地方,不理解的地方直接注释好,继续往下看,可能看到某一块就突然明白之前为啥这样写了。

初始化过程

index.js

  • src/core/instance/index.js
import { initMixin } from './init'
...

// Vue 构造函数
function Vue (options) {
  // 在非生产环境时,如果不是 new 创建实例给予警告
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 调用 Vue.prototype._init 方法,该方法在 initMixin 中定义
  this._init(options)
}
// 定义 Vue.prototype._init 方法,进行 初始化混合
initMixin(Vue)
...

export default Vue

initMixin-Vue.prototype._init

  • src/core/instance/init.js
...
// 每个 Vue 实例的 uid,从 0 开始递增
let uid = 0

// 初始化混入,定义 Vue.prototype._init 方法。
export function initMixin (Vue: Class<Component>) {
  // Vue 初始化方法,会在 new Vue 时自动调用
  Vue.prototype._init = function (options?: Object) {
    // this: vue 实例,在 Vue 构造函数那有判断 this instanceof Vue
    const vm: Component = this
    // 每个 Vue 实例的 uid,从 0 开始递增
    vm._uid = uid++
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && 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
    // 处理内部组件配置项
    if (options && options._isComponent) {
      // 优化内部组件实例化,每个子组件初始化时走着,这里做了一些性能优化,
      // 将组件配置项上的一些属性放到 vm.$options 选项中,提高代码的执行效率
      initInternalComponent(vm, options)
    } else {
      // 初始化根组件时走这
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      // 初始化代理
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化实例相关属性,如:$options、$parent、$children、$refs
    initLifecycle(vm)
    // 初始化父组件附加给子组件的事件,事件的派发和监听者都是子组件本身
    initEvents(vm)
    // 初始化组件插槽信息,得到 $slots、处理渲染函数,得到 $createElement 方法
    // 给 vm 定义 $attrs、$listeners 反应属性
    initRender(vm)
    // 调用 beforeCreate 钩子函数
    callHook(vm, 'beforeCreate')
    // 初始化组件的 inject 配置项,首先通过 resolveInject 方法得到形如 result[key] = value 的对象,
    // 然后使用 defineReactive 方法把得到的数据结果对象进行响应式处理,使每个 key 都代理到 vm 实例。
    initInjections(vm) // resolve injections before data/props
    // 初始化状态,是响应式的重点函数,处理 props、methods、data、computed、watch
    initState(vm)
    // 初始化组件的 provide 配置项,并将其挂载到 vm._provided 属性上,
    // provide 和 inject 成对出现,作用是允许一个组件向其所有子孙后台注入一个依赖。
    initProvide(vm) // resolve provide after data/props
    // 调用 created 钩子函数
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // 配置项上如果有 el 属性,Vue 会自动调用 $mount 方法。
    // 所以如果有 el 属性就不需要自己调用 $mount,如果没有就需要自己手动调用 $mount
    if (vm.$options.el) {
      // 调用 $mount 进入挂载
      vm.$mount(vm.$options.el)
    }
  }
}
...

resolveConstructorOptions

  • src/core/instance/init.js
// 从组件的构造函数中获取配置对象 options,并合并到父类配置 options 中
export function resolveConstructorOptions (Ctor: Class<Component>) {
  // 获取组件构造函数的选项
  let options = Ctor.options
  // 判断是否存在父类
  if (Ctor.super) {
    // 存在父类,就递归解析父类构造函数的配置选项
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    // 查看父类的构造函数选项是否改变
    if (superOptions !== cachedSuperOptions) {
      // 父类的构造函数选项发生改变,需要重新设置
      Ctor.superOptions = superOptions
      // 检查 Ctor.options 上是否有任何后期修改/增加的选项 (#4967)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      // 如果存在修改或者增加的选项,则更新 extendOptions
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      // 选项合并,并将合并结果赋值给 Ctor.options
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      // 如果存在组件名,则在 options.components 组件列表中添加这个组件的构造函数,并且 key 为组件名
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

resolveModifiedOptions

  • src/core/instance/init.js
// 解析 Ctor.options 上任何后期修改/增加的选项
function resolveModifiedOptions (Ctor: Class<Component>): ?Object {
  // 修改的选项对象
  let modified
  // 构造函数选项
  const latest = Ctor.options
  // 密封的构造函数选项,也是 Vue 备份的选项
  const sealed = Ctor.sealedOptions
  // 对比两个选项,并把不一致的选项记录下
  for (const key in latest) {
    if (latest[key] !== sealed[key]) {
      if (!modified) modified = {}
      modified[key] = latest[key]
    }
  }
  return modified
}

mergeOptions

  • src/core/util/options.js
// 将两个选项对象合并为一个新对象,相同配置选项子配置会覆盖父配置
// 用于实例化和继承的核心实用程序。
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  // 标准化 props、inject、directive 语法,方便后续使用
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 处理 child 对象上没有 _base 的 extends 和 mixins,分别调用 mergeOptions 合并到 parent
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, 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)
    }
  }
  // 合并选项配置,把 child 的配置内容优先级高于 parent 的
  function mergeField (key) {
    // 获取合并方法
    const strat = strats[key] || defaultStrat
    // 如果 child[key] 有配置内容,则返回 child[key],没有的话使用 parent[key]
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

initInjections

  • src/core/instance/inject.js
// 初始化组件的 inject 配置项,首先通过 resolveInject 方法得到形如 result[key] = value 的对象,
// 然后使用 defineReactive 方法把得到的数据结果对象进行响应式处理,使每个 key 都代理到 vm 实例。
export function initInjections (vm: Component) {
  // 解析组件的 inject 配置项,得到形如 result[key] = value 的对象
  const result = resolveInject(vm.$options.inject, vm)
  // 如果 result 存在,就把 result 做响应式处理,把 result 中每一个 key 都代理到 vm 实例上
  if (result) {
    // 不观察
    toggleObserving(false)
    // 对 result 中的每一个 key 都代理到 vm 实例上
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        // 会给出警告,inject 不能改变,因为每个改变都会导致组件中的更改被覆盖
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    // 开启观察
    toggleObserving(true)
  }
}

resolveInject

  • src/core/instance/inject.js
// 解析 inject,生成形如 result[key] = val 的对象
export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    // 创建一个空的对象,用来记录解析后的结果
    const result = Object.create(null)
    // 获取 inject 的所有 key
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    // 遍历所有 key
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // #6574 in case the inject object is observed...
      // 跳过 __ob__ 对象
      if (key === '__ob__') continue
      // 获取 injectKey,详见 flow/options.js
      const provideKey = inject[key].from
      let source = vm
      // 遍历所有组件直到根组件,获取所有 injectKey 对应的 value,生成 result[key] = val 形式的对象。
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      // 如果上一步没有找到 _provided,就使用 inject[key].default,还没有就报警告
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}

initProvide

  • src/core/instance/inject.js
// 初始化组件的 provide 配置项,并将其挂载到 vm._provided 属性上
export function initProvide (vm: Component) {
  // 获取组件的 provide 配置项
  const provide = vm.$options.provide
  // 如果存在 provide,判断其是函数吗,是函数就执行,不是就直接返回
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

总结

说明

initLifecycle、initEvents、initRender、initState、callHook 不在本篇解析,后续解析

Vue 初始化过程中 (new Vue(options)) 都做了什么?

  1. 处理组件配置项
  • 每个子组件走到初始化配置项时,都做一些性能优化

  • 初始化根组件时,从组件的构造函数中获取配置对象 options,并合并配置

  1. 初始化实例相关属性,如:options、parent、children、refs 等

  2. 初始化父组件附加给子组件的事件,事件的派发和监听者都是子组件本身

  3. 初始化组件的插槽信息得到 slots,处理渲染函数,得到slots,处理渲染函数,得到 createElement 方法

  4. 调用 beforeCreate 钩子函数

  5. 初始化组件的 inject 配置项,得到形如 result[key] = val 的对象,并把 result 每个 key 都代理到 vm 实例上

  6. 初始化状态,里面是响应式的核心内容

  7. 初始化组件的 provide 配置项,并将其挂载到 vm._provided 属性上

  8. 调用 create 钩子函数

  9. 判断配置上是否有 el 属性,如果有自动调用 vm.$mount 方法

  10. 接下来进入挂载阶段