Vue 源码学习(1)

时间:2023-03-09 04:04:07
Vue 源码学习(1)

概述

我在闲暇时间学习了一下 Vue 的源码,有一些心得,现在把它们分享给大家。

这个分享只是 Vue源码系列 的第一篇,主要讲述了如下内容:

  1. 寻找入口文件
  2. 在打包的过程中 Vue 发生了什么变化
  3. 在 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

我们总结一下:

  1. vue 是通过 rollup 进行打包的,打包的配置在一个单独的config.js文件里面
  2. 通过查阅 Vue 官网和 vue-cli3 官方文档,我们发现,项目中引用的 vue ,其实引用的是vue.runtime.esm.js文件
  3. 最后通过查找配置,我们发现 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.prototypeVue,来看看在打包的一步步过程中,挂载在它们上面的属性发生了什么变化

通过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)
}

可以看到,它在VueVue.prototype上面添加了平台特有utils、directives、components、patch 和 $mount方法。

到这里就全部结束了,我们整理这些属性和方法的目的是:

  1. 在接下来读源码的过程中,我们对 Vue 和 Vue.prototype 上的属性和方法更有信心
  2. 如果我们看到源码在调用某个不知道的属性或方法的时候,可以从这里来查找来源
  3. 我们能够看到 Vue 源码的代码结构是怎么组织的

我们再总结一下:

  1. Vue.prototype 上的属性和方法主要是在 src/core/instance/index.js 里面挂载的。
  2. Vue 上的静态属性和方法主要是在 src/core/index.js 里面的initGlobalAPI方法里面挂载的。这个文件里面还处理了ssr(服务端渲染)相关的东西和加上了版本号。
  3. 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<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
}

可以发现,Ctor的值是传入的vm.constructor,即 vm 的构造函数,而 vm 的构造函数就是 Vue,所以Ctor其实就是Vue。所以options就是Vue上的静态属性Vue.options。然后由于目前Vue没有继承,所以Vue.superundefined,所以这里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.extendschild.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方法在propsDatavm.$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.methodsvm.$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我们以后再讲,我们现在先理清大致的轮廓线)

其实后面的initComputedinitWatch都差不多,都是先做一些判断和报错信息,然后同步到 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实例到挂载完毕的全过程。

我们来总结一下:

  1. Vue 源码先使用_init方法进行初始化,然后使用各平台定义的$mount方法进行挂载的。
  2. 在初始化的过程中,Vue 源码先以一定的规则合并了我们传入的options和父组件的options,然后初始化了proxy,lifecycle,state等,同时也在不同的时间段调用了生命周期钩子。
  3. 在挂载的过程中,Vue 源码通过获取render方法的执行结果,把它加入到更新函数里面去,再使用watcher使更新函数变成响应式的,从而在挂载的过程中、在父组件更新的过程中、在传入的数据发生变化的过程中都能进行自动更新

后记

上面我们关于响应式的代码讲解都省略了,我打算放到下一期来一起讲,主要包括:defineReactive,observe,watcher。我们把响应式讲清楚之后,这里省略的部分就一目了然了,敬请期待!!!