Vue 2.6 源码剖析-响应式原理学习 - 2.初始化

Vue 初始化

初始化的过程

platforms 目录下是和平台相关的代码。

先查看 src/platforms/web 下打包 Vue 的入口文件(entry-*.js):

  • entry-runtime.js 打包运行时版本的入口文件
  • entry-runtime-with-compiler.js 打包完整版的入口文件
// entry-runtime.js 源码只有两行

// 导入Vue的构造函数
import Vue from './runtime/index'
// 导出
export default Vue
/src/platforms/web/entry-runtime-with-compiler.js
/* @flow */

// ...

// 导入 Vue 构造函数
import Vue from './runtime/index'
// ...

// 获取 $mount 初始定义
const mount = Vue.prototype.$mount
// 重写 $mount 方法,增加功能
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // ...
  // 判断是否定义了 render 选项
  if (!options.render) {
    // 如果没有定义 render 函数,获取tempalte,编译成render函数
    // 这里是 $mount 方法核心的部分
    // ...
  }
  // 调用 mount 方法,渲染DOM
  return mount.call(this, el, hydrating)
}

// 辅助方法
function getOuterHTML (el: Element): string {
  // ...
}

// 定义Vue静态方法 compiler:把html字符串编译成render函数
Vue.compile = compileToFunctions

// 导出构造函数
export default Vue

文件中的主要内容是:

  • 导入 Vue 构造函数
  • 重写Vue实例的$mount方法
    • 内部将template转化成render函数
  • 导出 Vue 构造函数
  • 它没有定义 Vue 构造函数

继续寻找定义 Vue 构造函数的地方。

/src/platforms/web/runtime/index
/* @flow */

// 导入模块
import Vue from 'core/index'
// ...

// install platform specific utils
// 向Vue.config注册了一些 和平台相关的特定的通用的 方法
// 这些方法在 Vue 内部使用,外部很少使用
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方法,注册了一些全局的指令和组件
// extend 方法 用于复制对象成员。将第二个参数对象中的所有成员,拷贝到第一个参数对象中
// 通过这段代码可以发现,全局的指令和组件,都存储在 Vue.options 中

// 注册组件 transition transition-group
extend(Vue.options.directives, platformDirectives)
// 注册指令 v-model v-show
extend(Vue.options.components, platformComponents)

// install platform patch function
// 在 Vue 的原型上注册 __patch__ 函数
// 类似 Snabbdom 的 patch 函数:把虚拟DOM转化成真实DOM
// inBrowser:判断是否是浏览器环境;noop:空函数
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
// 在 Vue 的原型上注册 $mount 方法
// 也就是在注册 Vue 实例上的方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  // 调用 mountComponent:渲染DOM
  return mountComponent(this, el, hydrating)
}

// devtools global hook
/* istanbul ignore next */
// 和调试相关的代码(暂不关心)
if (inBrowser) {
  // ...
}

// 导出 Vue 构造函数
export default Vue

  • 注册了和平台相关的指令和组件
  • 注册了 __patch__$mount 两个方法
  • 依然没有定义 Vue 的构造函数
/src/core/index

源码中使用 core/index的方式加载这个文件,语法看上去在加载一个模块。

flow的配置文件.flowconfigname_mapper匹配了名为 core 的模块,并将地址指向了/src/core别名,所以core/index指向/src/core/index

// 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'

// 给 Vue 挂载静态方法(这是这个文件的核心部分)
initGlobalAPI(Vue)

// 调用 Object.defineProperty 给 Vue 原型增加一些成员
// 这些成员都和 SSR(服务端渲染) 相关(暂时不关心)

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的版本
Vue.version = '__VERSION__'

// 导出
export default Vue

// core/global-api/index.js
/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // 初始化 Vue.config 对象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.

  // 定义 Vue.util:一些公用的方法
  // 上面英文翻译:这些工具方法不视作全局 API 的一部分,除非你已经意识到某些风险,否则不要去依赖它们
  // 也就是调用这个方法可能会发生意外,不建议外部使用
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  // 定义一些外部常用的静态方法
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 定义 observable 方法:让一个对象可响应
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  // 初始化 Vue.options 对象,并设置一些属性
  // components/directives/filters
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  // 设置 keep-alive 组件
  extend(Vue.options.components, builtInComponents)

  // 注册 Vue.use() 用来注册插件
  initUse(Vue)
  // 注册 Vue.mixin() 实现混入
  initMixin(Vue)
  // 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
  initExtend(Vue)
  // 注册 Vue.directive()、Vue.component()、Vue.filter()
  initAssetRegisters(Vue)
}

  • core/index 与平台无关
  • 主要部分就是 给 Vue 挂载静态方法
  • 内部没有定义 Vue 的构造函数。
core/instance/index
// ./instance/index.js
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'

// Vue 的构造函数
// 此处不用 class 的原因是为了方便后续给 Vue 实例混入实例成员
function Vue (options) {
  // 判断是否是开发环境
  // 通过this判断,是否通过 new 使用 Vue 构造函数
  // 如果当作普通函数调用 -> Vue() 就发出警告
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 调用 _init() 方法
  this._init(options)
}

