Vue.js源码学习 —— 各全局API的实现

如果您是刚开始准备阅读Vue.js的源码,建议先看一下本系列的Vue.js源码学习 —— 起步,相信会对您后面的阅读有很大帮助。

今天我们来看一下 Vue 为开发者提供的各全局API的实现,大部分其实我们都已经遇到了,有的也分析过了实现过程,这篇文章我将其放在一起,这样可以从总体上有一个感观印象。

为开发者提供的全局API都可以在文件夹core/global-api中找到,我们打开文件core/global-api/index.js看一下。这里对外提供了一个initGlobalAPI函数,初始化了所有的全局API,可以看到,全局API其实是放在Vue构造器本身上的。

Vue.config

Vue.config相关的代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 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.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)

通过Object.defineProperty定义了configgetset方法,主要是为了在开发环境下当开发者试图给Vue.config重新赋值时给出告警信息。我们打个断点看一下在开发环境下默认的Vue.config有哪些,如下图所示:

关于Vue.config中有哪些可以由开发者配置的字段,可查阅官方API文档#全局配置

Vue.util

initGlobalAPI函数中也添加了一些工具方法,方便在需要的时候使用,但这些方法的本意并不打算给开发者使用,所以我们如果在自己的项目使用时要谨慎。这些方法如下:

1
2
3
4
5
6
7
8
9
// 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 = {
warn,
extend,
mergeOptions,
defineReactive
}

这几个放在前面的文章都说过了,这里就不展开讲了。

Vue.set/Vue.delete

1
2
Vue.set = set
Vue.delete = del

这里的setdel方法就是响应系统模块提供的,来自文件core/observer/index.js,在Vue.js源码学习 —— 响应系统的设计中有详细地介绍它们的实现过程。

官方文档中对Vue.set的说明如下:

向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')

Vue.delete的说明如下:

删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到 property 被删除的限制,但是你应该很少会使用它。

这两个API都有对应的实例方法:vm.$setvm.$delete

Vue.nextTick

1
Vue.nextTick = nextTick

nextTick来自文件core/util/next-tick.js,在前面的文章Vue.js源码学习 —— 组件的更新中也分析过了。

官方文档中对Vue.nextTick的说明如下:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

这个API也有对应的实例方法:vm.$nextTick

Vue.observable

1
2
3
4
5
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}

observe函数也来自于文件core/observer/index.js,它的实现过程也可以看Vue.js源码学习 —— 响应系统的设计

官方文档中对Vue.observable的说明如下:

让一个对象可响应。Vue 内部会用它来处理 data 函数返回的对象。返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器,用于简单的场景:

1
2
3
4
5
6
7
8
9
const state = Vue.observable({ count: 0 })

const Demo = {
render(h) {
return h('button', {
on: { click: () => { state.count++ }}
}, `count is: ${state.count}`)
}
}

Vue.Mixin

官方文档中对Vue.Mixin的说明如下:

全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。

initMixin函数来自文件core/global-api/mixin.js,代码如下:

1
2
3
4
5
6
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}

可以看到Vue.mixin做的事就是将传入的选项和 Vue 构造器的选项进行合并,所以一定混入会影响所有 Vue 实例对象。我们已经知道 Vue 实例对象初始化时,会去合并 Vue 构造器自身的选项。

Vue.component/Vue.directive/Vue.filter

ASSET_TYPES的内容如下:

1
2
3
4
5
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]

我们将initAssetRegisters函数拆开成3个函数来看,处理过程就一目了然了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
Vue.component = function (
id: string,
definition: Function | Object
) {
if (!definition) {
return this.options.components[id]
} else {
if (process.env.NODE_ENV !== 'production') {
// 校验组件的名称是否可用
validateComponentName(id)
}
definition.name = definition.name || id
// this.options._base 就是 Vue
// 所以这里就是调用 Vue.extend(definition) 得到的就是 VueComponent 构造函数,
// 后面看 Vue.extend 的实现时会看到
definition = this.options._base.extend(definition)
// 所以这里就是在 Vue 的 components选项中添加了当前这个组件的构造函数
this.options.components[id] = definition
return definition
}
}

Vue.directive = function (
id: string,
definition: Function | Object
) {
if (!definition) {
return this.options.directives[id]
} else {
// 如果 definition 是函数,会默认作为 bind 和 update 两个钩子函数
if (typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options.directives[id] = definition
return definition
}
}

Vue.filter = function (
id: string,
definition: Function | Object
) {
if (!definition) {
return this.options.filter[id]
} else {
this.options.filter[id] = definition
return definition
}
}

Vue.Use

官方文档中对Vue.use的说明如下:

安装 Vue.js 插件。如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。

该方法需要在调用 new Vue() 之前被调用。

当 install 方法被同一个插件多次调用,插件将只会被安装一次。

再看initUse函数,正是实现这里说的每一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 判断插件是否已经存在了,如果已经存在了,则不再继续下面的处理
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}

