Vue 源码学习(1)

概述

我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家。

这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容:

  1. 寻找入口文件
  2. 在打包的过程中 Vue 发生了什么变化
  3. 在 Vue 实例化的时候,它的内部到底做了什么

寻找入口文件

首先我们寻找入口文件,我们查看package.json文件去找它的打包指令:

"scripts": {
  // ...
  "build": "node scripts/build.js",
  // ...
}

可以看到,打包的执行文件是scripts/build.js,于是我们再去看这个文件做了什么:

// ...
let builds = require('./config').getAllBuilds()
// ...
build(builds)

function build (builds) {
  let built = 0
  const total = builds.length
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }

  next()
}
// ...

我们看到,它是通过获取获取./config.js里面的配置来进行打包的,于是我们再去看./config.js里面有什么配置:

// ...
const builds = {
  // ...
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
    format: 'es',
    banner
  },
  // ...
}

可以看到,上面就是rollup打包的典型配置,有很多像这样的配置,我只列出了其中一个。那么我们在项目中使用的 Vue 版本到底是哪个配置生成的呢?我们通过查阅 Vue 官网和 vue-cli3 官方文档来弄清这个问题。

1.通过查阅Vue 官网可以看到,我们在项目中一般使用的是vue.runtime.esm.js这个版本。官网原文如下:

为这些打包工具提供的默认文件 (pkg.module) 是只有运行时的 ES Module 构建 (vue.runtime.esm.js)。

2.我们再来看 vue-cli3 源码:

// ...
webpackConfig.resolve
  .alias
  .set(
    'vue$',
    options.runtimeCompiler
      ? 'vue/dist/vue.esm.js'
      : 'vue/dist/vue.runtime.esm.js'
  )
/// ...

