为何Vue.mixin中的定义的data全局可用

0. 背景

目前在丁香医生的业务中,我会负责一个基于Vue全家桶的WebApp项目。css

一直有件不太弄得明白的事:在每一个组件的template标签里,都会使用dataReady来进行渲染控制。例如像下面这样,请求完了之后再渲染页面。html

## 模板部分
<template>
  <div class="wrap"
       v-if="dataReady">
  </div>
</template>

## Script部分

  async created() {
    await this.makeSomeRequest();
    this.dataReady = true;
  },
复制代码

可是实际上,我在组件的data选项里并无定义dataReady属性。前端

因而,我查了查入口文件main.js中,有这么句话vue

Vue.mixin({
    data() {
      return {
        dataReady: false
      };
    }
    // 如下省略
  });
复制代码

为何一个在全局定义的变量,在每一个组件里均可以用呢?Vue是怎么作到的呢?bash

因而,在翻了一堆资料和源码以后,有点儿答案了。前端工程师

1. 前置知识

因为部分前置知识解释起来很复杂,所以我直接以结论的形式给出:async

  • Vue是个构造函数,经过new Vue创造出来的是根实例
  • 全部的单文件组件,都是经过Vue.extend扩展出来的子类。
  • 每一个在父组件的标签中template标签,或者render函数中渲染的组件,是对应子类的实例。

2. 先从Vue.mixin看起

源码长这样:函数

Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
复制代码

很简单,把当前上下文对象的options和传入的参数作一次扩展嘛。post

因此作事的,实际上是mergeOptions这个函数,它把Vue类上的静态属性options扩展了。ui

那咱们看看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对象上的某个属性的键。

好,能够确认的是,child对象上,必定包含一个key为data的属性。

行咯,那咱们找找看什么是strats.data

strats.data = 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 = 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策略函数执行一遍,但此次流程未必同样。

注意,parentValVue.options.data,而childVal多是一个data函数,也可能为空。为何?去问前面的extendOptions啊,它传的参数啊。

strats.data = 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 = 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 = 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

7. 尾声

如今你理解,为何每一个组件里,都会有一个dataReady: false了吗?

其实一句话归纳起来,就是:Vue类上的data函数(我称为parentDataFn)会与子类的data函数(我称为childDataFn)合并,获得一个新函数,这个新函数会会在子类在实例化时执行,且同时执行parentDataFn和childDataFn,并返回合并后的data对象。

顺便,刚才

Sub.options.data = function mergedDataFn(){
    return mergeData(
        subData.call(this, this),
        vueData.call(this, this)
    )
}
复制代码

这里的this,是一个Sub类的实例。

8. 结语

说实在的,以前会本身在作完工做之后,写一点文章,让本身可以更好地理解本身到底学到了什么,好比:

可是都是很简单的“技能记录”或者“基础探究”。

而此次,则是第一次尝试理解像Vue源码这样的复杂系统,很担忧不少地方会误导人,因此特别感谢如下参考资料:

若是还有什么说得不太对,还请多提些意见。

最后,丁香医生前端团队正在招人。

团队介绍在这里

对招聘有意向或者疑问的话,能够在知乎上私信做者。

做者:丁香园 前端工程师 @Kevin Wong

相关文章
相关标签/搜索