// 注册 vm 的 _init() 方法:初始化 vm
initMixin(Vue)
// 继续混入(注册) vm 的成员:$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

export default Vue

  • 与平台无关
  • 创建了 Vue 的构造函数
  • 设置 Vue 实例的成员
为何 Vue 不用 ES6 的 Class 去实现

从源码中看到后面有很多 xxxMixin 的函数调用,并把Vue作为参数传入。

它们的功能都是给 Vue 的prototype 上扩展一些方法,也就是向 Vue 实例混入成员。

Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有。

这么做的好处是方便代码的维护和管理。

这种方式用 Class (的标准用法)难以实现。

Class 创建的类也可以通过prototype添加成员,这在写法上违背了它的语法糖。

Class的标准用法:

  • 一个类就是一个模块
  • 所有成员都应该在类内部定义,或继承自父类,而不是使用 prototype 定义成员
  • prototype 不应该和 Class 搭配使用

所以这里相对于它的标准用法,function要方便。

// 非标准用法
class Vue {}
Vue.prototype.$set = () => {}

// 标准用法1:继承
class GlobalApi {
  $set() {}
}
class Vue extends GlobalApi {}

// 标准用法2:直接定义
class Vue {
  $set() {}
}
总结:四个导出 Vue 的模块
  • src/platforms/web/entry-runtime-with-compiler.js
    • web 平台相关的入口,核心就是增加了编译的功能
      • 重写了平台相关的 $mount 方法
        • $mount方法内部编译模板,将template 转换成 render函数
      • 注册了 Vue.compile() 方法
        • 传递一个 HTML 字符串 返回 render 函数
  • src/platforms/web/runtime/index.js
    • 与 web 平台相关
    • 注册和平台相关的全局指令,挂载到 Vue.options.directives
      • v-model
      • v-show
    • 注册和平台相关的全局组件,挂载到 Vue.options.components
      • transition
      • transition-group
    • 定义了两个全局方法:
      • __patch__ :把虚拟DOM转换成真实DOM
      • $mount:挂载方法,把DOM渲染到界面中
        • 该文件中定义 $mount
        • 在入口文件中重写了 $mount,增加了编译的能力
  • src/core/index.js
    • 与平台无关
    • 核心作用:
      • 内部直接设置了一些静态方法
      • 还调用 initGlobalAPI(Vue) 定义了一些静态方法
  • src/core/instance/index.js
    • 与平台无关,与实例(instance)相关
    • 内部定义了Vue的构造函数,构造函数内部调用了 this._init(options) 方法
      • _init 相当于整个程序的入口
    • 给Vue中混入了常用的实例成员

之后会详细查看具体注册了哪些静态成员和实例成员。

两个问题

语言检查

vscode中查看源码中的 Flow 语法时(例如声明类型 Vue: GlobalAPI)会有红色波浪线,提示:"types" 只能在 .ts 文件中使用

vscode 和 TypeScript 都是微软开发的,所以 vscode 默认支持 TS 语法。

TypeScript 与 Flow 语法类似,而 vscode 不支持 Flow,所以这里 vscode 将其识别为 TS 语法。

TS的语法只能在.ts文件中使用,当前是 .js 文件,所以判定为校验错误。

解决方法:在 vscode 配置中关闭 JS 语法校验:

// settings.json
// 设置不检查 js 的语法问题,防止 flow 报错
"javascript.validate.enable": false
泛型影响后面代码高亮

在查看src\core\global-api\index.js源码时,有一段代码使用了泛型。之后的代码:

  • 没有高亮显示了
  • 几乎无法使用 转到定义Ctrl + 左键 的功能

在这里插入图片描述

因为vscode在识别到泛型的时候会有问题。

解决高亮的方法:安装插件 Babel JavaScript

在这里插入图片描述

转到定义Ctrl + 左键 暂时没有办法解决。

在这里插入图片描述

静态成员

src\core\index.js文件中调用的 initGlobalAPI方法中,初始化了 Vue 的大部分静态成员。

initGlobalAPI

注册全局API。

查看 src\core\global-api\index.js 中定义的这个方法:

  • 初始化了 Vue.config
    • 之后在src\platforms\web\runtime\index.js中向Vue.config 挂载了一些方法