可以看到,它通过判断options.runtimeCompiler的值,来设置 vue 的别名。vue-cli3官方文档里面这个值的默认值为false,所以 vue 的别名设置为vue.runtime.esm.js。(因此我们在 vue-cli3 的项目里面执行语句import Vue from 'vue';时,我们其实在执行import Vue from 'vue/dist/vue.runtime.esm.js';

然后通过查看上面config.js里面的配置,我们可以看到,生成vue.runtime.esm.js的配置里面入口文件是web/entry-runtime.js

到这里还没有结束,查看vue 的官方 repo,里面是没有 web 文件夹的,所以这个路径web/entry-runtime.js里面的 web 应该也是一个别名。于是继续查找,我们发现,scripts 文件夹里面还有一个文件alias.js,里面的内容如下:

const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

很明显,web 被设置为src/platforms/web的别名。所以,最后 vue 的入口文件是src/platforms/web/entry-runtime.js

我们总结一下:

  1. vue 是通过 rollup 进行打包的,打包的配置在一个单独的config.js文件里面
  2. 通过查阅 Vue 官网和 vue-cli3 官方文档,我们发现,项目中引用的 vue ,其实引用的是vue.runtime.esm.js文件
  3. 最后通过查找配置,我们发现 vue 的入口文件是src/platforms/web/entry-runtime.js

在打包的过程中 Vue 发生了什么变化

我们查看这个入口文件src/platforms/web/entry-runtime.js,它总共就只有这么一段代码:

/* @flow */
import Vue from './runtime/index'
export default Vue

我们再打开./runtime/index文件,发现它的第一行是:

import Vue from 'core/index'

在上面alias.js文件中我们看到了,core 的别名是src/core,所以我们继续看src/core/index.js文件:

import Vue from './instance/index'

继续看./instance/index:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

可以看到,它是通过函数式的方式来定义Vue这个类的,然后检查代码执行环境,如果是非开发环境就会报错;如果新建一个它的实例,就会执行_init(options)进行初始化工作。

我们先不管 Vue 的实例,而只关注Vue.prototypeVue,来看看在打包的一步步过程中,挂载在它们上面的属性发生了什么变化

通过console.log边打包边输出,我们可以看到,在./instance/index文件里面,initMixin方法为Vue.prototype增加了_init方法:

{
  _init: ƒ (options)
}

stateMixin方法初始化了各种state:

{
  $data: {get: ƒ, set: ƒ}
  $props: {get: ƒ, set: ƒ}
  $set: ƒ (target, key, val)
  $delete: ƒ del(target, key)
  $watch: ƒ ( expOrFn, cb, options )
}

eventsMixin方法添加了各种事件相关的方法:

{
  $on: ƒ (event, fn)
  $once: ƒ (event, fn)
  $off: ƒ (event, fn)
  $emit: ƒ (event)
}

lifecycleMixin方法增加了_update、$forceUpdate 和 $destroy

{
  _update: ƒ (vnode, hydrating)
  $forceUpdate: ƒ ()
  $destroy: ƒ ()
}

renderMixin方法则把render时需要调用的方法都加进去了:

{
  _n: ƒ toNumber(val)
  _s: ƒ toString(val)
  _l: ƒ renderList( val, render )
  _t: ƒ renderSlot( name, fallback, props, bindObject )
  _q: ƒ looseEqual(a, b)
  _i: ƒ looseIndexOf(arr, val)
  _m: ƒ renderStatic( index, isInFor )
  _f: ƒ resolveFilter(id)
  _k: ƒ checkKeyCodes( eventKeyCode, key, builtInKeyCode, eventKeyName, builtInKeyName )
  _b: ƒ bindObjectProps( data, tag, value, asProp, isSync )
  _v: ƒ createTextVNode(val)
  _e: ƒ (text)
  _u: ƒ resolveScopedSlots( fns, hasDynamicKeys, contentHashKey )
  _g: ƒ bindObjectListeners(data, value)
  _d: ƒ bindDynamicKeys(baseObj, values)
  _p: ƒ prependModifier(value, symbol)
  $nextTick: ƒ (fn)
  _render: ƒ ()
}

但是到这里还没有结束,上面这些只是在 src/core/instance/index.js 文件里面做的处理。我们现在来看看引用这个文件的 src/core/index.js 文件:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

这个文件里面,首先通过initGlobalAPI方法给Vue添加了这些属性:

// 在 Vue 上面挂载
{
  config: {get: ƒ, set: ƒ}
  util: {value: {…}}
  set: {value: ƒ}
  delete: {value: ƒ}
  nextTick: {value: ƒ}
  observable: {value: ƒ}
  options: {value: {components, directives, filters, _base}}
  use: {value: ƒ}
  mixin: {value: ƒ}
  cid: {value: 0}
  extend: {value: ƒ}
  component: {value: ƒ}
  directive: {value: ƒ}
  filter: {value: ƒ}
}

之后直接在Vue.prototype上面添加了$isServer$ssrContext属性,在Vue上面添加了FunctionalRenderContext方法和版本号。

最后我们看看引用 src/core/index.js 文件的 web/runtime/index.js 文件:

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

可以看到,它在VueVue.prototype上面添加了平台特有utils、directives、components、patch 和 $mount方法。

到这里就全部结束了,我们整理这些属性和方法的目的是:

  1. 在接下来读源码的过程中,我们对 Vue 和 Vue.prototype 上的属性和方法更有信心
  2. 如果我们看到源码在调用某个不知道的属性或方法的时候,可以从这里来查找来源
  3. 我们能够看到 Vue 源码的代码结构是怎么组织的

我们再总结一下:

  1. Vue.prototype 上的属性和方法主要是在 src/core/instance/index.js 里面挂载的。
  2. Vue 上的静态属性和方法主要是在 src/core/index.js 里面的initGlobalAPI方法里面挂载的。这个文件里面还处理了ssr(服务端渲染)相关的东西和加上了版本号。
  3. web/runtime/index.js 文件则添加了 web 平台特有的属性和方法。

在 Vue 实例化的时候,它的内部到底做了什么

下面是我们在大多数项目里面写的Vue实例化代码:

new Vue({
  router,
  store,
  i18n,
  render: h => h(App),
}).$mount('#app');

首先,我们来看前半段new Vue({router, store, i18n, render: h => h(App)})做了什么。

我们知道,Vue在实例化的时候,会继承Vue.prototype的属性和方法,所以通过汇总我们刚才总结的属性和方法,我们可以知道,这个实例拥有如下属性和方法:

{
  _init: ƒ (options)
  $data: {get: ƒ, set: ƒ}
  $props: {get: ƒ, set: ƒ}
  $set: ƒ (target, key, val)
  $delete: ƒ del(target, key)
  $watch: ƒ ( expOrFn, cb, options )
  $on: ƒ (event, fn)
  $once: ƒ (event, fn)
  $off: ƒ (event, fn)
  $emit: ƒ (event)
  _update: ƒ (vnode, hydrating)
  $forceUpdate: ƒ ()
  $destroy: ƒ ()_o: ƒ markOnce( tree, index, key )
  _n: ƒ toNumber(val)
  _s: ƒ toString(val)
  _l: ƒ renderList( val, render )
  _t: ƒ renderSlot( name, fallback, props, bindObject )
  _q: ƒ looseEqual(a, b)
  _i: ƒ looseIndexOf(arr, val)
  _m: ƒ renderStatic( index, isInFor )
  _f: ƒ resolveFilter(id)
  _k: ƒ checkKeyCodes( eventKeyCode, key, builtInKeyCode, eventKeyName, builtInKeyName )
  _b: ƒ bindObjectProps( data, tag, value, asProp, isSync )
  _v: ƒ createTextVNode(val)
  _e: ƒ (text)
  _u: ƒ resolveScopedSlots( fns, hasDynamicKeys, contentHashKey )
  _g: ƒ bindObjectListeners(data, value)
  _d: ƒ bindDynamicKeys(baseObj, values)
  _p: ƒ prependModifier(value, symbol)
  $nextTick: ƒ (fn)
  _render: ƒ ()
  __patch__: ƒ patch(oldVnode, vnode, hydrating, removeOnly)
  $mount: ƒ ( el, hydrating )
}

然后我们回到前面的实例化代码:

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

结合前面实例化传的参数,它相当于执行了如下代码:

this._init({
  router,
  store,
  i18n,
  render: h => h(App),
});

我们继续找_init方法的定义,他是在 core/instance/init.js 里面定义的,简化代码如下:

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // a uid
  vm._uid = uid++

  // a flag to avoid this being observed
  vm._isVue = true
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    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
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

代码里面,首先给实例2个标记_uid_isVue,其中每个实例的_uid各不相同。

合并 options

然后进行合并 options,由于这一步比较复杂,所以我们独立为一个小节。

由于我们并没有传 _isComponent 这个变量,所以执行下面这段代码:

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)

