概述
我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家。
这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容:
- 寻找入口文件
- 在打包的过程中 Vue 发生了什么变化
- 在 Vue 实例化的时候,它的内部到底做了什么
寻找入口文件
首先我们寻找入口文件,我们查看package.json
文件去找它的打包指令:
"scripts": {
// ...
"build": "node scripts/build.js",
// ...
}
可以看到,打包的执行文件是scripts/build.js
,于是我们再去看这个文件做了什么:
// ...
let builds = require('./config').getAllBuilds()
// ...
build(builds)
function build (builds) {
let built = 0
const total = builds.length
const next = () => {
buildEntry(builds[built]).then(() => {
built++
if (built < total) {
next()
}
}).catch(logError)
}
next()
}
// ...
我们看到,它是通过获取获取./config.js
里面的配置来进行打包的,于是我们再去看./config.js
里面有什么配置:
// ...
const builds = {
// ...
// Runtime only ES modules build (for bundlers)
'web-runtime-esm': {
entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.runtime.esm.js'),
format: 'es',
banner
},
// ...
}
可以看到,上面就是rollup打包的典型配置,有很多像这样的配置,我只列出了其中一个。那么我们在项目中使用的 Vue 版本到底是哪个配置生成的呢?我们通过查阅 Vue 官网和 vue-cli3 官方文档来弄清这个问题。
1.通过查阅Vue 官网可以看到,我们在项目中一般使用的是vue.runtime.esm.js
这个版本。官网原文如下:
为这些打包工具提供的默认文件 (pkg.module) 是只有运行时的 ES Module 构建 (vue.runtime.esm.js)。
2.我们再来看 vue-cli3 源码:
// ...
webpackConfig.resolve
.alias
.set(
'vue$',
options.runtimeCompiler
? 'vue/dist/vue.esm.js'
: 'vue/dist/vue.runtime.esm.js'
)
/// ...
可以看到,它通过判断options.runtimeCompiler
的值,来设置 vue 的别名。vue-cli3官方文档里面这个值的默认值为false
,所以 vue 的别名设置为vue.runtime.esm.js
。(因此我们在 vue-cli3 的项目里面执行语句import Vue from 'vue';
时,我们其实在执行import Vue from 'vue/dist/vue.runtime.esm.js';
)
然后通过查看上面config.js
里面的配置,我们可以看到,生成vue.runtime.esm.js
的配置里面入口文件是web/entry-runtime.js
。
到这里还没有结束,查看vue 的官方 repo,里面是没有 web 文件夹的,所以这个路径web/entry-runtime.js
里面的 web 应该也是一个别名。于是继续查找,我们发现,scripts 文件夹里面还有一个文件alias.js
,里面的内容如下:
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
很明显,web 被设置为src/platforms/web
的别名。所以,最后 vue 的入口文件是src/platforms/web/entry-runtime.js
。
我们总结一下:
- vue 是通过 rollup 进行打包的,打包的配置在一个单独的
config.js
文件里面 - 通过查阅 Vue 官网和 vue-cli3 官方文档,我们发现,项目中引用的 vue ,其实引用的是
vue.runtime.esm.js
文件 - 最后通过查找配置,我们发现 vue 的入口文件是
src/platforms/web/entry-runtime.js
在打包的过程中 Vue 发生了什么变化
我们查看这个入口文件src/platforms/web/entry-runtime.js
,它总共就只有这么一段代码:
/* @flow */
import Vue from './runtime/index'
export default Vue
我们再打开./runtime/index
文件,发现它的第一行是:
import Vue from 'core/index'
在上面alias.js
文件中我们看到了,core 的别名是src/core
,所以我们继续看src/core/index.js
文件:
import Vue from './instance/index'
继续看./instance/index
:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
可以看到,它是通过函数式的方式来定义Vue
这个类的,然后检查代码执行环境,如果是非开发环境就会报错;如果新建一个它的实例,就会执行_init(options)
进行初始化工作。
我们先不管 Vue 的实例,而只关注Vue.prototype
和 Vue
,来看看在打包的一步步过程中,挂载在它们上面的属性发生了什么变化。
通过console.log
边打包边输出,我们可以看到,在./instance/index
文件里面,initMixin
方法为Vue.prototype
增加了_init
方法:
{
_init: ƒ (options)
}
stateMixin
方法初始化了各种state
:
{
$data: {get: ƒ, set: ƒ}
$props: {get: ƒ, set: ƒ}
$set: ƒ (target, key, val)
$delete: ƒ del(target, key)
$watch: ƒ ( expOrFn, cb, options )
}
eventsMixin
方法添加了各种事件相关的方法:
{
$on: ƒ (event, fn)
$once: ƒ (event, fn)
$off: ƒ (event, fn)
$emit: ƒ (event)
}
lifecycleMixin
方法增加了_update、$forceUpdate 和 $destroy
:
{
_update: ƒ (vnode, hydrating)
$forceUpdate: ƒ ()
$destroy: ƒ ()
}
renderMixin
方法则把render
时需要调用的方法都加进去了:
{
_n: ƒ toNumber(val)
_s: ƒ toString(val)
_l: ƒ renderList( val, render )
_t: ƒ renderSlot( name, fallback, props, bindObject )
_q: ƒ looseEqual(a, b)
_i: ƒ looseIndexOf(arr, val)
_m: ƒ renderStatic( index, isInFor )
_f: ƒ resolveFilter(id)
_k: ƒ checkKeyCodes( eventKeyCode, key, builtInKeyCode, eventKeyName, builtInKeyName )
_b: ƒ bindObjectProps( data, tag, value, asProp, isSync )
_v: ƒ createTextVNode(val)
_e: ƒ (text)
_u: ƒ resolveScopedSlots( fns, hasDynamicKeys, contentHashKey )
_g: ƒ bindObjectListeners(data, value)
_d: ƒ bindDynamicKeys(baseObj, values)
_p: ƒ prependModifier(value, symbol)
$nextTick: ƒ (fn)
_render: ƒ ()
}
但是到这里还没有结束,上面这些只是在 src/core/instance/index.js
文件里面做的处理。我们现在来看看引用这个文件的 src/core/index.js
文件:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
这个文件里面,首先通过initGlobalAPI
方法给Vue
添加了这些属性:
// 在 Vue 上面挂载
{
config: {get: ƒ, set: ƒ}
util: {value: {…}}
set: {value: ƒ}
delete: {value: ƒ}
nextTick: {value: ƒ}
observable: {value: ƒ}
options: {value: {components, directives, filters, _base}}
use: {value: ƒ}
mixin: {value: ƒ}
cid: {value: 0}
extend: {value: ƒ}
component: {value: ƒ}
directive: {value: ƒ}
filter: {value: ƒ}
}
之后直接在Vue.prototype
上面添加了$isServer
和$ssrContext
属性,在Vue
上面添加了FunctionalRenderContext
方法和版本号。
最后我们看看引用 src/core/index.js
文件的 web/runtime/index.js
文件:
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
可以看到,它在Vue
和Vue.prototype
上面添加了平台特有的utils、directives、components、patch 和 $mount
方法。
到这里就全部结束了,我们整理这些属性和方法的目的是:
- 在接下来读源码的过程中,我们对 Vue 和 Vue.prototype 上的属性和方法更有信心。
- 如果我们看到源码在调用某个不知道的属性或方法的时候,可以从这里来查找来源。
- 我们能够看到 Vue 源码的代码结构是怎么组织的。
我们再总结一下:
- Vue.prototype 上的属性和方法主要是在
src/core/instance/index.js
里面挂载的。 - Vue 上的静态属性和方法主要是在
src/core/index.js
里面的initGlobalAPI
方法里面挂载的。这个文件里面还处理了ssr(服务端渲染)
相关的东西和加上了版本号。 web/runtime/index.js
文件则添加了 web 平台特有的属性和方法。
在 Vue 实例化的时候,它的内部到底做了什么
下面是我们在大多数项目里面写的Vue
实例化代码:
new Vue({
router,
store,
i18n,
render: h => h(App),
}).$mount('#app');
首先,我们来看前半段new Vue({router, store, i18n, render: h => h(App)})
做了什么。
我们知道,Vue
在实例化的时候,会继承Vue.prototype
的属性和方法,所以通过汇总我们刚才总结的属性和方法,我们可以知道,这个实例拥有如下属性和方法:
{
_init: ƒ (options)
$data: {get: ƒ, set: ƒ}
$props: {get: ƒ, set: ƒ}
$set: ƒ (target, key, val)
$delete: ƒ del(target, key)
$watch: ƒ ( expOrFn, cb, options )
$on: ƒ (event, fn)
$once: ƒ (event, fn)
$off: ƒ (event, fn)
$emit: ƒ (event)
_update: ƒ (vnode, hydrating)
$forceUpdate: ƒ ()
$destroy: ƒ ()_o: ƒ markOnce( tree, index, key )
_n: ƒ toNumber(val)
_s: ƒ toString(val)
_l: ƒ renderList( val, render )
_t: ƒ renderSlot( name, fallback, props, bindObject )
_q: ƒ looseEqual(a, b)
_i: ƒ looseIndexOf(arr, val)
_m: ƒ renderStatic( index, isInFor )
_f: ƒ resolveFilter(id)
_k: ƒ checkKeyCodes( eventKeyCode, key, builtInKeyCode, eventKeyName, builtInKeyName )
_b: ƒ bindObjectProps( data, tag, value, asProp, isSync )
_v: ƒ createTextVNode(val)
_e: ƒ (text)
_u: ƒ resolveScopedSlots( fns, hasDynamicKeys, contentHashKey )
_g: ƒ bindObjectListeners(data, value)
_d: ƒ bindDynamicKeys(baseObj, values)
_p: ƒ prependModifier(value, symbol)
$nextTick: ƒ (fn)
_render: ƒ ()
__patch__: ƒ patch(oldVnode, vnode, hydrating, removeOnly)
$mount: ƒ ( el, hydrating )
}
然后我们回到前面的实例化代码:
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
结合前面实例化传的参数,它相当于执行了如下代码:
this._init({
router,
store,
i18n,
render: h => h(App),
});
我们继续找_init
方法的定义,他是在 core/instance/init.js
里面定义的,简化代码如下:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
代码里面,首先给实例2个标记_uid
和_isVue
,其中每个实例的_uid
各不相同。
合并 options
然后进行合并 options,由于这一步比较复杂,所以我们独立为一个小节。
由于我们并没有传 _isComponent
这个变量,所以执行下面这段代码:
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
我们来看看 resolveConstructorOptions
的定义:
export function resolveConstructorOptions (Ctor: Class) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
可以发现,Ctor
的值是传入的vm.constructor
,即 vm 的构造函数,而 vm 的构造函数就是 Vue
,所以Ctor
其实就是Vue
。所以options
就是Vue
上的静态属性Vue.options
。然后由于目前Vue
没有继承,所以Vue.super
是undefined
,所以这里resolveConstructorOptions(vm.constructor)
其实就返回Vue.options
。
再看之前给Vue
挂载options
的时候,它是由initGlobalAPI
挂载的全局方法,结构是这样的:
options: {value: {components, directives, filters, _base}}
所以简化一下,上面合并 options 的那段代码可以改写为:
vm.$options = mergeOptions(
{
components,
directives,
filters,
_base
},
{
router,
store,
i18n,
render: h => h(App)
},
vm
)
我们来看一下mergeOptions
的定义代码:
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// ...
// Apply extends and mixins on the child options,
// but only if it is a raw options object that isn't
// the result of another mergeOptions call.
// Only merged options has the _base property.
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
可以看到,一共分为2步来进行合并:
1.把child.extends
和child.mixins
合并到parent
里面去。
2.对于其它的属性 key,通过 const strat = strats[key] || defaultStrat
来根据 key 获取定制的合并函数,然后用这个合并函数来合并这个属性。
其中,router、store、i18n 和 render 这几个字段都没有定制的合并函数,所以使用默认策略defaultStrat
进行合并,即直接赋值的形式。
/**
* Default strategy.
*/
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
所以上面合并 options 的代码其实就是:
vm.$options = {
components,
directives,
filters,
_base,
router,
store,
i18n,
render: h => h(App),
}
这样合并 options 就结束了。
设置代理
接下来的代码如下:
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
这段代码判断是否是生产环境,然后在非生产环境的时候,调用initProxy
方法。我们从字面意思可以理解为:给 vm 设置代理,其中 vm 就是实例本身。我们再来看initProxy
方法的源码:
initProxy = function initProxy (vm) {
if (hasProxy) {
// determine which proxy handler to use
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
可以看到,通过vm._renderProxy = new Proxy(vm, handlers)
这行代码,我们给 vm 设置了一些代理,当我们调用vm._renderProxy
的时候,就会执行这些代理函数。我们接下来看看有哪些代理函数,下面是其中一个:
const warnNonPresent = (target, key) => {
warn(
`Property or method "${key}" is not defined on the instance but ` +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
)
}
const getHandler = {
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
}
}
可以看到,我们在调用vm._renderProxy.xxx
的时候,会检查这个 xxx 是否在target.$data
也就是vm.$data
里面,如果不是就会弹出 warning。
初始化生命周期
接下来是这段代码:
initLifecycle(vm)
我们查找initLifecycle
方法的源码:
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
我们知道,vm.$options
就是我们刚才合并的 options,它包含Vue
的静态属性options
,和我们提供的各种参数。所以,这段代码里面添加了父子实例属性,以及各种生命周期属性。
值得一提的是,abstract
是什么属性?官网并没有找到这个参数,我们在项目中也没有传过这个参数。但是通过搜索源码,我发现,keep-alive
组件和transition
组件的abstract
属性被设置为 true。所以在处理父子实例那里,会忽略所有的keep-alive
组件和transition
组件。因此,abstract
属性是抽象的意思,有abstract
属性的组件在处理时没有被当成真正的 Vue 实例。
初始化事件
接下来是这段代码:
initEvents(vm)
我们查找initEvents
方法的源码:
import { updateListeners } from '../vdom/helpers/index'
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
可以看到,由于没有传oldListeners
参数,所以最后是通过这段代码updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
给 vm 添加各种事件的。由于我们在初始化代码里面并没有传事件,所以这里没有添加任何事件。
初始化 Render 相关属性和方法
接下来是这段代码:
initRender(vm)
我们查找initRender
方法的源码:
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
}
可以看到,这段代码先在 vm 上绑定了一些属性,然后处理有slots
的情况,再然后在 vm 上绑定_c
和$createElement
,它们都是createElement
的语法糖,其中_c
是内部用的,而$createElement
是给用户在render
函数里面用的。最后就是在 vm 上定义了 2 个响应式属性(关于响应式我们以后再说):$attrs
和$listeners
。
调用 beforeCreate 钩子
接下来是这段代码很好理解,就是调用beforeCreate
钩子
callHook(vm, 'beforeCreate')
初始化 injections
接下来是这段代码:
initInjections(vm)
我们查找initInjections
方法的简化源码:
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
}
}
这段代码很好理解,它首先用resolveInject
收集了所有的injections
,然后把它们响应式地挂载到了 vm 上面。如果是在非生产环境,给它们赋值还会产生 warning。接下来我们来看下它是怎么收集所有的injections
的,resolveInject
的简化源码如下:
export function resolveInject (inject: any, vm: Component): ?Object {
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
const result = Object.create(null)
const keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
// #6574 in case the inject object is observed...
if (key === '__ob__') continue
const provideKey = inject[key].from
let source = vm
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey]
break
}
source = source.$parent
}
if (!source) {
if ('default' in inject[key]) {
const provideDefault = inject[key].default
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault
} else if (process.env.NODE_ENV !== 'production') {
warn(`Injection "${key}" not found`, vm)
}
}
}
return result
}
}
可以看到,如果是from
的注入方式,则使用 while 循环一级级找父节点,然后从父节点的_provided
里面得到这个inject
属性的值;如果是default
的注入方式,则直接赋值即可。
初始化 state
接下来是这段代码:
initState(vm)
我们查找initState
方法的简化源码:
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
可以看到,initState
方法按顺序初始化了props、methods、data、computed、watch
。我们一个个看是怎么初始化的。
首先是initProps
的简化源码:
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
}
toggleObserving(true)
}
可以看到,源码里面propsOptions
其实就是我们传的opts.props
,即vm.$options.props
,也就是我们传的options
参数里面的props
。然后源码对里面的每个值进行一系列判断和添加报错信息,再通过validateProp
方法在propsData
即vm.$options.propsData
里面找到相应的 value,最后把这个 value 通过defineReactive
方法响应式的绑定在了vm._props
上面。
再来看一看initMethods
的简化源码:
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
可以看到,它对opts.methods
即vm.$options.methods
里面的每个方法,还是先做一系列判断和报错,最后使用bind
方法把它们绑定到vm 上面去。
再来看一看initData
的简化源码:
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
可以看到,这段代码和前面的类似,都是做了一些判断和报错信息,然后用proxy
绑定到了 vm 的_data
属性上面去,最后用observe
定义了响应式。(observe我们以后再讲,我们现在先理清大致的轮廓线)
其实后面的initComputed
和initWatch
都差不多,都是先做一些判断和报错信息,然后同步到 vm 上线去,这里就省略了。
初始化 provide
接下来是这段代码:
initProvide(vm)
我们查找initProvide
方法的简化源码:
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
可以看到,只是简单地把vm.$options.provide
赋值给vm._provided
而已。
调用 created 钩子 和 判断是否挂载
接下来是这段代码:
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
可以看到,先调用了created
钩子,然后判断options
里面是否有el
属性,如果有就使用$mount
进行挂载($mount
方法的挂载过程我们等会儿会讲到,现在先略过)。
需要注意的是,这段代码说明,如果我们不按照下面的方式初始化:
new Vue({
router,
store,
i18n,
render: h => h(App),
}).$mount('#app');
而是使用这个方式初始化也是可以的:
new Vue({
el: '#app',
router,
store,
i18n,
render: h => h(App)
});
到这里我们还没有结束,我们最后来看我们在项目中使用的初始化代码的后半段:
new Vue({
router,
store,
i18n,
render: h => h(App),
}).$mount('#app');
上面的代码通过$mount
方法把Vue
实例挂载到了 id 为 app 的节点上面,我们来看$mount
是怎么挂载的。那么$mount
方法是在哪里定义的呢?通过我们之前的那些属性和方法的记录,我们找到:$mount
是在 web/runtime/index.js
文件里面定义平台属性和方法的时候定义的:
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
可以看到,它其实调用的是mountComponent
方法,我们来看mountComponent
的源码,它是在core/instance/lifecycle
里面定义的,简化代码如下:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount')
let = updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
可以看到,它会先判断 vm 有没有render
方法,如果没有就检查有没有template
属性,如果有就报错:我们应该使用compiler-included
那个版本,如果没有template
属性就报错:挂载失败。
检查完毕之后调用了beforeMount
钩子。
再然后是调用 vm 的_render()
方法获取渲染后的结果,把结果写入更新函数里面去,再使用watcher
使更新函数变成响应式的。(这里watcher
的具体实现我们放到以后来讲)
最后调用了mounted
钩子,挂载完毕。
以上就是从新建Vue
实例到挂载完毕的全过程。
我们来总结一下:
- Vue 源码先使用
_init
方法进行初始化,然后使用各平台定义的$mount
方法进行挂载的。 - 在初始化的过程中,Vue 源码先以一定的规则合并了我们传入的
options
和父组件的options
,然后初始化了proxy,lifecycle,state
等,同时也在不同的时间段调用了生命周期钩子。 - 在挂载的过程中,Vue 源码通过获取
render
方法的执行结果,把它加入到更新函数里面去,再使用watcher
使更新函数变成响应式的,从而在挂载的过程中、在父组件更新的过程中、在传入的数据发生变化的过程中都能进行自动更新。
后记
上面我们关于响应式的代码讲解都省略了,我打算放到下一期来一起讲,主要包括:defineReactive,observe,watcher
。我们把响应式讲清楚之后,这里省略的部分就一目了然了,敬请期待!!!