【手摸手】带你看 Vue2 源码 - 第一章

445 阅读10分钟

前言

本系列将用七天时间,带你分析一波 Vue2 源码,基于 Vue 2.6.11 版本。文章内容基于个人学习总结,同时也大量借鉴了大佬们的资料,引用处均会注释说明并给出传送门。

水平一般,能力有限。如对源码理解有误,欢迎指正,万分感谢。

准备

环境准备 💻

  1. git clone https://github.com/vuejs/vue.git
  2. cd vue & npm install
  3. npm run dev
  4. 创建 index.html 并引入 dist/vue.js 文件

建议在 dev 命令中添加 --sourcemap

"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",

目录结构 📁

src
├─shared            # 浏览器 Browser 和服务端 SSR 共享的代码
├─sfc               # 解析单文件组件
├─server            # 服务端 SSR 代码
├─platforms         # 平台支持 Web / Weex
├─core              # Vue 核心
├─compiler          # 编译器

入口文件 🚪

在根目录中找到 script/config.js 文件,该文件包括各个平台的编译参数,目前我们基于浏览器去调试源码,所以找到 Runtime+compiler development build (Browser) 对应的配置。resolve('web/entry-runtime-with-compiler.js') 其实对应就是 src/platforms/web/entry-runtime-with-compiler.js 文件,这就是携带编译器 Browser 版本的入口。

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  },
  'web-runtime-cjs-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.prod.js'),
    format: 'cjs',
    env: 'production',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.dev.js'),
    format: 'cjs',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  'web-full-cjs-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.prod.js'),
    format: 'cjs',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // 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
  },
  // Runtime+compiler ES modules build (for bundlers)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.js'),
    format: 'es',
    transpile: false,
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.min.js'),
    format: 'es',
    transpile: false,
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
    format: 'umd',
    env: 'development',
    banner
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
    format: 'umd',
    env: 'production',
    banner
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
    format: 'umd',
    env: 'production',
    alias: { he: './entity-decoder' },
    banner
  },
  // Web compiler (CommonJS).
  'web-compiler': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-template-compiler/package.json').dependencies)
  },
  // Web compiler (UMD for in-browser use).
  'web-compiler-browser': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/browser.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'VueTemplateCompiler',
    plugins: [node(), cjs()]
  },
  // Web server renderer (CommonJS).
  'web-server-renderer-dev': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.dev.js'),
    format: 'cjs',
    env: 'development',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-prod': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.prod.js'),
    format: 'cjs',
    env: 'production',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-basic': {
    entry: resolve('web/entry-server-basic-renderer.js'),
    dest: resolve('packages/vue-server-renderer/basic.js'),
    format: 'umd',
    env: 'development',
    moduleName: 'renderVueComponentToString',
    plugins: [node(), cjs()]
  },
  'web-server-renderer-webpack-server-plugin': {
    entry: resolve('server/webpack-plugin/server.js'),
    dest: resolve('packages/vue-server-renderer/server-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  'web-server-renderer-webpack-client-plugin': {
    entry: resolve('server/webpack-plugin/client.js'),
    dest: resolve('packages/vue-server-renderer/client-plugin.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/vue-server-renderer/package.json').dependencies)
  },
  // Weex runtime factory
  'weex-factory': {
    weex: true,
    entry: resolve('weex/entry-runtime-factory.js'),
    dest: resolve('packages/weex-vue-framework/factory.js'),
    format: 'cjs',
    plugins: [weexFactoryPlugin]
  },
  // Weex runtime framework (CommonJS).
  'weex-framework': {
    weex: true,
    entry: resolve('weex/entry-framework.js'),
    dest: resolve('packages/weex-vue-framework/index.js'),
    format: 'cjs'
  },
  // Weex compiler (CommonJS). Used by Weex's Webpack loader.
  'weex-compiler': {
    weex: true,
    entry: resolve('weex/entry-compiler.js'),
    dest: resolve('packages/weex-template-compiler/build.js'),
    format: 'cjs',
    external: Object.keys(require('../packages/weex-template-compiler/package.json').dependencies)
  }
}

