0. 背景 目前在丁香医生的业务中,我会负责一个基于Vue全家桶的WebApp项目。
一直有件不太弄得明白的事:在每个组件的template标签里,都会使用dataReady
来进行渲染控制。例如像下面这样,请求完了以后再渲染页面。
## 模板部分
"wrap"
v-if="dataReady">## Script部分async created() {
await this.makeSomeRequest();
this.dataReady = true;
},
复制代码
但是实际上,我在组件的data选项里并没有定义
dataReady
属性。于是,我查了查入口文件
main.js
中,有这么句话Vue.mixin({
data() {
return {
dataReady: false
};
}
// 以下省略
});
复制代码
为什么一个在全局定义的变量,在每个组件里都可以用呢?Vue是怎么做到的呢?
于是,在翻了一堆资料和源码之后,有点儿答案了。
1. 前置知识 由于部分前置知识解释起来很复杂,因此我直接以结论的形式给出:
- Vue是个构造函数,通过
new Vue
创造出来的是根实例 - 所有的单文件组件,都是通过Vue.extend扩展出来的子类。
- 每个在父组件的标签中template标签,或者render函数中渲染的组件,是对应子类的实例。
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
复制代码
很简单,把当前上下文对象的options和传入的参数做一次扩展嘛。
所以做事的,其实是
mergeOptions
这个函数,它把Vue类上的静态属性options扩展了。那我们看看
mergeOptions
,到底做了什么。3. Vue类上用mergeOptions进行选项合并 找到
mergeOptions
源码,记住一下。export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 中间好长一串代码,都跳过不看,暂时和data属性没关系。
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
}
复制代码
这个
mergeOptions
函数,其实就只是在传入的options
对象上,遍历自身的属性,来执行mergeField
函数,然后返回一个新的options。那么问题就变化成了:
mergeField
到底做了什么?我们看它的代码。// 找到合并策略函数
const strat = strats[key] || defaultStrat// 执行合并策略函数
options[key] = strat(parent[key], child[key], vm, key)
复制代码
现在回忆一下,
- parent是什么?—— 在这个例子里,是Vue.options
- child是什么?对,就是使用mixin方法时传入的参数对象。
- 那么key是什么? —— 是在parents或者child对象上的某个属性的键。
行咯,那我们找找看什么是
strats.data
。strats.data = https://www.it610.com/article/function (
// parentVal,在这个例子里,是Vue自身的options选项上的data属性,有可能不存在
parentVal: any,// childVal,在这个例子里,是mixin方法传入的选项对象中的data属性
childVal: any,
vm?: Component
): ?Function {// 回想一下Vue.mixin的代码,会发现vm为空
if (!vm) {
if (childVal && typeof childVal !=='function') {
// 这个错误眼熟吗?想想如果你刚才.mixin的时候,传入的data如果不是函数,是不是就报错了?
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)return parentVal
}// 这条语句的返回值,将会在mergeField函数中,作为options.data的值。
return mergeDataOrFn(parentVal, childVal)
}
// 在这个例子里,下面这行不会执行,为什么?自己想想。
return mergeDataOrFn(parentVal, childVal, vm)
}
复制代码
OK,那我们再来看看,
mergeDataOrFn
,到底是什么。export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// childVal是刚刚mixin方法的参数中的data属性,一个函数
if (!childVal) {
return parentVal
}
// parentVal是Vue.options.data属性,然鹅Vue属性并没有自带的data属性
if (!parentVal) {
return childVal
}
// 下边也不用看了,到这里就返回了。
} else {
// 这里不用看先,反正你也没有传递vm参数嘛
}
}
复制代码
所以,是不是最终就是这么句话
Vue.options.data = https://www.it610.com/article/function data(){
return {
dataReady: false
}
}
复制代码
4. 从Vue类 -> 子类 话说,刚刚这个data属性,明明加在了Vue.options上,凭啥Vue的那些单文件组件,也就是子类,它们的实例里也能用啊?
这就要讲到
Vue.extend
函数了,它是用来扩展子类的,平时我们写的一个个SFC单文件组件,其实都是Vue类的子类。Vue.extend = function (extendOptions: Object): Function {
const Super = this// 你不用关心中间还有一些代码const Sub = function VueComponent (options) {
this._init(options)
}// 继承
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++// 注意这里也执行了options函数,做了选项合并工作。
Sub.options = mergeOptions(
Super.options,
extendOptions
)// 你不用关心中间还有一些代码// 把子类返回出去了。
return Sub;
}
复制代码
- extendOptions是什么?
export default {
// 当然,也可能没有data函数
data(){
return{
id: 0
}
},
methods: {
handleClick(){}
}
}
复制代码
- Super.options是什么?
Vue -> Parent -> Child
这样的多重继承关系的,所以可以认为Super.options
,就是前面说的Vue.options
!记得吗?在执行完了Vue.mixin之后,
Vue.options
有data属性噢。5. Vue类 -> 子类时的mergeOptions 这时候再来看
Sub.options = mergeOptions(
Super.options,
extendOptions
)
复制代码
我们再次回到
mergeOptions
函数。export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
// 省略上面一些检查和规范化
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
}
复制代码
就和刚才一样,还是会返回一个options,并且给到
Sub.options
。其中options.data属性,仍然会被
strats.data
策略函数执行一遍,但这次流程未必一样。注意,
parentVal
是Vue.options.data
,而childVal可能是一个data
函数,也可能为空。为什么?去问前面的extendOptions
啊,它传的参数啊。strats.data = https://www.it610.com/article/function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !=='function') {
// 省略
}
// 没问题,还是执行这一句。
return mergeDataOrFn(parentVal, childVal)
}return mergeDataOrFn(parentVal, childVal, vm)
}
复制代码
我们可以看到,流程基本一致,还是执行
return mergeDataOrFn(parentVal, childVal)
。我们再看这个
mergeDataOrFn
。首先假定childVal为空。
export function mergeDataOrFn (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
// 到这里就返回了
if (!childVal) {
return parentVal
}
} else {
// 省略
}
}
复制代码
所以如果
extendOptions
没传data属性(一个函数),那么他就会使用parentVal,也就是Vue.options.data
。所以,可以简单理解为
Sub.options.data = https://www.it610.com/article/Vue.options.data = function data(){
return {
dataReady: false
}
}复制代码
那要是
extendOptions
传了个data函数呢?我们可以在mergeDataOrFn这个函数里继续找return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
复制代码
返回的是个函数,考虑到这里的childVal和parentVal都是函数,我们可以简化一下代码
// 现在假设子类的data选项长这样
function subData(){
return{
id: 0
}
}function vueData(){
return {
dataReady: false
}
}// Sub得到了什么?Sub.options.data = https://www.it610.com/article/function data(){
return mergeData(
subData.call(this, this),
vueData.call(this, this)
)
}复制代码
请想一下这里的this是什么,在结尾告诉你。
在Sub类进行一次实例化的时候,
Sub.options.data
会进行执行。所以会得到这个形式的结果。return mergeData({ id: 0 }, { dataReady: false })
复制代码
具体mergeData的原理也很简单:遍历key + 深度合并;而如果key同名的话,就不会执行覆盖。具体的去看下
mergeData
这个函数好了,这不是本文重点。具体怎么执行实例化,怎么执行
data
函数的,有兴趣的可以自己去了解,简单说下,和三个函数有关:- Vue.prototype._init
- initState
- initData
dataReady: false
了吗?其实一句话概括起来,就是:Vue类上的data函数(我称为parentDataFn)会与子类的data函数(我称为childDataFn)合并,得到一个新函数,这个新函数会会在子类在实例化时执行,且同时执行parentDataFn和childDataFn,并返回合并后的data对象。
顺便,刚才
Sub.options.data = https://www.it610.com/article/function mergedDataFn(){
return mergeData(
subData.call(this, this),
vueData.call(this, this)
)
}
复制代码
这里的this,是一个Sub类的实例。
8. 结语 说实在的,之前会自己在做完工作以后,写一点文章,让自己能够更好地理解自己到底学到了什么,比如:
- Linux 前后台进程切换
- new过程中发生了什么?
而这次,则是第一次尝试理解像Vue源码这样的复杂系统,很担心很多地方会误导人,所以特别感谢以下参考资料:
- 对MergeOption的解析: Vue技术内幕
- Vue-mixin合并的应用:在 Vue.js 中使用Mixin。
最后,丁香医生前端团队正在招人。
团队介绍在这里。
对招聘有意向或者疑问的话,可以在知乎上私信作者。
【javascript|为什么Vue.mixin中的定义的data全局可用】作者:丁香园 前端工程师 @Kevin Wong
推荐阅读
- 前端|vue面试题(自用)
- javascript基本数据类型详细解读
- 高德地图|vue高德地图实现海量点分组(用icon图标替换海量点)动态循环出groupStyleMap集合
- 网络|网络流行简笔画图片大全,互联网图标简笔画
- 游戏|[转]JS游戏引擎 & HTML5
- 前端|【Vue全家桶】--- 第一章(Vue核心)
- 前端|【Vue全家桶】--- 第三章(Vue脚手架(Vue CLI))
- Vue|第二章(Vue组件化编程)
- node|第四章(数据库和身份认证)