虽然 Vue
出来很长时间了,但是还没有看过源码,只是在刷面试题的时候会涉及到一些源码上的问题,第一次阅读 Vue
源码感觉有些吃力,希望能够坚持下去。
关于学习源码
之前也有看过源码的冲动,去 Vue
官网 clone
下来之后就开始看,毫无目的,然后就没有然后了。
最近又有了这个冲动,但是没有像之前那样,而是先在网上看了一些学习源码的文章,看了看他们的建议。看了很多文章,其中觉得写得不错的屈指可数,最清晰明了的唯属 HcySunYang 的 Vue2.1.7源码学习 ,很适合初学者,这篇文章就是我跟着这篇文章的学习笔记。
看源码前辈们的建议:
- 看代码要带着问题,不要盲目的去看
- 你的问题就是你的主线,要时刻记着你的主线是什么,那些和主线没有多大关系的该忽略就暂时忽略掉
- 在网上找到高质量的博客或视频,跟着前辈走会让你事半功倍
现在的我重新开始了,希望看源码和我跳绳一样能够坚持下去。
Vue 版本
我看的是 Vue 2.6.11
版本,以后所有笔记都会基于这个版本来输出。
目录结构
dist // 构建后文件的输出目录
examples // 存放一些使用 Vue 开发的应用案例
flow // 包含 Flow 的类型声明。这些声明是全局加载的,您将在普通源代码的类型注释中看到它们。
packages // 包含 vue-server-renderer 和 vue-template-compiler。
scripts // 包含与构建相关的脚本和配置文件。
alias.js // 在所有源代码和测试中使用的模块导入别名。
config.js // 包含在 package.json 中找到的所有文件的构建配置。
src // 包含源代码。
compiler // 包含用于模板到渲染功能的编译器的代码。
parser // 包含将模板字符串转换成元素抽象语法树的代码。
codegen // 包含从抽象语法树(AST)生成 render 函数的代码。
optimizer.js // 分析静态树,优化 vdom 渲染。
core // 包含与平台无关的通用运行时代码。
observer // 包含数据观测的核心代码。
vdom // 包含虚拟 DOM 创建(creation)和打补丁(pathing)的代码。
instance // 包含 Vue 构造函数设计相关的代码。
global-api // 包含给 Vue 构造函数挂在全局方法(静态方法)或属性的代码。
components // 包含抽象出来的通用组件。
platforms // 包含特定于平台的代码。
server // 包含与服务器端渲染有关的代码。
sfc // 包含单个文件组件(*.vue文件)的解析逻辑。在vue-template-compiler包装中使用。
shared // 包含在整个代码库中共享的实用程序。
test // 包含所有测试。
types // 包含TypeScript类型定义
test // 包含类型定义测试
前奏
执行 npm install
安装依赖,然后执行 npm run dev
生成 dist/vue.js
,以备后期用来引用调试源码。
第一个问题:dist/vue.js 是怎么来的
我们执行了 npm run dev
命令,这个 dev
就是我们在 package.json
中定义好的。打开 package.json
看 scripts
中的定义项可以找到 dev
,虽然这里边的配置项有很多,但是我们现在只关心 dev
:
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
- rollup: Rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码。
- -w: 开启 watch 模式,也就是热更新
- -c: 指定配置文件,配置文件就是跟在后边的
scripts/config.js
- –environment: 环境变量,没有找到具体的解释,猜测应该是指向后边的 TARGET
- TARGET:web-full-dev: 指向的就是
scripts/config.js
中的具体配置
搁我的第一反应就是,因为这个命令里边只出现了一个文件路径 scripts/config.js
,那么我就从这个配置文件开始入手。
找到配置文件看一下:
// 引入各种依赖,定义 banner
...
// 我们需要关心的是下边 builds 对象
const builds = {
// Runtime+compiler development build (Browser)
'web-full-dev': {
// web/entry-runtime-with-compiler.js 在 src\platforms 下
// 入口文件
entry: resolve('web/entry-runtime-with-compiler.js'),
// 最终输出
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
}
}
// 生成配置的方法
function genConfig(name) {
...
// 返回的对象就是 Rollup 的配置对象
return config
}
if (process.env.TARGET) {
// 根据 package.json script TARGET 的值(TARGET:web-full-dev) 去生成对应的配置方法
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
这里配置文件中的 dest
就是指定输出 dist/vue.js
。
总结一下 dist/vue.js 是怎么来的
- 当执行
nmp run dev
时会先去package.json
中找到script
中builds
定义的dev
- 根据
dev
中的配置会找到scripts/config.js
并且将TARGET
赋值为web-full-dev
- 进入到
scripts/config.js
中并执行module.exports = genConfig(process.env.TARGET)
genConfig(process.env.TARGET)
就等于genConfig('web-full-dev')
- 通过
genConfig()
方法返回的就是Rollup
的配置对象Rollup
的配置对象中就定义了npm run dev
的最终输出目录及文件
每次用 Vue 的时候都需要 new 一下,那么这个构造函数在哪里呢。
刚才在 scripts/config.js
中 builds
对象里有一个 web-full-dev
选项,里边定义的 entry
指向了 web/entry-runtime-with-compiler.js
文件(具体路径: src\platforms\web\entry-runtime-with-compiler.js
),我们可以从这里入手开始找。
打开 web/entry-runtime-with-compiler.js
文件我们可以看到:
...
import Vue from './runtime/index'
...
我们要找的是 Vue 这个构造函数,但是这里是引入进来的,所以继续进入 ./runtime/index
文件。
进入 ./runtime/index
这个文件又看到了一个引入:
import Vue from 'core/index'
...
根据这个思路,我们找啊找找啊找,最终可以在 src\core\instance\index.js
文件中找到:
...
function Vue(options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
...
那么找 Vue 构造函数的过程是这样的
- 进入
package.json
文件,找到scripts
对象中的dev
- 在
dev
中可以看到一个rollup
配置文件的路径scripts\config.js
- 根据
package.json
中dev
中定义的web-full-dev
,可以在scripts\config.js
文件中的builds
对象中找到web-full-dev
,也就可以找到入口文件src\platforms\web\entry-runtime-with-compiler.js
- 进入到
src\platforms\web\entry-runtime-with-compiler.js
文件后看到import Vue from './runtime/index'
就再去./runtime/index
文件中去找- 进入
src\platforms\web\runtime\index.js
文件看到还存在引用import Vue from 'core/index'
再继续去core/index
文件中找src\core\index.js
文件中还有引用./instance/index
- 最终在
src\core\instance\index.js
文件中找到Vue
的构造函数
找到了 Vue 构造函数,接下来看看这个构造函数是什么样的
想了解这个构造函数是什么样的就要由内而外的看,也就是 逆着 刚才找构造函数的步骤来看。这样就能知道每一个文件对最一开始的 Vue
构造函数做了什么。
第一步先来看一下 src\core\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'
// 定义构造函数
function Vue(options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// 原型上挂载了init方法,用来做初始化
initMixin(Vue)
/**
* 原型上挂载$data的属性描述符getter,返回this._data
* 原型上挂载$props的属性描述符getter, 返回this._props
* 原型上挂载$set与$delete方法,用来为对象新增/删除响应式属性
* 原型上挂载$watch方法
*/
stateMixin(Vue)
// 原型上挂载事件相关的方法, $on、$once、$off、$emit。
eventsMixin(Vue)
// 原型上挂载_update、$destroy与$forceUpdate方法,与组件更新有关。
lifecycleMixin(Vue)
// 原型挂载组件渲染相关方法,_render方法(用来返回vnode,即虚拟dom)
renderMixin(Vue)
// 导出 Vue
export default Vue
这个文件中除了定义了构造函数,还执行了五个方法,其中的对应关系如下:
initMixin(Vue) => src\core\instance\init.js
// 执行结果
Vue.prototype._init = function (options){}
stateMixin(Vue) => src\core\instance\state.js
// 执行结果
Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$set
Vue.prototype.$delete
Vue.prototype.$watch
eventsMixin(Vue) => src\core\instance\events.js
// 执行结果
Vue.prototype.$on
Vue.prototype.$once
Vue.prototype.$off
Vue.prototype.$emit
lifecycleMixin(Vue) => src\core\instance\lifecycle.js
// 执行结果
Vue.prototype._update
Vue.prototype.$destroy
Vue.prototype.$forceUpdate
renderMixin(Vue) => src\core\instance\render.js
// 执行结果
Vue.prototype._render
Vue.prototype.$nextTick
Vue.prototype._o
Vue.prototype._n
Vue.prototype._s
Vue.prototype._l
Vue.prototype._t
Vue.prototype._q
Vue.prototype._i
Vue.prototype._m
Vue.prototype._f
Vue.prototype._k
Vue.prototype._b
Vue.prototype._v
Vue.prototype._e
Vue.prototype._u
Vue.prototype._g
Vue.prototype._d
Vue.prototype._p
src\core\instance\index.js
文件看完之后继续进击下一个文件 src\core\index.js
。记住我们是逆着找构造函数的顺序来看的!
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
// 是否是服务端渲染
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
// ssr 相关
Object.defineProperty(Vue.prototype, '$ssrContext', {
get() {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
文件开头引入了一些东西:引入的 Vue
是已经在 src\core\instance\index.js
文件中对 Vue
原型上增加了各种方法和属性的 Vue
;引入 src\core\global-api\index.js
文件中的 initGlobalAPI
方法;引入 core/util/env
文件下的 isServerRendering
和 core/vdom/create-functional-component
文件下的 FunctionalRenderContext
,这两个都是和 SSR
相关的。
引入的 initGlobalAPI
方法的作用是在 Vue
构造函数上挂在静态属性和方法。
在 initGlobalAPI
方法中 着重 需要看一下处理 Vue.options
的地方,因为后边还会对这个对象有操作:
...
Vue.options = Object.create(null)
// ['component', 'directive', 'filter'] => src\shared\constants.js
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
// 将 builtInComponents => KeepAlive 中的属性混合到 Vue.options.components
extend(Vue.options.components, builtInComponents)
...
执行完这个文件之后,Vue
会变成这个样子:
// initGlobalAPI() 方法处理结果
Vue.config
Vue.set
Vue.delete
Vue.nextTick
Vue.observable
Vue.options = {
conponents: {
KeepAlive
}
directives: {}
filters: {}
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
// 当前文件处理中处理
Vue.prototype.$isServer
Vue.prototype.$ssrContext
Vue.FunctionalRenderContext
Vue.version = '__VERSION__'
src\core\index.js
文件就看完了,下一个是 src\platforms\web\runtime\index.js
文件
文件实现了在 Vue.config
中安装特定于平台的实用工具:
// install platform specific utils // platform: 平台; specific: 具体的
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
安装平台运行时指令和组件
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
- 上边用到的
extend
方法的作用就是将参数2
混入到参数1
中去platformDirectives
包含了v-model
和v-show
两个指令platformComponents
包含了Transition
和TransitionGroup
两个组件
安装平台补丁功能
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
公共的装载方法
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 首先根据是否是浏览器环境决定要不要 query(el) 获取元素
el = el && inBrowser ? query(el) : undefined
// 然后将 el 作为参数传递给 mountComponent()。
return mountComponent(this, el, hydrating)
}
Vue.prototype.$mount
方法先判断了根据是不是浏览器环境,然后决定要不要获取元素,然后将el
作为参数传给mountComponent
方法。
最终 Vue
就变成了这个样子:
Vue.config = {
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
}
Vue.set
Vue.delete
Vue.nextTick
Vue.observable
// 安装平台运行时指令和组件 这里要注意 options 里边的变化,和之前的是不一样的。
Vue.options = {
conponents: {
KeepAlive,
Transition,
TransitionGroup
}
directives: {
model,
show
}
filters: {}
_base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
Vue.prototype.$isServer
Vue.prototype.$ssrContext
Vue.FunctionalRenderContext
Vue.version
Vue.prototype.__patch__
Vue.prototype.$mount
又一个文件看完了。最终处理的是入口文件 src\platforms\web\entry-runtime-with-compiler.js
,这个文件主要做了 两件事:
重新定义并继承了
Vue.prototype.$mount
方法// 重新定义 Vue.prototype.$mount 并继承 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { ... return mount.call(this, el, hydrating) }
Vue
上挂载compile
Vue.compile = compileToFunctions
这里的
compileToFunctions
就是将template
编译为render
函数
总结一下 Vue
构造函数
Vue.prototype
下的属性和方法的挂载主要是在src\core\instance
目录中的代码处理的Vue
的静态属性和方法的挂载主要是在src\core\global-api
目录中的代码处理的src\platforms\web\runtime\index.js
文件主要是添加 web 平台特有的配置、组件和指令,定义$mount
方法src\platforms\web\entry-runtime-with-compiler.js
重新定义并继承$mount
方法,添加compiler
编译器,支持template
编译为render
函数
Vue
初始化做了什么
我们在 example
文件夹中创建一个我们自己的文件并引入 dist/vue.js
文件,这个样子:
<body>
<div id="app">
{{message}}
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
message: '源码学习'
}
})
</script>
</body>
这里代码第一步就是 new Vue()
,所以我们需要在 dist/vue.js
中找到 Vue
这个构造函数并在里边打一个断点:
function Vue(options) {
debugger
if (
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
在构造函数中第一个执行的是 this._init()
方法,根据我们之前的整理可以知道 _init()
方法是在 src\core\instance\init.js
是在这个文件夹里,也就是在 initMinxin()
方法中定义的:
export function initMixin(Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
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)
}
// a flag to avoid this being observed
// 一个避免被观察到的标志
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// 优化内部组件实例化
// since dynamic options merging is pretty slow, and none of the
// 因为动态选项合并非常慢,而且
// internal component options needs special treatment.
// 内部组件选项需要特殊处理。
// 在使用 Vue 开发项目的时候,我们是不会使用 _isComponent 选项的,这个选项是 Vue 内部使用的
initInternalComponent(vm, options)
} else {
// 使用策略对象合并参数选项(合并参数中有多个策略)
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // 返回 Vue.options
options || {}, // 调用 Vue 构造函数时的参数选项
vm // this 对象(上边代码有定义: const vm: Component = this)
)
}
// 初始化工作与 Vue 实例对象的设计
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') // 初始化之前
// 初始化 data/props 之前
initInjections(vm) // resolve injections before data/props
initState(vm)
// 初始化 data/props 之后
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 初始化完成之后
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果调用 Vue 构造函数时的参数选项中有 el, 则会调用 $mount()
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
这个文件中首先定义了 _uid
和 _isVue
两个属性,然后判断 options
中有没有 _isComponent
,根据 Vue2.1.7源码学习 作者说的: _isComponent
在我们使用 Vue
开发项目的时候是不会用到这个属性的,这个是 Vue
内部使用的。所以这里会走 else
分支:
// 使用策略对象合并参数(合并参数中有多个策略)
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor), // 返回 Vue.options
options || {}, // 调用 Vue 构造函数时的参数选项
vm // this 对象(上边代码有定义: const vm: Component = this)
)
这里使用策略对象合并选项参数:
可以看到 vm.$options
接收了 mergeOptions
方法的返回值。mergeOptions
方法接收了三个参数,分别是 resolveConstructorOptions(vm.constructor)
方法返回值、options || {}
和 vm
。
先来看一下 resolveConstructorOptions(vm.constructor)
方法,这个方法和 _init
方法在同一个文件夹中:
// 传入的就是 Vue 本身
export function resolveConstructorOptions(Ctor: Class<Component>) {
let options = Ctor.options
// 判断继承
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
根据传入的参数 vm.constructor
可以知道传入的就是 Vue
构造函数本身。这里的 Ctor.options
也就相当于是 Vue.options
,而 Vue.options
在 src\platforms\web\runtime\index.js
文件中我们做过最后一次处理,处理完之后是这样的:
Vue.options = {
conponents: {
KeepAlive,
Transition,
TransitionGroup
}
directives: {
model,
show
}
filters: {}
_base: Vue
}
之后判断了 Ctor.super
是否存在,这里是用来判断继承的,暂不关心。在这个例子中是直接将 Vue.options
返回了,所以传给 mergeOptions
方法的第一个参数就是 Vue.options
。
第二个参数就是调用 Vue
构造函数时的参数选项,在本例中就是:
{
el: '#app',
data: {
message: '源码学习'
}
}
第三个参数就是指向 Vue
实例的 this
对象。
知道了这三个参数是什么,再来看一下 src\core\util\options.js
文件下 mergeOptions
方法:
...
/**
* Option overwriting strategies are functions that handle
* how to merge a parent option value and a child option
* value into the final value.
* 选项覆盖策略是处理如何将父选项值和子选项值合并为最终值的函数。
*/
const strats = config.optionMergeStrategies // Object.create(null)
...
// 在 starts 定义属性
...
/**
* Merge two option objects into a new one.
* 将两个选项对象合并为一个新对象。
* Core utility used in both instantiation and inheritance.
* 用于实例化和继承的核心实用程序。
*/
export function mergeOptions(
parent: Object,
child: Object,
vm?: Component
): Object {
...
function mergeField(key) {
// strats 中存在的都是有专属的合并规则的
// 不存在的就使用默认规则
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
这个文件的作用就是将传入的参数遍历,并使用预先定义好的合并策略来处理每个值并返回。
文件中首先通过 Object.create(null)
定义了一个空对象 starts
,其次在 starts
定义了一堆属性,每个属性都分别对应着不同的合并策略。在 mergeOptions
方法中的 mergeField
方法中有一个判断:如果传入的 key
在 starts
对象中存在,那么返回的就是预先定义好的专属合并规则,反之则使用默认的合并规则。其中 el
选项会使用默认参数合并策略,data
会使用 starts
中定义的 data
指向的合并策略,starts.data
最终会返回一个函数: mergedInstanceDataFn
。随后执行合并规则并将返回值赋给 options
。mergeOptions
方法的返回值就是合并规则处理完之后的 options
。前边说过,mergeOptions
方法的返回值会赋值给 vm.$options
。
合并完参数之后开始进行初始化工作:
首先会在 vm
上声明两个属性,这里的 vm
就是 Vue
的实例对象。这两个属性分别是 _renderProxy
和 _self
,其中 _self = vm
;_renderProxy
的值则会根据是不是生产环境而改变,如果是生成环境的话它的值和 _self
的值一样都是 vm
,如果不是生产环境则会通过 initProxy
方法判断 如果 Proxy
存在的话,则会通过 Proxy
进行 vm
的代理再返回,如果 Proxy
不存在的话还是直接将 vm
赋值给 _renderProxy
。
至此,Vue
实例对象上的属性有这些:
this._uid = uid++
this._isVue = true
this.$options = {
components,
directives,
filters,
_base,
el,
data: mergedInstanceDataFn()
}
this._renderProxy = this
this._self = this
接下来就是执行了六个初始化方法,分别是 initLifecycle
、initEvents
、initRender
、initInjections
、initState
、initProvide
。其中执行 initInjections
方法之前会执行一个 beforeCreate
生命周期钩子,在 initProvide
方法执行之后会执行 created
声明周期钩子。这里也说明了为什么在 created
钩子中不能操作 DOM
,因为 created
只是数据初始化完成,并没有挂载到 DOM
上。
initLifecycle
方法对 Vue
实例对象的处理:
this.$parent = parent
this.$root = parent ? parent.$root : vm
this.$children = []
this.$refs = {}
this._watcher = null
this._inactive = null
this._directInactive = false
this._isMounted = false
this._isDestroyed = false
this._isBeingDestroyed = false
initEvents
方法对 Vue
实例对象的处理:
this._events = Object.create(null)
this._hasHookEvent = false
在 initEvents
方法中,如果 vm.$options._parentListeners
存在则还需要执行 updateComponentListeners
方法。
initRender
方法对 Vue
实例对象的处理:
this._vnode = null
this._staticTrees = null
this.$vnode
this.$slots
this.$scopedSlots
this._c
this.$createElement
initInjections
方法主要是初始化 vue
实例的 inject
initState
方法对 Vue
实例对象的处理:
vm._watchers = []
initProps()
vm._props = {}
vm.$options._propKeys = []
initMethods()
initData()
vm._data
initComputed()
vm._computedWatchers = Object.create(null)
initWatch()
vm.$watch()
在 initState
方法中还执行了 initProps
、initMethods
、initData
、initComputed
、initWatch
方法,只不过执行这些的前提是需要在调用 Vue
构造函数时的参数选项中提前定义。
initProvide
方法主要是将 vm.$options
里的 provide
赋值到当前实例上
在 init
方法的最后判断了调用 Vue
构造函数时的参数选项中是否有 el
,如果有则会执行 vm.$mount()
方法,这里也恰恰说明了为什么不传递 el
选项时需要手动执行 mount
的原因。
总结 Vue 初始化做了什么
Vue 初始化时执行了 init 方法,这个方法执行了一些初始化方法顺序如下:
initProxy(vm)
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProps()
initMethods()
initData()
initComputed()
initWatch()
initProvide(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
首先是根据当前环境决定是否执行
initProxy
方法;initLifecycle
方法是在Vue
实例上添加一些属性;initEvents
方法根据vm.$options._parentListeners
是否存在来决定是否执行updateComponentListeners
方法,否则也是在Vue
实例上添加一些属性;initRender
方法也是在Vue
是俩上添加一些属性;initInjections
方法是用来初始化vue
实例的inject
;initState
方法用来初始化props
、methods
、data
、computed
、watch
;initProvide
方法是将vm.$options
里的provide
赋值到当前实例上;最后如果el
选项存在的话会执行vm.$mount(vm.$options.el)
。