开始

Demo

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <script src="../../dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <div>
        <h1>Vue</h1>
    </div>
    {{ msg }}
  </div>
  <script>
    new Vue({
      data: {
        msg: 'Hello'
      }
    }).$mount('#app')
  </script>
</body>
</html>

src/platforms/web/entry-runtime-with-compiler.js

入口文件 entry-runtime-with-compiler.js 主要的作用是扩展了原型 $mount 方法,使用 compileToFunctionstemplate 编译成渲染函数。如果使用 entry-runtime 版本则需要预先编译,比如 webpack

import Vue from './runtime/index'

/**
 * 扩展了 src/platforms/web/runtime/index.js - $mount 
 * 最终执行的是 src/core/instance/lifecycle.js - mountComponent
 * 用于判断是否需要添加 render 函数,在浏览器中默认需要通过 compiler 生成一个渲染函数
 * runtime-only 版本没有 compiler 编译器,需要预先编译,如 webpack
 * 浏览器环境 <script> 应该使用 entry-runtime-with-compiler 版本
 * @param el 需要挂载的根结点 #app
 * @param hydrating Vue SSR 强制使用激活模式
 */
// 缓存 Vue.prototype.$mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  // 不能挂载 body 和 html 上
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  // 把 template 或者 el 转换成 render function
  if (!options.render) {
    // 如果没有写 render () {} 则找 template / el
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      // 调用 src/platforms/web/compiler/index.js - compileToFunctions
      // template 编译成渲染函数 => render, staticRenderFns
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      /* 
        生成的 render
        function anonymous() {
          with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v("\n    "+_s(msg)+"\n  ")])}
        } 
      */
      console.log(render.toString())
      options.staticRenderFns = staticRenderFns
      /* 
        生成的 staticRenderFns 是一个数组,索性代表对应元素在 render 中出现的位置
        vnode 节点的 staticRoot 为 true 时(包括其在内的所有子节点全部是静态节点)
        如 
        <div>
          <h1>Vue</h1>
        </div>
        转化成:
        [0: fn ...] 0 与 render 中 _m(0) 对应
        function anonymous() {
          with(this){return _c('div',[_c('h1',[_v("Vue")])])}
        }
      */  
      console.log(staticRenderFns)

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // 生成好 render 后,最终调用 src/core/instance/lifecycle.js - mountComponent
  return mount.call(this, el, hydrating)
}

src/platforms/web/runtime/index.js

Vue 添加与 web 平台运行时相关的类属性

import Vue from 'core/index'

// 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) // v-show v-model
extend(Vue.options.components, platformComponents) // Transition TransitionGroup

// install platform patch function
// 安装平台的 patch 方法,这里判断 inBrowser 应该是只考虑在 window 拥有 dom 的环境
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
// 被 entry-runtime-with-compiler.js 扩展的 $mount 方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

src/core/index.js

Vue 添加全局 API 方法

import Vue from './instance/index'

/* 
  添加全局类属性 API 方法,包括:
  1. Vue.config
  2. Vue.set
  3. Vue.delete
  4. Vue.nextTick
  5. Vue.util
  6. Vue.observable
  7. Vue.options
  8. Vue.mixin
  9. Vue.use
  10. Vue.extend
  11. 内置组件 KeepAlive
*/
initGlobalAPI(Vue)

src/core/instance/index.js

这个文件就是 Vue 类的本尊

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) // 添加原型 _init 方法  this._init(options)
stateMixin(Vue) // 添加原型 $data $props $set $delete $watch
eventsMixin(Vue) // 添加原型 $on $once $off $emit
lifecycleMixin(Vue) // 添加原型 _update $forceUpdate $destroy
renderMixin(Vue) // 添加原型 $nextTick _render

export default Vue

src/core/instance/init.js