我们来看看 resolveConstructorOptions的定义:

export function resolveConstructorOptions (Ctor: Class) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

可以发现,Ctor的值是传入的vm.constructor,即 vm 的构造函数,而 vm 的构造函数就是 Vue,所以Ctor其实就是Vue。所以options就是Vue上的静态属性Vue.options。然后由于目前Vue没有继承,所以Vue.superundefined,所以这里resolveConstructorOptions(vm.constructor)其实就返回Vue.options

再看之前给Vue挂载options的时候,它是由initGlobalAPI挂载的全局方法,结构是这样的:

options: {value: {components, directives, filters, _base}}

所以简化一下,上面合并 options 的那段代码可以改写为:

vm.$options = mergeOptions(
  {
    components,
    directives,
    filters,
    _base
  },
  {
    router,
    store,
    i18n,
    render: h => h(App)
  },
  vm
)

我们来看一下mergeOptions的定义代码:

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // ...

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  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)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

可以看到,一共分为2步来进行合并:

1.把child.extendschild.mixins合并到parent里面去。

2.对于其它的属性 key,通过 const strat = strats[key] || defaultStrat来根据 key 获取定制的合并函数,然后用这个合并函数来合并这个属性。

其中,router、store、i18n 和 render 这几个字段都没有定制的合并函数,所以使用默认策略defaultStrat进行合并,即直接赋值的形式。

