0. 背景
目前在丁香医生的业务中, 我会负责一个基于 vue 全家桶的 webApp 项目.
一直有件不太弄得明白的事: 在每个组件的 template 标签里, 都会使用 dataReady 来进行渲染控制. 例如像下面这样, 请求完了以后再渲染页面.
- ## 模板部分
- <template>
- <div class="wrap"
- v-if="dataReady">
- </div>
- </template>
- ## 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 函数中渲染的组件, 是对应子类的实例.
2. 先从 Vue.mixin 看起
源码长这样:
- 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 对象上的某个属性的键.
好, 可以确认的是, 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 策略函数执行一遍, 但这次流程未必一样.
注意, parentVal 是 Vue.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 类的实例.
来源: https://juejin.im/post/5b7bb9b56fb9a019f671266a