Vue 类中调用 this._init(options) 方法初始化,主要做的是合并选项、挂载原型属性和执行 $mount 挂载节点。

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    // 每个组件实例的递增 id
    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
    // 不观测 this 目前我发现的 observe 只观测 props 和 data
    vm._isVue = true
    // merge options
    // 合并传进来的选项
    if (options && options._isComponent) {
      // 如果是组件场景 Vue.component => Vue.extend()
      // 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 {
      // 如果是外部调用场景,new Vue()
      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) // $parent $root $children $refs
    initEvents(vm) // _events
    initRender(vm) // _vnode $slots $scopedSlots $createElement
    callHook(vm, 'beforeCreate') // 执行生命周期 beforeCreate
    initInjections(vm) // resolve injections before data/props
    initState(vm) // props methods data computed watch
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created') // 执行生命周期 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)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el) // 挂载节点
    }
  }

initInternalComponent

src/core/instance/init.js

Vue.component({...options}) 中的 options 合并到组件实例上

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // vm.constructor 是一个通过 extend 产生的 VueComponent 类
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode // 当前组件 vnode created by createComponentInstanceForVnode
  opts.parent = options.parent // 父组件实例
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData // 当前组件 props
  opts._parentListeners = vnodeComponentOptions.listeners // 当前组件 listener
  opts._renderChildren = vnodeComponentOptions.children // 当前组件 children
  opts._componentTag = vnodeComponentOptions.tag // 当前组件 tag
  
  // 是否定义了组件的 render
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

mergeOptions

src/core/util/options.js

这个方法里有 strats 对象,它使用策略模式定义了每种属性应该如何合并

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
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // 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.
  // 递归把 options 中的 extends 和 mixins 合并到 vm.$options 中
  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
  // parent 上所有的属性直接合
  for (key in parent) {
    mergeField(key)
  }
  // child 里的属性,如果 parent 没有就合并
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  // strats 存储各种属性的合并策略(策略模式)
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

initLifecycle

src/core/instance/lifecycle.js

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

  // locate first non-abstract parent
  let parent = options.parent
  
  // 有父组件且不是 keep-alive 或 transition
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm) // 把自己 push 到父组件的 $children
  }

  vm.$parent = parent // 把父组件赋值给当前组件 $parent
  vm.$root = parent ? parent.$root : vm
  ...
}

initEvents

src/core/instance/events.js

initEvents 最终是执行名为 updateListeners 的方法,该方法位于 src/core/vdom/helpers/update-listeners.js,主要是用于事件的注册更新。

事件分为浏览器原生事件和自定义事件,分别对应不同的处理流程:

  1. 对于浏览器原生事件,如 @click.native='xxx',是在父组件中处理。
  2. 对于自定义事件,如 @select='xxx',父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化。

总结来说,对于自定义事件,子组件派发,最终还是子组件接收。

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, def, cur, old, event
  // 遍历 Listener 的属性 @select="selectHandler" ===> { select: fn () ... }
  for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    /**
     * capture: false
     * name: "select"
     * once: false
     * passive: false  
     */
    event = normalizeEvent(name) // 解析事件所带的何种修饰符 
    /* istanbul ignore if */
    if (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      // 如果事件名在 Old Listener 中不存在 则注册
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm) // 返回了一个 invoker function 
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      add(event.name, cur, event.capture, event.passive, event.params) // 注册 其实就是调用实例的 $on 方法
    } else if (cur !== old) {
      // 更新事件名 name 的引用 old.fns 为最新的 cur
      old.fns = cur
      on[name] = old
    }
  }
  // 遍历 oldOn 移除在 on 中不存在的事件名
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

initRender

src/core/instance/render.js

这个方法在 vm 实例上挂载了 $slots_c$createElement,并对 $attrs$listeners 做了响应式处理。首先很有意思的是 _c$createElement 本质都是 createElement,唯一的区别在于第 6 个参数 alwaysNormalize: boolean 不同。

// 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
/**
* 默认编译器已经生成 render 时,如 with(this){return _c('div',[_c('h1',[_v("Vue")])])}
*/
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.
/**
* 一般用于用户手写 render Function 的场景
*/
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