/**
 * Default strategy.
 */
const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

所以上面合并 options 的代码其实就是:

vm.$options = {
  components,
  directives,
  filters,
  _base,
  router,
  store,
  i18n,
  render: h => h(App),
}

这样合并 options 就结束了。

设置代理

接下来的代码如下:

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}

这段代码判断是否是生产环境,然后在非生产环境的时候,调用initProxy方法。我们从字面意思可以理解为:给 vm 设置代理,其中 vm 就是实例本身。我们再来看initProxy方法的源码:

initProxy = function initProxy (vm) {
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
      ? getHandler
      : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}

可以看到,通过vm._renderProxy = new Proxy(vm, handlers)这行代码,我们给 vm 设置了一些代理,当我们调用vm._renderProxy的时候,就会执行这些代理函数。我们接下来看看有哪些代理函数,下面是其中一个

const warnNonPresent = (target, key) => {
  warn(
    `Property or method "${key}" is not defined on the instance but ` +
    'referenced during render. Make sure that this property is reactive, ' +
    'either in the data option, or for class-based components, by ' +
    'initializing the property. ' +
    'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
    target
  )
}

const getHandler = {
  get (target, key) {
    if (typeof key === 'string' && !(key in target)) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return target[key]
  }
}

可以看到,我们在调用vm._renderProxy.xxx的时候,会检查这个 xxx 是否在target.$data也就是vm.$data里面,如果不是就会弹出 warning。

初始化生命周期

接下来是这段代码:

initLifecycle(vm)

我们查找initLifecycle方法的源码:

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

我们知道,vm.$options就是我们刚才合并的 options,它包含Vue的静态属性options,和我们提供的各种参数。所以,这段代码里面添加了父子实例属性,以及各种生命周期属性。

值得一提的是,abstract是什么属性?官网并没有找到这个参数,我们在项目中也没有传过这个参数。但是通过搜索源码,我发现,keep-alive组件和transition组件的abstract属性被设置为 true。所以在处理父子实例那里,会忽略所有的keep-alive组件和transition组件。因此,abstract属性是抽象的意思,有abstract属性的组件在处理时没有被当成真正的 Vue 实例

初始化事件

接下来是这段代码:

initEvents(vm)

我们查找initEvents方法的源码:

import { updateListeners } from '../vdom/helpers/index'

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

可以看到,由于没有传oldListeners参数,所以最后是通过这段代码updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)给 vm 添加各种事件的。由于我们在初始化代码里面并没有传事件,所以这里没有添加任何事件。

初始化 Render 相关属性和方法

接下来是这段代码:

initRender(vm)

我们查找initRender方法的源码:

export function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

可以看到,这段代码先在 vm 上绑定了一些属性,然后处理有slots的情况,再然后在 vm 上绑定_c$createElement,它们都是createElement的语法糖,其中_c是内部用的,而$createElement是给用户在render函数里面用的。最后就是在 vm 上定义了 2 个响应式属性(关于响应式我们以后再说):$attrs$listeners

调用 beforeCreate 钩子

接下来是这段代码很好理解,就是调用beforeCreate钩子

callHook(vm, 'beforeCreate')

初始化 injections

接下来是这段代码:

initInjections(vm)

我们查找initInjections方法的简化源码:

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        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])
      }
    })
  }
}

这段代码很好理解,它首先用resolveInject收集了所有的injections,然后把它们响应式地挂载到了 vm 上面。如果是在非生产环境,给它们赋值还会产生 warning。接下来我们来看下它是怎么收集所有的injections的,resolveInject的简化源码如下:

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)
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      const provideKey = inject[key].from
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      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
  }
}

可以看到,如果是from的注入方式,则使用 while 循环一级级找父节点,然后从父节点的_provided里面得到这个inject属性的值;如果是default的注入方式,则直接赋值即可。

初始化 state

接下来是这段代码:

initState(vm)

我们查找initState方法的简化源码:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

可以看到,initState方法按顺序初始化了props、methods、data、computed、watch。我们一个个看是怎么初始化的。