// 取 plugin 后面的参数
const args = toArray(arguments, 1)
// 将 Vue 构造函数作为第1个参数 plugin
args.unshift(this)
// 如果 plugin 是对象,必须提供 install 方法
if (typeof plugin.install === 'function') {
// 执行 plugin.install
plugin.install.apply(plugin, args)
}
// 如果是一个函数,直接将它当做 install 方法
else if (typeof plugin === 'function') {
plugin.apply(null, args)
}

// 缓存该插件
installedPlugins.push(plugin)
return this
}
}

插件 通常用来为 Vue 添加全局功能,一般可以通过下面几种方式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}

// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}

// ...
})

// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}

// ...
})

// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}

Vue.extend

我们曾在讲选项合并部分多次使用到Vue.extend,所以对它应该不算陌生,它的主要作用如下:

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

initExtend函数来自文件core/global-api/extend.js,我们看一下它的实现。

实现子类构造器

我们已经知道Vue其实是一个函数,所以initExtend函数中主要是实现了基于原型的一个继承。我将这部分代码抽出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export function initExtend (Vue: GlobalAPI) {
// 每一个构造器都有一个唯一id,Vue本身的id为 0
Vue.cid = 0
let cid = 1

Vue.extend = function (extendOptions: Object): Function {
// ...
// 可以看到,VueComponent 就是 Vue 的子类
const Sub = function VueComponent (options) {
// 调用 Vue.prototype._init 方法进行初始化
this._init(options)
}

// 继承父类的原型
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub

// id 自增
Sub.cid = cid++

// 将用户传入的选项与父类构造器的选项合并
Sub.options = mergeOptions(
Super.options,
extendOptions
)

// 添加 super,可以完全模拟 es6 中的 class了
Sub['super'] = Super

// 如果用户在子类构造器中传入了 props 和 computed,在直接将其代理到子类的原型上
// 这是一种优化手段,避免了每次创建子类实例时都要调用 Object.defineProperty
// 在 http://shinancao.cn/2020/02/08/Vue-Source-Code-12/ 中有具体讲解代理过程
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 为 component、directive、filter
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})

// ...
}
}

Sub中还存了几个options,主要是为了在初始化实例对象时判断父构造器中的选项是否有发生变化。

1
2
3
4
5
6
// 父构造器的选项
Sub.superOptions = Super.options
// 用户在 Vue.extend 中传入选项
Sub.extendOptions = extendOptions
// Sub 本身的选项
Sub.sealedOptions = extend({}, Sub.options)

这样就可以用Sub.super.optionsSub.superOptions做各比较来判断是否改变了。

举一个官方文档的例子来调试一下,看看此时Sub是什么样子的:

1
<div id="mount-point"></div>
1
2
3
4
5
6
7
8
9
10
11
12
const Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})

new Profile().$mount('#mount-point')

继承Vue后,此时Sub中的内容如下图所示:

处理name选项

再看一下如果传入了name选项时的特殊处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue.extend = function (extendOptions: Object): Function {
// ...

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

// ...

// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
}

首先校验了name作为组件名字的有效性,validateComponentName函数在Vue.js源码学习 —— 合并选项前的预处理有详细介绍。

然后在刚创建的子类构造器的components选项中添加了自身,这样就可以支持组件递归查找自身,也就是可以来实现递归组件了。将前面的例子改造成递归组件:

1
2
3
<div id="mount-point">
<profile v-bind:depth="2"/>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Profile = Vue.extend({
name: 'profile', // 提供 name 选项
props: {
depth: Number // 提供递归结束的条件,否则会无限创建下去
},
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
},
template: '<div><p v-if="depth === 0">{{firstName}} {{lastName}} aka {{alias}}</p><profile v-else v-bind:depth="depth-1"/></div>'
})

new Vue({
el: '#mount-point',
components: {
'profile': Profile
}
})

这时Sub.options.components的内容如下:

1
2
3
4
5
6
7
8
components: {
profile: ƒ VueComponent(options),
'__proto__': {
KeepAlive,
Transition,
TransitionGroup
}
}

这样在执行createElement函数创建组件节点时,执行到下面这样代码时,调用resolveAsset函数就可以拿到组件的构造器了。

1
2
3
4
if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
}

resolveAsset函数位于文件core/util/options.js中,在Vue.js源码学习 —— VNode/createElement中也分析过,不赘述了。

当要创建组件时,提供name除了能让我们实现递归组件,还有一个好处,就是一个有名字的组件更便于调试。当你打断点查看组件变量时可以明确知道是哪个组件,控制台提示的告警信息也会更友好。在文件createComponent函数中可以看到组件节点的tag的生成方式:

1
2
3
4
5
6
7
8
9
10
11
12
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, // tag
data, // data
undefined, // children
undefined, // text
undefined, // elm
context, // context
{ Ctor, propsData, listeners, tag, children }, // componentOptions
asyncFactory // asyncFactory
)

缓存子类构造器

创建过的子类构造器会被缓存下来,我把这部分代码单独抽出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}

// ...

// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}

我们上面的例子中子类构造器被缓存后如下:

以上就是各个全局API的实现过程了,很多地方都涉及到了之前分析的内容,所以如果本系之前的内容都看过,理解上面的内容还是比较轻松的。

本文作者:意林
本文链接:http://shinancao.cn/2020/02/18/Vue-Source-Code-16/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!