下面我们看一下 alwaysNormalize 这个参数的作用。

跳转到 src/core/vdom/create-element.jscreateElement 方法,我们发现 alwaysNormalize 的值对应两个常量 const SIMPLE_NORMALIZE = 1const ALWAYS_NORMALIZE = 2,这个常量在下面的 _createElement 中使用,会调用 normalizeChildrensimpleNormalizeChildren 去生成 children

simpleNormalizeChildren 的作用其实就是将 vnode 打平成一个数组,让其深度只有一层。

normalizeChildren 的作用是判断 children 是否为 string / number / symbol / boolean 类型其中之一,是则生成一个文本节点数组 [createTextVNode(children)],否则继续调用 simpleNormalizeChildren 打平。

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 如果 children 是 string / number / symbol / boolean 其中一种则创建 [createTextVNode(children)]
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 打平成数组,深度一层
    children = simpleNormalizeChildren(children)
  }
}
// $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

/**
 * 为了更早的创建 HOC 高阶组件,所以要对 $attrs & $listeners 做响应式处理,以便当使用它们是保持最新
 */

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

initInjections

src/core/instance/inject.js

首先要解释一个问题,为什么 initInjections initState initProvide 的顺序:

因为 provide 选项注入的值作为 datapropswatchcomputedmethod 入口,inject 选项接收到注入的值有可能被以上这些数据所使用到,所以初始化好 inject 后要调用 initState 初始化好数据,然后才能初始化 provide

export function initInjections (vm: Component) {
  /**
   * 把子组件中的 inject 转换成对象 inject: ['foo'] => { 'foo': 'bar' }
   */
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    /**
     * provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
     */
    toggleObserving(false) // 通知 defineReactive 不要把 provide / inject 转换成响应式
    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]) // 挂载在实例上 this.foo = 'bar'
      }
    })
    toggleObserving(true)
  }
}

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
      /**
       * 把 inject 的 key 取出来,然后递归遍历当前组件的 $parent 直至找到提供当前 key 的_provided
       */
      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
  }
}

initState

