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的配置文件.flowconfig
中name_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 函数
- 重写了平台相关的 $mount 方法
- web 平台相关的入口,核心就是增加了编译的功能
- 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.componet
、
Vue.directive、
Vue.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、 data、props 属性
- 初始化 s e t 、 set、 set、delete、$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中
- 首次渲染会调用,数据更新会调用
- 核心部分就是调用了 __patch__ 方法
- $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.inject和vm.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) 位置打断点
- 初始化实例成员的地方:core/instance/index.js
- platforms/web 目录下与平台有关的两个文件:
- platforms/web/runtime/index.js
Vue.config.mustUseProp = mustUseProp
位置
- 打包入口文件:platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
位置
- platforms/web/runtime/index.js
调试方法
刷新页面停止在断点时,在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 函数
- 挂载了一些 下划线 _ 开头的方法,如 _b、_d、_e 等
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.options.directives
- 在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方法
// 重写的 $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
}
// ...
}
首次渲染过程总结
- Vue 初始化:实例成员、静态成员
- new Vue() 调用 Vue 的构造函数
- 构造函数中调用 this._init(),相当于整个Vue 的入口
- _init() 方法中调用了两个 vm.$mount()
- 入口文件中重写的$mount(
src\platforms\web\entry-runtime-with-compiler.js
)- 核心作用:如果没有传render,把template模板编译成render函数
- 通过 compileToFunctions() 生成 render() 渲染函数
- 编译好后存入到 options.render 中
- 核心作用:如果没有传render,把template模板编译成render函数
- 原始定义的 $mount(
src\platforms\web\runtime\index.js
)- 调用 mountComponent
- mountComponent(
src\core\instance\lifecycle.js
)- 判断如果没有 render,但是传入了 template,说明当前是运行时版本的Vue(完整版会把tempalte编译成render存入options选项),开发环境会发出警告
- 触发 beforeMount 钩子函数
- 定义 updateComponent
- 此处仅仅定义这个函数
- 在这个函数中调用了 vm._update 和 vm._render
- _render - 渲染:生成虚拟DOM
- 内部最终调用了 用户传入的render 或 编译template 生成的 render
- 返回 VNode
- _update - 更新:将虚拟DOM转换成真实DOM,更新到视图
- 调用了 __patch__ 方法挂载真实DOM
- 记录到 vm.$el
- _render - 渲染:生成虚拟DOM
- 创建 Watcher 实例
- 传入 updateComponent 函数
- 创建完 Watcher 会调用一次 get() 方法
- 执行了传入的 updateComponent 函数
- 触发 mounted 钩子函数
- 最终返回 Vue 实例