在这里插入图片描述

  • 设置了 Vue.util,挂载了一些API
    • 这些 API 用于内部使用,官方文档未提供,也不推荐使用。(不用关心)
  • 定义了常用的方法:
    • set
    • delete
    • nextTick
    • observable
  • 初始化了 Vue.options
    • 先使用Object.create(null)初始化这个对象,表示这个对象不需要原型,可以提高它的性能
    • 然后遍历ASSET_TYPES,获取属性名并初始化为不需要原型的空对象
      • 由于泛型使用,此处无法通过 转到定义 查看ASSET_TYPES,需要手动查找。
    • ASSET_TYPES 初始化的3个成员:components/directives/filters
      • 用于存储全局的组件、指令、过滤器
      • 通过 ``Vue.componetVue.directiveVue.filter注册的全局的组件、指令、过滤器,都会存储到Vue.options.[components/directives/filters]`中去。
  • Vue.options._base中记录 Vue 的构造函数
    • weex 场景中使用
  • 使用 extend 注册内置组件 keep-alive
    • extend方法:将对象的所有属性(参数1),(浅)拷贝到另一个对象中(参数2)
    • extend方法定义位置:src\shared\util.js
      • 这个文件中定义了很多通用的方法
  • 最后调用几个函数,分别为Vue注册了几个静态方法
    • Vue.use
    • Vue.mixin
    • Vue.extend
    • Vue.component、Vue.directive、Vue.filter
// src\core\global-api\index.js
/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  // 首先定义 config 属性
  const configDef = {}
  // 定义 config 的属性描述符
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    // 开发环境增加一个 set 方法,禁止给 config 属性直接赋值
    // 之后在`src\platforms\web\runtime\index.js`中向Vue.config 挂在了一些方法
    configDef.set = () => {
      warn(
        // 警告:不要对Vue.config重新赋值,可以挂载或修改对象中的属性。
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // 初始化 Vue.config 对象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.

  // 定义 Vue.util:一些公用的方法
  // 上面英文翻译:这些工具方法不视作全局 API 的一部分,除非你已经意识到某些风险,否则不要去依赖它们
  // 也就是调用这个方法可能会发生意外,不建议外部使用
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  // 定义一些外部常用的静态方法
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 定义 observable 方法:让一个对象可响应
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  // 初始化 Vue.options 对象,挂载3个成员,并初始化为空对象
  // components/directives/filters
  // 3个成员用于存储全局的组件、指令、过滤器

  // Object.create(null) 没有原型,可以提高性能
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  // 记录 Vue 构造函数到 _base
  Vue.options._base = Vue

  // 注册内置组件 keep-alive
  extend(Vue.options.components, builtInComponents)

  // 注册 Vue.use() 用来注册插件
  initUse(Vue)
  // 注册 Vue.mixin() 实现混入
  initMixin(Vue)
  // 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
  initExtend(Vue)
  // 注册 Vue.directive()、Vue.component()、Vue.filter()
  initAssetRegisters(Vue)
}

ASSET_TYPES
// `ASSET_TYPES`
// src\shared\constants.js
// 设置服务端渲染的标签的属性
// 如果一个标签上包含这个属性,说明它是服务端渲染出来的
export const SSR_ATTR = 'data-server-rendered'

// options下的一些属性名
export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

// 生命周期的所有函数名称
export const LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
]

extend
// extend方法
// src\shared\util.js
/**
 * Mix properties into target object.
 */
export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}
initUse

注册 Vue.use()

// initUse(Vue)
// src\core\global-api\use.js
/* @flow */

import { toArray } from '../util/index'

export function initUse (Vue: GlobalAPI) {
  // 定义use,接收一个参数:插件
  Vue.use = function (plugin: Function | Object) {
    // 定义一个常量,获取已安装的插件
    // Vue实例中的_installedPlugins存储所有已安装的插件
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    // 判断插件是否安装
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    // 获取注册插件用的参数,去除参数1(插件)
    // toArray:用于将参数1转化为数组,并去除前n(参数2)个元素
    const args = toArray(arguments, 1)
    // 把 Vue 插入到第一个元素的位置
    args.unshift(this)
    // 如果插件参数是一个包含install方法的对象,执行install方法
    // 如果插件参数是一个函数,直接执行
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    // 将插件添加到已安装的记录中
    installedPlugins.push(plugin)
    return this
  }
}

initMixin

注册 Vue.mixin()

// initMixin
// src\core\global-api\mixin.js
/* @flow */

import { mergeOptions } from '../util/index'

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    // 将传入的对象中的成员,拷贝到 Vue.options 中
    // 所以 mixin 注册的选项,是全局注册
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

initExtend

注册 Vue.extend() 。

Vue.extend() 返回一个组件的构造函数,用于自定义组件。

// src\core\global-api\extend.js
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'

export function initExtend (Vue: GlobalAPI) {
  /**
   * 每个实例构造函数(包括Vue)都又一个唯一的 cid。
   * 这使我们能够创建用于原型继承的 child 构造函数并缓存它们。
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    // 获取 Vue 构造函数
    const Super = this
    const SuperId = Super.cid
    // 先判断是否可以从缓存中获取
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

    // 创建组件的构造函数
    const Sub = function VueComponent (options) {
      // 内部调用 _init() 初始化
      this._init(options)
    }
    // 改造构造函数的原型,使其继承自 Vue
    // 所以所有的 Vue 组件都继承自 Vue
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    // 合并选项
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // 下面就是将 Vue 的成员拷贝到 Sub 组件中

    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })

    // 把组件构造函数保存到 [Vue/Sub].options.components 对象
    if (name) {
      Sub.options.components[name] = Sub
    }

    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    // 把组件的构造函数缓存到 options._Ctor
    cachedCtors[SuperId] = Sub
    // 返回构造函数
    return Sub
  }
}

function initProps (Comp) {
  const props = Comp.options.props
  for (const key in props) {
    proxy(Comp.prototype, `_props`, key)
  }
}

function initComputed (Comp) {
  const computed = Comp.options.computed
  for (const key in computed) {
    defineComputed(Comp.prototype, key, computed[key])
  }
}

initAssetRegisters

注册 Vue.component、Vue.directive、Vue.filter。

这3个方法的参数几乎一样,所以放在一个方法中注册。

  • Vue.component( {String} id, { Function | Object } [definition\] )
  • Vue.directive( { String } id, { Function | Object } [definition] )
  • Vue.filter( { String } id, { Function | Object } [definition] )
// src\core\global-api\assets.js
/* @flow */

import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  // 遍历 ASSET_TYPES 数组,为 Vue 定义相应方法
  // ASSET_TYPES: [directive, component, filter]
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        // 获取
        return this.options[type + 's'][id]
      } else {
        // 创建
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        
        // isPlainObject 通过 toString() 判断是否是 原始Object对象

        // 组件处理
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          // Vue.options._base 存储的就是 Vue 构造函数
          // 这里调用 Vue.extend 把组件配置definition  转换为 组件的构造函数
          definition = this.options._base.extend(definition)
        }

        // 指令处理
        if (type === 'directive' && typeof definition === 'function') {
          // 如果配置是函数,就包装一下
          definition = { bind: definition, update: definition }
        }

        // 将配置记录到对应全局中
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

isPlainObject & _toString
// isPlainObject 判断是否是原始 Object 对象
// src\shared\util.js
/**
 * Get the raw type string of a value, e.g., [object Object].
 */
const _toString = Object.prototype.toString

/**
 * Strict object type check. Only returns true
 * for plain JavaScript objects.
 */
export function isPlainObject (obj: any): boolean {
  return _toString.call(obj) === '[object Object]'
}

实例成员

src/core/instance目录是和实例相关的代码,core目录下是和平台无关的。

src\core\instance\index.js中定义了 Vue 的构造函数,并调用了几个xxxMixin()函数注册实例成员。

  • 函数名都以 Mixin 结尾,表示混入
  • 函数都接收 Vue 构造函数为参数
  • 函数都做了类似的事情:给 Vue 原型/实例 混入成员
initMixin

初始化 _init 方法。

// src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
  // 给 Vue 实例挂载 _init() 方法
  Vue.prototype._init = function (options?: Object) {
    // ...
  }
}
stateMixin
  • 初始化 d a t a 、 data、 dataprops 属性
  • 初始化 s e t 、 set、 setdelete、$watch 方法
// src\core\instance\state.js
export function stateMixin (Vue: Class<Component>) {
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  // 非生产环境下定义set,禁止直接对属性赋值
  if (process.env.NODE_ENV !== 'production') {
    dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data. ' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  // 使用 Object.defineProperty 定义设置了 set 描述符的属性
  // set 方法用于判断非生产环境下,不允许直接对属性赋值
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    // ...
  }
}
eventsMixin

初始化事件相关的方法:$on/$once/$off/$emit

这里的事件机制使用的是 发布订阅模式

// src\core\instance\events.js
/* @flow */

// ...

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 eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {
      // 如果绑定多个事件,则循环调用$on
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      // 向_events中对应的事件回调列表中,添加回调函数
      // _events 对象由 initEvents 方法创建
      (vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    // ...
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    // ...
  }

  Vue.prototype.$emit = function (event: string): Component {
    // ...
  }
}

lifecycleMixin

混入了和生命周期相关的方法:_update/$forceUpdate/$destroy

  • _update 方法的作用是把 VNode 渲染成真实的 DOM
    • 核心部分就是调用了 __patch__ 方法
      • 把虚拟DOM转换成真实DOM,存储到$el中
    • 首次渲染会调用,数据更新会调用
  • $forceUpdate 强制更新,调用 vm._watcher.update()
  • $destroy 销毁 Vue 实例
// src\core\instance\lifecycle.js

export function lifecycleMixin (Vue: Class<Component>) {
  // _update 方法的作用是把 VNode 渲染成真实的 DOM
  // 首次渲染会调用,数据更新会调用
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // ...
    
    // 核心部分:调用__patch__,把虚拟DOM转换成真实DOM,最终挂载到 $el
    if (!prevVnode) {
      // initial render
      // 首次渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      // 数据变更渲染
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    // ...
  }

  // $forceUpdate 强制更新,调用 vm.watcher.update()
  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

  // $destroy 销毁 Vue 实例
  Vue.prototype.$destroy = function () {
    // ...
  }
}

renderMixin
  • installRenderHelpers 注册了和渲染相关的一些方法
  • 定义了 $nextTick
  • 定义了 _render
    • 调用用户传入的render 或 tempalte转换的render函数
// src\core\instance\render.js
export function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  // 安装渲染相关的帮助方法
  installRenderHelpers(Vue.prototype)

  // 定义$nextTick
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    // 获取用户定义的render 或者 通过template转换的 render函数
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      currentRenderingInstance = vm
      // 调用render函数
      // vm.$createElement -> h函数:生成虚拟DOM
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      // ...
    } finally {
      currentRenderingInstance = null
    }
    // ...
    return vnode
  }
}

installRenderHelpers
// installRenderHelpers
// src\core\instance\render-helpers\index.js
/* @flow */

// import ...

// 这里的方法都和渲染相关,将来在编译的时候使用
// 在把模板编译成render函数时,render函数会调用这些方法
export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode // 创建文本虚拟节点
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

_init

当静态成员和实例成员都初始化完毕之后,接着就来调用Vue 的构造函数。

Vue 的构造函数中调用 _init 方法,_init方法是在initMixin中初始化的。

_init 函数的作用:

  • 初始化了一些实例成员
  • 触发 beforeCreate 和 created 钩子函数
  • 调用 $mount 渲染页面
// src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
  // 给 Vue 实例挂载 _init() 方法
  Vue.prototype._init = function (options?: Object) {
    // 定义一个常量,记录Vue实例
    const vm: Component = this
    // a uid
    // 定义一个唯一标识
    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)
    }

    // _isVue 标识当前实例是Vue的实例
    // 目的是:将来调用 observe 方法(设置响应式数据)的时候不对它做处理
    vm._isVue = true
    // 合并 options
    // 将用户传入的 options 和 Vue 构造函数中初始化的options 进行合并
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    // 设置渲染时候的代理对象 _renderProxy
    // 开发环境+支持Proxy时:使用new Proxy 代理 Vue 实例
    // 其他环境:Vue 实例
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    vm._self = vm
    // 调用一些 initXxxx 函数对 Vue 实例进行初始化

    // 初始化和生命周期相关的一些属性
    // $parent/$root/$children/$refs 等
    initLifecycle(vm)
    // 初始化当前组件的事件,绑定父组件上附加的事件
    initEvents(vm)
    // 初始化 render 中使用的h函数 和 其他几个属性
    // $slot/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)
    // 调用生命周期钩子函数:beforeCreate
    callHook(vm, 'beforeCreate')
    // 实现依赖注入(provide/inject):把 inject 的成员注入到 vm 上
    initInjections(vm) // resolve injections before data/props
    // 初始化 vm 的 _props/methods/_data/computed/watch
    initState(vm)
    // 实现依赖注入(provide/inject):记录 provide
    initProvide(vm) // resolve provide after data/props
    // 调用生命周期钩子函数:created
    callHook(vm, 'created')

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

    // 最后调用 $mount 方法挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}
initProxy
// src\core\instance\proxy.js
// ...
let initProxy

if (process.env.NODE_ENV !== 'production') {
  // ...
  
  const hasProxy =
    typeof Proxy !== 'undefined' && isNative(Proxy)
    
  // ...
  initProxy = function initProxy (vm) {
    // 判断当前环境下是否支持 Proxy
    if (hasProxy) {
      // 如果支持,使用new Proxy初始化_renderProxy
      // 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 {
      // 如果不支持,就设置成 Vue 实例
      vm._renderProxy = vm
    }
  }
}

export { initProxy }
initLifecycle

初始化和生命周期相关的一些属性

  • 找到当前组件的父组件,将当前组件添加到父组件的$children中
  • 初始化 p a r e n t / parent/ parent/root/ c h i l d r e n / children/ children/refs
  • 初始化一些私有成员
// src\core\instance\lifecycle.js
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  // 找到当前组件的父组件
  // 将当前组件添加到父组件的$children中
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  // 初始化$parent/$root/$children/$refs
  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
}
initEvents

初始化当前组件的事件。

  • 初始化存储事件的属性 _events
  • 核心部分:获取父组件上附加的事件,注册到当前组件上。
// src\core\instance\events.js
export function initEvents (vm: Component) {
  // 初始化 _events 属性为一个空对象
  // _events 用于存储事件名称及对应的处理函数
  // _events: {'click': [fn1, fn2]}
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  // 获取父组件上附加的事件
  const listeners = vm.$options._parentListeners
  if (listeners) {
    // 把当前父组件上附加的事件注册到当前组件上
    updateComponentListeners(vm, listeners)
  }
}
initRender

初始化 render 中使用的h函数 和 其他几个属性:

  • s l o t / slot/ slot/scopedSlots
  • _c/$createElement
  • a t t r s / attrs/ attrs/listeners
// src\core\instance\render.js
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
  // 初始化和插槽相关的属性:$slots $scopedSlots
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject

  // 初始化使用 createElement 的方法:把虚拟DOM转换成真实DOM

  // 对编译生成的 render 进行渲染的方法
  // _c是在 template 选项转换成的 render 函数中调用
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

  // 对手写 render 函数进行渲染的方法
  // $createElement 是在用户手写的render选项中使用的 h 函数
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  // 通过 defineReactive 定义响应式数据:$attrs $listeners
  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)
  }
}
initProvide & initInjections

Vue 的 provide/inject 选项一起使用,以允许一个组件向其所有子孙后代注入依赖,不论组件层次有多深。

initProvide 和 initInjections 用于实现(provide/inject 选项的)依赖注入。

initProvide
// src\core\instance\inject.js
export function initProvide (vm: Component) {
  // 获取选项中的 provide 注入到 vm._provided
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}
initInjections

用于获取 vm. o p t i o n s . i n j e c t 和 v m . options.inject 和 vm. options.injectvm.options.provide(来自祖先组件)的共同属性,将它们设置为 vm 的响应式属性。

// src\core\instance\inject.js
export function initInjections (vm: Component) {
  // resolveInject 用于获取 vm.$options.inject 和 vm.$options.provide(来自祖先组件) 的共同属性
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    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])
      }
    })
    toggleObserving(true)
  }
}
initState

初始化 vm 的 _props/methods/_data/computed/watch。

// src\core\instance\state.js
export function initState (vm: Component) {
  vm._watchers = []
  // 获取 Vue 实例的 $options
  const opts = vm.$options

  // 判断 $options 中的属性,并初始化它们

  // 把 props 中的成员转换成响应式数据,并且注入到Vue实例中
  if (opts.props) initProps(vm, opts.props)
  // 把 methods 中的成员注入到Vue实例
  if (opts.methods) initMethods(vm, opts.methods)
  // 把 data 中的成员转换成响应式数据,并且注入到 Vue 实例中
  if (opts.data) {
    initData(vm)
  } else {
    // 如果没有定义data选项,就初始化一个空的响应式对象
    observe(vm._data = {}, true /* asRootData */)
  }
  // 把 计算属性 注入到 Vue 实例
  if (opts.computed) initComputed(vm, opts.computed)
  // 把 侦听器 注入到 Vue 实例
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
initProps

把props中的成员转换成响应式数据,并且注入到Vue实例中

// src\core\instance\state.js
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  // 初始化 _props 为一个空对象
  const props = vm._props = {}
  
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  
  if (!isRoot) {
    toggleObserving(false)
  }
  // 遍历 $options.props
  // 通过 defineReactive 将它们转换成 响应式 并注入到 vm._props 中
  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)
    }
    
    // 判断Vue实例中是否存在该属性
    if (!(key in vm)) {
      // 如果不存在,通过proxy方法把它注入到Vue实例中
      // 结果:通过this._props[key]设置和获取
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}
proxy
// src\core\instance\state.js
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
initMethods

把 methods 中的成员注入到Vue实例,在注入之前进行一些校验并发出警告:

  • 校验 methods 中的属性是否是 function
  • 校验方法名是否与 props 中属性同名
  • 校验是否是重定义 私有成员(下划线 _ 开头) 或 Vue提供的成员($ 开头)
// src\core\instance\state.js
function initMethods (vm: Component, methods: Object) {
  // 获取 props,用于校验重名属性
  const props = vm.$options.props
  // 遍历方法名称
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      // 判断 methods 中的成员必须是 function
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm 
        )
      }
      // 判断 methods 中的方法名称是否和 props 中的属性重名
      // 因为最终methods和props中的成员都要注入到Vue实例中
      // 所以它们中不允许有同名的属性
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      // 判断属性名是否在 Vue 实例中存在,并且是否是私有成员(_开头)或Vue提供的成员($开头)
      // isReserved 判断key是否以 _ 或 $ 开头
      // Vue 不建议 methods 中重定义 私有成员 和 Vue 提供的成员
      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
    // noop:空函数
    // bind:改变函数的this指向
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}
initData

把 data 中的成员转换成响应式数据,并且注入到 Vue 实例中,在注入之前进行一些校验:

  • 如果 data 选项是一个函数,获取函数调用的结果
  • 校验是否与 props 和 methods 中属性同名,并发出警告
  • 校验不能是 私有成员(下划线 _ 开头) 或 Vue 提供的成员($ 开头)
// src\core\instance\state.js
function initData (vm: Component) {
  // 获取data选项
  let data = vm.$options.data
  // 初始化 _data,判断 data 选项是否是函数
  // 如果是函数(组件中定义data):就初始化为调用这个函数返回的结果
  // 否则(Vue实例中定义data):直接初始化为 data
  // getData:return data.call(vm, vm)
  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
  // 获取 data 中的所有属性名
  const keys = Object.keys(data)
  // 获取 props/methods,用于判断是否有同名属性
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 判断 data 上的成员是否和 props/methods 中的成员重名
  // 如果重名,发出警告
  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)) {
      // 判断如果是以下划线 _ 或 $ 开头,就不会注入到 Vue 实例中
      // 否则,通过proxy方法把它注入到Vue实例中
      // 结果:通过this._data[key]设置和获取
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 最后把data转化成响应式对象
  // asRootData 注释表示这是根数据(根实例的 data 选项)
  observe(data, true /* asRootData */)
}
initComputed & initWatch

把 计算属性 和 侦听器 注入到 Vue 实例中。

总结 和 调试 Vue 首次渲染

Vue 首次渲染包括:

  • Vue 初始化过程
    • 初始化静态成员
    • 初始化实例成员
  • Vue 构造函数中调用 _init 方法
    • 添加一些实例成员
    • 转换响应式数据
    • 渲染视图
调试代码
<div id="app">
  {{ msg }}
</div>
<script src="../../dist/vue.js"></script>
<script>
  const vm = new Vue({
    el: "#app",
    data: {
      msg: 'Hello Vue'
    }
  });
</script>
断点位置

调试过程,主要调试 4 个导出Vue的文件,在这4个文件中对Vue做了初始化。

  • core 目录下与平台无关的两个文件:
    • 初始化实例成员的地方:core/instance/index.js
      • 在 initMixin(Vue) 位置打断点
    • _init 的地方:core/instance/index.js
      • 在 this._init(options) 位置打断点
    • 初始化静态成员的地方:core/index.js
      • 在 initGlobalAPI(Vue) 位置打断点
  • platforms/web 目录下与平台有关的两个文件:
    • platforms/web/runtime/index.js
      • Vue.config.mustUseProp = mustUseProp 位置
    • 打包入口文件:platforms/web/entry-runtime-with-compiler.js
      • const mount = Vue.prototype.$mount 位置
调试方法

刷新页面停止在断点时,在Watch面板下可以添加要监视的变量,例如Vue

调试中,可以通过观察 Vue 监视 静态成员和实例成员的变化。

core/instance/index.js - 初始化

初始化实例成员的的过程,查看Vue.prototype 变化过程:

  • initMixin
    • _init
  • stateMixin
    • $data - undefined,将来通过选项进行赋值
    • $props - undefined,将来通过选项进行赋值
    • $set
    • $delete
    • $watch
  • eventsMixin
    • $on
    • $once
    • $off
    • $emit
  • lifectcleMixin
    • _update - 内部调用 patch 方法
    • $forceUpdate
    • $destroy
  • renderMixin
    • 挂载了一些 下划线 _ 开头的方法,如 _b、_d、_e 等
      • 这些方法的作用:当把template模板转换成render函数时,在render函数中会调用这些方法
    • $nextTick
    • _render
      • 调用 用户传入的 render 函数 或者 template模板转换的 render 函数

F8 进入下一个断点。

core/index.js - initGlobalAPI

初始化静态成员的过程。

  • Vue.config
  • Vue.util
  • Vue.set
  • Vue.delete
  • Vue.nextTick
  • Vue.observable
  • Vue.options - 先初始化为一个没有原型的空对象,然后添加一些成员
    • components - 用于存储全局的组件
    • directives - 用于存储全局的指令
    • filters - 用于存储全局的过滤器
    • _base - 存储Vue的构造函数
  • 注册 keep-alive 组件
    • 存储到了 Vue.options.components 中
  • 调用一些 initXxxx 函数,初始化 Vue 的一些静态方法
    • Vue.use
    • Vue.mixin
    • Vue.extend
    • Vue.directive、Vue.component、Vue.filter
      • 用于注册全局的 组件、指令、过滤器,分别存储到 Vue.options 对应的属性中
platforms/web/runtime/index.js
  • 给 Vue.config 中注册了一些和平台相关的方法(暂不关心):mustUseProp、isReservedTag、isReservedAttr、getTagNamespace、isUnknownElement
  • 注册了一些和平台相关的指令和组件
    • 查看 Vue.options.directives
      • model
      • show
    • 查看 Vue.options.components
      • Transition
      • TransitionGroup
  • 在Vue.prototype(原型)上挂载两个方法,将来会在 _init 方法中调用:
    • __patch__
    • $mount
platforms/web/entry-runtime-with-compiler.js
  • 获取 原型 中的 $mount 方法, 然后重写 $mount。
    • 新增了 把 template 模板编译成 render 函数的功能。
  • Vue.compile
    • 用于手动把 template 模板转换成 render 函数
core/instance/index.js - _init
  • 设置 _isVue
    • 将来在响应式处理(observe)时,用于判断如果当前是Vue实例,不进行响应式处理
  • 合并options
    • 将用户传入的options和构造函数中的options合并
  • 设置 _renderProxy - 渲染时候的代理对象
  • 调用 initXxxx 方法,给 Vue 实例挂载一些成员
  • 调用 $mount 挂载页面
    • 此时调用的是重写后的 $mount方法
      • 它的作用是把 template 编译成 render 函数
      • 并在最后调用了 初始定义的 $mount
// 重写的 $mount
// src\platforms\web\entry-runtime-with-compiler.js
// 获取 $mount 初始定义
const mount = Vue.prototype.$mount
// 重写 $mount 方法,增加功能:把template编译成render函数
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 获取 el 对象
  el = el && query(el)

  /* istanbul ignore if */
  // el 不能是 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
  // 判断是否定义了 render 选项 
  if (!options.render) {
    // 如果没有定义 render 函数,获取tempalte,编译成render函数
    // 这里是 $mount 方法核心的部分
    let template = options.template
    // 如果模板存在
    if (template) {
      // 判断 template 是否是字符串 或者 拥有 nodeType 属性
      if (typeof template === 'string') {
        // 如果是 id 选择器
        if (template.charAt(0) === '#') {
          // 获取对应的 DOM 对象的 innerHTML
          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) {
        // 如果有 nodeType 属性,说明template是DOM元素
        // 返回元素的 innerHTML
        template = template.innerHTML
      } else {
        // 如果模板不是 字符串 或 DOM元素,则不合法
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        // 直接返回当前实例
        return this
      }
    } else if (el) {
      // 如果选项中没有 template,获取el的 outerHTML 作为模板
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      // 调用 compileToFunctions 把 template 转换成 render 函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 将render 函数存储到 options
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // 调用 mount 方法,渲染DOM
  return mount.call(this, el, hydrating)
}
// 初始定义的 $mount
// src\platforms\web\runtime\index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  // 调用 mountComponent:渲染DOM
  return mountComponent(this, el, hydrating)
}
mountComponent

这是 Vue 的核心代码。

mountComponent 与平台无关。

// src\core\instance\lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  
  // 判断选项中是否有 render 函数
  // 判断如果没有 render,但是传入了 template
  // 说明当前是运行时版本的Vue
  // 因为完整版会把tempalte编译成render存入options选项
  
  // 用于判断运行时版本(不带编译器)使用template的情况,发出警告
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      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
        )
      }
    }
  }

  // 触发生命周期钩子函数:beforeMount
  callHook(vm, 'beforeMount')

  // 创建并定义 updateComponent 函数:更新组件(挂载)
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // 如果是开发环境,并且启动了性能监测
    // 就添加一些性能监测的代码(暂不关心)
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      // 内部调用了 _update 和 _render 方法
      // _render:调用用户传入的render 或 编译器生成的render,最终返回虚拟DOM
      // _update:将传入的虚拟DOM转换成真实DOM,更新到界面
      vm._update(vm._render(), hydrating)
    }
  }

  // 创建 Watcher 对象,并传入 updateComponent 方法
  // updateComponent 是在 Watcher 中调用的
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    // 最终触发 mounted 钩子函数
    callHook(vm, 'mounted')
  }
  return vm
}
Watcher 构造函数

observer 目录下的文件都是和响应式相关的。

Vue 中的Watcher有3种:

  • 渲染 Watcher
    • mountComponent 中创建的 Watcher
  • 计算属性 Watcher
  • 侦听器 Watcher

所以 计算属性 和 侦听器 最终也是通过Watcher实现的。

// src\core\observer\watcher.js
// ...
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // 是否是 渲染 Watcher
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)

    // 下面定义了很多属性(暂不关心)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      // Watcehr中最终要更新视图
      // lazy 表示是否延迟更新视图
      // 计算属性的Watcher:lazy为true
      // 因为计算属性要在数据变化后才会更新视图
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      // 如果 expOrFn 是方法,直接设置为getter
      this.getter = expOrFn
    } else {
      // 侦听器的 Watcher 会传入字符串(侦听的属性),例如'person.name'
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    // 判断是否延迟执行
    // get中调用了当前组件的getter,并将this指向Vue实例
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    // 把当前的 Watcher 对象存入到 targetStack(target栈)中
    // pushTarget的作用:
    //  每一个组件都会对应一个 Watcher,Watcher会渲染视图
    //  如果组件有嵌套的话会先渲染内部的组件
    //  所以它要把父组件对应的Watcher先保存起来
    // 它还负责给 Dep.target 赋值,用于为响应式数据收集依赖
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用 getter
      value = this.getter.call(vm, vm)
    } catch (e) {
			// ...
    } finally {
      // ...
    }
    return value
  }

	// ...
}
首次渲染过程总结
  1. Vue 初始化:实例成员、静态成员
  2. new Vue() 调用 Vue 的构造函数
  3. 构造函数中调用 this._init(),相当于整个Vue 的入口
    1. _init() 方法中调用了两个 vm.$mount()
  4. 入口文件中重写的$mount(src\platforms\web\entry-runtime-with-compiler.js
    1. 核心作用:如果没有传render,把template模板编译成render函数
      1. 通过 compileToFunctions() 生成 render() 渲染函数
    2. 编译好后存入到 options.render 中
  5. 原始定义的 $mount(src\platforms\web\runtime\index.js
    1. 调用 mountComponent
  6. mountComponent(src\core\instance\lifecycle.js
    1. 判断如果没有 render,但是传入了 template,说明当前是运行时版本的Vue(完整版会把tempalte编译成render存入options选项),开发环境会发出警告
    2. 触发 beforeMount 钩子函数
    3. 定义 updateComponent
      1. 此处仅仅定义这个函数
      2. 在这个函数中调用了 vm._update 和 vm._render
        1. _render - 渲染:生成虚拟DOM
          1. 内部最终调用了 用户传入的render 或 编译template 生成的 render
          2. 返回 VNode
        2. _update - 更新:将虚拟DOM转换成真实DOM,更新到视图
          1. 调用了 __patch__ 方法挂载真实DOM
          2. 记录到 vm.$el
    4. 创建 Watcher 实例
      1. 传入 updateComponent 函数
      2. 创建完 Watcher 会调用一次 get() 方法
        1. 执行了传入的 updateComponent 函数
    5. 触发 mounted 钩子函数
    6. 最终返回 Vue 实例
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值