首先是initProps的简化源码:

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }
  }
  toggleObserving(true)
}

可以看到,源码里面propsOptions其实就是我们传的opts.props,即vm.$options.props,也就是我们传的options参数里面的props。然后源码对里面的每个值进行一系列判断和添加报错信息,再通过validateProp方法在propsDatavm.$options.propsData里面找到相应的 value,最后把这个 value 通过defineReactive方法响应式的绑定在了vm._props上面。

再来看一看initMethods的简化源码:

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

可以看到,它对opts.methodsvm.$options.methods里面的每个方法,还是先做一系列判断和报错,最后使用bind方法把它们绑定到vm 上面去。

再来看一看initData的简化源码:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

可以看到,这段代码和前面的类似,都是做了一些判断和报错信息,然后用proxy绑定到了 vm 的_data属性上面去,最后用observe定义了响应式。(observe我们以后再讲,我们现在先理清大致的轮廓线)

其实后面的initComputedinitWatch都差不多,都是先做一些判断和报错信息,然后同步到 vm 上线去,这里就省略了。

初始化 provide

接下来是这段代码:

initProvide(vm)

我们查找initProvide方法的简化源码:

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

可以看到,只是简单地把vm.$options.provide赋值给vm._provided而已。

调用 created 钩子 和 判断是否挂载

接下来是这段代码:

callHook(vm, 'created')

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

可以看到,先调用了created钩子,然后判断options里面是否有el属性,如果有就使用$mount进行挂载($mount方法的挂载过程我们等会儿会讲到,现在先略过)。

需要注意的是,这段代码说明,如果我们不按照下面的方式初始化:

new Vue({
  router,
  store,
  i18n,
  render: h => h(App),
}).$mount('#app');

而是使用这个方式初始化也是可以的:

new Vue({
  el: '#app',
  router,
  store,
  i18n,
  render: h => h(App)
});

到这里我们还没有结束,我们最后来看我们在项目中使用的初始化代码的后半段:

new Vue({
  router,
  store,
  i18n,
  render: h => h(App),
}).$mount('#app');

上面的代码通过$mount方法把Vue实例挂载到了 id 为 app 的节点上面,我们来看$mount是怎么挂载的。那么$mount方法是在哪里定义的呢?通过我们之前的那些属性和方法的记录,我们找到:$mount是在 web/runtime/index.js 文件里面定义平台属性和方法的时候定义的:

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

可以看到,它其实调用的是mountComponent方法,我们来看mountComponent的源码,它是在core/instance/lifecycle里面定义的,简化代码如下:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let = updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

可以看到,它会先判断 vm 有没有render方法,如果没有就检查有没有template属性,如果有就报错:我们应该使用compiler-included那个版本,如果没有template属性就报错:挂载失败。

检查完毕之后调用了beforeMount钩子。

再然后是调用 vm 的_render()方法获取渲染后的结果,把结果写入更新函数里面去,再使用watcher使更新函数变成响应式的。(这里watcher的具体实现我们放到以后来讲)

最后调用了mounted钩子,挂载完毕。

以上就是从新建Vue实例到挂载完毕的全过程。

我们来总结一下:

  1. Vue 源码先使用_init方法进行初始化,然后使用各平台定义的$mount方法进行挂载的。
  2. 在初始化的过程中,Vue 源码先以一定的规则合并了我们传入的options和父组件的options,然后初始化了proxy,lifecycle,state等,同时也在不同的时间段调用了生命周期钩子。
  3. 在挂载的过程中,Vue 源码通过获取render方法的执行结果,把它加入到更新函数里面去,再使用watcher使更新函数变成响应式的,从而在挂载的过程中、在父组件更新的过程中、在传入的数据发生变化的过程中都能进行自动更新

后记

上面我们关于响应式的代码讲解都省略了,我打算放到下一期来一起讲,主要包括:defineReactive,observe,watcher。我们把响应式讲清楚之后,这里省略的部分就一目了然了,敬请期待!!!

你可能感兴趣的:(Vue 源码学习(1))