• wordpress CMS主题:ssmay主题 wordpress CMS主题:ssmay主题
  • 首页 > 前端开发 > 为什么Vue.mixin中的定义的data全局可用

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

    作者: 分类:前端开发 点击: 437 次
    wordpress CMS主题:ssmay主题

      扫描下面的二维码,“关注”我的百家号。

      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策略函数执行一遍,但这次流程未必一样。

      注意,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

      本文转载于:猿2048https://www.mk2048.com/blog/blog_h0cijbkc1j.html



      欢迎“关注”我的百家号。

      头条二维码
      加入我的QQ群
      头条二维码
      关注我的百家号

    文章作者:sunny
    本文地址:http://wanlimm.com/77202006208331.html
    版权所有 © 转载时必须以链接形式注明作者和原始出处!

    上一篇:
    下一篇:
    wordpress CMS主题:ssmay主题

    或许你会感兴趣的文章:

    发表评论

    电子邮件地址不会被公开。 必填项已用*标注

    This site uses Akismet to reduce spam. Learn how your comment data is processed.