src/core/instance/state.js

  1. initProps

     function initProps (vm: Component, propsOptions: Object) {
       const propsData = vm.$options.propsData || {} // 传入的 props
       const props = vm._props = {} // 指向 vm._props 的指针
       // cache prop keys so that future props updates can iterate using Array
       // instead of dynamic object key enumeration.
       /**
        * 如果 props 发生变化,使用数组的方式取代动态枚举对象属性迭代 key 可能是处于性能考虑
        */
       const keys = vm.$options._propKeys = [] // 缓存 props 的 key
       const isRoot = !vm.$parent
       // root instance props should be converted
       // 根组件的 props 已经响应式转化完 why?
       if (!isRoot) {
         toggleObserving(false)
       }
       for (const key in propsOptions) {
         keys.push(key) // prop key 加入缓存中
         const value = validateProp(key, propsOptions, propsData, vm) // 校验传入的 prop value 类型是否匹配
         /* 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) // 转化响应式 添加到 vm._props
         }
         // static props are already proxied on the component's prototype
         // during Vue.extend(). We only need to proxy props defined at
         // instantiation here.
         /**
          * 判断当前 key vm.key 在实例中是否存在,如果不存在则添加一个代理,vm[key] ===> vm._props[key]
          */
         if (!(key in vm)) {
           proxy(vm, `_props`, key)
         }
       }
       toggleObserving(true)
     }
    
  2. initProps

    function initMethods (vm: Component, methods: Object) {
      const props = vm.$options.props
      // 遍历 methods 的 key 如果 value 是函数则 bind 当前 vm 上下文,然后添加到 vm[key]
      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)
      }
    }
    
  3. initData

    function initData (vm: Component) {
      let data = vm.$options.data
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      // 校验 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
      // 遍历 data key
      while (i--) {
        const key = keys[i]
        // data key 不可以和 props 和 methods 中的 key 重名
        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) // 添加 key 的代理到 vm _data 以后可以 vm.key 访问
        }
      }
      // observe data
      observe(data, true /* asRootData */) // 响应式处理 data key
    }
    
  4. initComputed

    function initComputed (vm: Component, computed: Object) {
      // $flow-disable-line
      // 定义 _computedWatchers 计算属性相关的 watchers
      const watchers = vm._computedWatchers = Object.create(null)
      // computed properties are just getters during SSR
      const isSSR = isServerRendering()
    
      for (const key in computed) {
        const userDef = computed[key]
        // 计算属可以是 function 或者包含 get 的对象
        const getter = typeof userDef === 'function' ? userDef : userDef.get
        if (process.env.NODE_ENV !== 'production' && getter == null) {
          warn(
            `Getter is missing for computed property "${key}".`,
            vm
          )
        }
    
        if (!isSSR) {
          // create internal watcher for the computed property.
          // 为计算属性创建 watcher
          watchers[key] = new Watcher(
            vm,
            getter || noop,
            noop,
            computedWatcherOptions
          )
        }
    
        // component-defined computed properties are already defined on the
        // component prototype. We only need to define computed properties defined
        // at instantiation here.
        // 如果 key 在 vm 上不存在,则为 vm 设置计算属性
        if (!(key in vm)) {
          defineComputed(vm, key, userDef)
        } else if (process.env.NODE_ENV !== 'production') {
          if (key in vm.$data) {
            warn(`The computed property "${key}" is already defined in data.`, vm)
          } else if (vm.$options.props && key in vm.$options.props) {
            warn(`The computed property "${key}" is already defined as a prop.`, vm)
          }
        }
      }
    }
    
    export function defineComputed (
      target: any,
      key: string,
      userDef: Object | Function
    ) {
      const shouldCache = !isServerRendering() // 是否缓存,非 SSR 下是 true
      // sharedPropertyDefinition 属性描述符 descriptor => Object.defineProperty(obj, prop, descriptor)
      if (typeof userDef === 'function') {
        // 如果用户写的 computed 是 function 则调用 createComputedGetter 去定义属性描述符的 get
        sharedPropertyDefinition.get = shouldCache
          ? createComputedGetter(key)
          : createGetterInvoker(userDef)
        sharedPropertyDefinition.set = noop
      } else {
        // 如果是对象 则取 get
        sharedPropertyDefinition.get = userDef.get
          ? shouldCache && userDef.cache !== false
            ? createComputedGetter(key)
            : createGetterInvoker(userDef.get)
          : noop
        sharedPropertyDefinition.set = userDef.set || noop
      }
      if (process.env.NODE_ENV !== 'production' &&
          sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function () {
          warn(
            `Computed property "${key}" was assigned to but it has no setter.`,
            this
          )
        }
      }
      // 为当前实例 vm 定为 key 的计算属性
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    /**
     * 使用时 获取计算属性的值
     * @param {*} key 
     */
    
    function createComputedGetter (key) {
      return function computedGetter () {
        // 取出当前计算属性 key 的 watcher
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          // watcher dirty 为 true 证明依赖更新了,要重新计算。
          // 依赖更新会触发 watcher update 会把 watcher dirty 设置为 true
          if (watcher.dirty) {
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    
  5. initWatch

    /**
     * 在 vm 定义 key 的 watcher
     * @param {*} vm 
     * @param {*} watch 
     */
    function initWatch (vm: Component, watch: Object) {
      for (const key in watch) {
        const handler = watch[key]
        if (Array.isArray(handler)) {
          for (let i = 0; i < handler.length; i++) {
            createWatcher(vm, key, handler[i])
          }
        } else {
          createWatcher(vm, key, handler)
        }
      }
    }
    

initProvide

src/core/instance/inject.js

/**
 * provide 的初始化就是调用 _provided 如果是函数则执行,如果是其他则直接返回
 * @param {*} vm 
 */

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

结束

至此 Vue 在初始化阶段的流程就全部结束,下一章,我们来分析一下数据变化相关的源码。