zoukankan      html  css  js  c++  java
  • 挖掘隐藏在源码中的Vue技巧!

    前言

    最近关于Vue的技巧文章大热,我自己也写过一篇(vue开发中的"骚操作"),但这篇文章的技巧是能在Vue的文档中找到蛛丝马迹的,而有些文章说的技巧在Vue文档中根本找不到踪迹!这是为什么呢?

    当我开始阅读源码的时候,我才发现,其实这些所谓的技巧就是对源码的理解而已。

    下面我分享一下我的收获。

    隐藏在源码中的技巧

    我们知道,在使用Vue时,要使用new关键字进行调用,这就说明Vue是一个构造函数。所以源头就是定义Vue构造函数的地方!

    src/core/instance/index.js中找到了这个构造函数

    function Vue (options) {
      if (process.env.NODE_ENV !== 'production' &&
        !(this instanceof Vue)
      ) {
        warn('Vue is a constructor and should be called with the `new` keyword')
      }
      this._init(options)
    }
    initMixin(Vue)
    stateMixin(Vue)
    eventsMixin(Vue)
    lifecycleMixin(Vue)
    renderMixin(Vue)

    在构造函数中,只做一件事——执行this._init(options)

    _init()函数是在initMixin(Vue)中定义的

    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        // ... _init 方法的函数体,此处省略
      }
    }

    以此为主线,来看看在这过程中有什么好玩的技巧。

    解构赋值子组件data的参数

    按照官方文档,我们一般是这样写子组件data选项的:

    props: ['parentData'],
    data () {
      return {
        childData: this.parentData
      }
    }

    但你知道吗,也是可以这么写:

    data (vm) {
      return {
        childData: vm.parentData
      }
    }
    // 或者使用解构赋值
    data ({ parentData }) {
      return {
        childData: parentData
      }
    }

    通过解构赋值的方式将props里的变量传给data函数中,也就是说 data 函数的参数就是当前实例对象。

    这是因为data函数的执行是用call()方法强制绑定了当前实例对象。这发生在data合并的阶段,接下来去看看,说不定还有一些别的收获!

    _init()函数中主要是执行一系列的初始化,其中options选项的合并是初始化的基础。

    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )

    Vue实例上添加了$options属性,在那些初始化方法中,无一例外的都使用到了实例的$options属性,即vm.$options

    其中合并data就是在mergeOption中进行的。

    strats.data = function (
      parentVal: any,
      childVal: any,
      vm?: Component
    ): ?Function {
      if (!vm) {
        if (childVal && typeof childVal !== 'function') {
          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
        }
        return mergeDataOrFn(parentVal, childVal)
      }
    
      return mergeDataOrFn(parentVal, childVal, vm)
    }

    上面代码是data选项的合并策略函数,首先通过判断是否存在vm,来判断是否为父子组件,存在vm则为父组件。不管怎么,最后都是返回mergeDataOrFn的执行结果。区别在于处理父组件时,透传vm

    接下来看看mergeDataOrFn函数。

    export function mergeDataOrFn (
      parentVal: any,
      childVal: any,
      vm?: Component
    ): ?Function {
      if (!vm) {
        // in a Vue.extend merge, both should be functions
        if (!childVal) {
          return parentVal
        }
        if (!parentVal) {
          return childVal
        }
        // when parentVal & childVal are both present,
        // we need to return a function that returns the
        // merged result of both functions... no need to
        // check if parentVal is a function here because
        // it has to be a function to pass previous merges.
        return function mergedDataFn () {
          return mergeData(
            typeof childVal === 'function' ? childVal.call(this, this) : childVal,
            typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
          )
        }
      } else {
        return function mergedInstanceDataFn () {
          // instance merge
          const instanceData = typeof childVal === 'function'
            ? childVal.call(vm, vm)
            : childVal
          const defaultData = typeof parentVal === 'function'
            ? parentVal.call(vm, vm)
            : parentVal
          if (instanceData) {
            return mergeData(instanceData, defaultData)
          } else {
            return defaultData
          }
        }
      }
    }

    函数整体是由if判断分支语句块组成,对vm进行判断,也使得mergeDataOrFn也能区分父子组件。

    return function mergedDataFn () {
      return mergeData(
        typeof childVal === 'function' ? childVal.call(this, this) : childVal,
        typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
      )
    }

    来看这一段,当父子组件的data选项同时存在,那么就返回mergedDataFn函数。mergedDataFn函数又返回mergeData函数。

    在mergeData函数中,执行父子组件的data选项函数,注意这里的 childVal.call(this, this) 和 parentVal.call(this, this),关键在于 call(this, this),可以看到,第一个 this 指定了 data 函数的作用域,而第二个 this 就是传递给 data 函数的参数。这就是开头能用解构赋值的原理。

    接着往下看!

    注意因为函数已经返回了(return),所以mergedDataFn函数还没有执行。

    以上就是处理子组件的data选项时所做的事,可以发现在处理子组件选项时返回的总是一个函数。

    说完了处理子组件选项的情况,再看看处理非子组件选项的情况,也就是使用 new 操作符创建实例时的情况。

    if (!vm) {
      ...
    } else {
      return function mergedInstanceDataFn () {
        // instance merge
        const instanceData = typeof childVal === 'function'
          ? childVal.call(vm, vm)
          : childVal
        const defaultData = typeof parentVal === 'function'
          ? parentVal.call(vm, vm)
          : parentVal
        if (instanceData) {
          return mergeData(instanceData, defaultData)
        } else {
          return defaultData
        }
      }
    }

    如果走else分支的话那么就直接返回mergedInstanceDataFn函数。其中父子组件data选项函数的执行也是用了call(vm, vm)方法,强制绑定当前实例对象。

    const instanceData = typeof childVal === 'function'
      ? childVal.call(vm, vm)
      : childVal
    const defaultData = typeof parentVal === 'function'
      ? parentVal.call(vm, vm)
      : parentVal

    注意此时的mergedInstanceDataFn函数同样还没有执行。所以mergeDataFn函数永远返回一个函数。

    为什么这么强调返回的是一个函数呢?也就是说strats.data最终结果是一个函数?

    这是因为,通过函数返回的数据对象,保证了每个组件实例都要有一个唯一的数据副本,避免了组件间数据互相影响。

    这个mergeDataFn就是后面的初始化阶段处理执行的。mergeDataFn返回是mergeData(childVal, parentVal)的执行结果才是真正合并父子组件的data选项。也就是到了初始化阶段才是真正合并,这是因为propsinject这两个选项的初始化是先于data选项的,这就保证了能够使用props初始化data中的数据。

    这才能在data选项中调用props或者inject的值!

    生命周期钩子可以写成数组形式

    生命周期钩子可以写成数组形式,不信你可以试试!

    created: [
        function () {
          console.log('first')
        },
        function () {
          console.log('second')
        },
        function () {
          console.log('third')
        }
    ]

    这啥能这么写?来看看生命周期钩子的合并处理!

    mergeHook是用于合并生命周期钩子。

    /**
     * Hooks and props are merged as arrays.
     */
    function mergeHook (
      parentVal: ?Array<Function>,
      childVal: ?Function | ?Array<Function>
    ): ?Array<Function> {
      return childVal
        ? parentVal
          ? parentVal.concat(childVal)
          : Array.isArray(childVal)
            ? childVal
            : [childVal]
        : parentVal
    }
    
    LIFECYCLE_HOOKS.forEach(hook => {
      strats[hook] = mergeHook
    })
    其实从注释中也能发现Hooks and props are merged as arrays.

    使用forEach遍历LIFECYCLE_HOOKS常量,说明LIFECYCLE_HOOKS是一个数组。LIFECYCLE_HOOKS来自于shared/constants.js文件。

    export const LIFECYCLE_HOOKS = [
      'beforeCreate',
      'created',
      'beforeMount',
      'mounted',
      'beforeUpdate',
      'updated',
      'beforeDestroy',
      'destroyed',
      'activated',
      'deactivated',
      'errorCaptured'
    ]

    所以那段forEach语句,它的作用就是在strats策略对象上添加用来合并各个生命周期钩子选项的函数。

    return childVal
        ? parentVal
          ? parentVal.concat(childVal)
          : Array.isArray(childVal)
            ? childVal
            : [childVal]
        : parentVal

    函数体由三组三目运算符组成,在经过 mergeHook 函数处理之后,组件选项的生命周期钩子函数被合并成一个数组。

    在第一个三目运算符中,首先判断是否有 childVal,即组件的选项是否写了生命周期钩子函数,如果没有则直接返回了 parentVal,这里有一个预设的假定,就是如果有 parentVal 那么一定是个数组,如果没有 parentVal 那么 strats[hooks] 函数根本不会执行。以 created 生命周期钩子函数为例:

    new Vue({
        created: function () {
            console.log('created')
        }
    })

    对于 strats.created 策略函数来讲,childVal 就是例子中的 created 选项,它是一个函数。parentVal 应该是 Vue.options.created,但 Vue.options.created 是不存在的,所以最终经过 strats.created 函数的处理将返回一个数组:

    options.created = [
      function () {
        console.log('created')
      }  
    ]

    再看下面的例子:

    const Parent = Vue.extend({
      created: function () {
        console.log('parentVal')
      }
    })
    
    const Child = new Parent({
      created: function () {
        console.log('childVal')
      }
    })

    其中 Child 是使用 new Parent 生成的,所以对于 Child 来讲,childVal 是:

    created: function () {
      console.log('childVal')
    }

    而 parentVal 已经不是 Vue.options.created 了,而是 Parent.options.created,那么 Parent.options.created 是什么呢?它其实是通过 Vue.extend 函数内部的 mergeOptions 处理过的,所以它应该是这样的:

    Parent.options.created = [
      created: function () {
        console.log('parentVal')
      }
    ]

    经过mergeHook函数处理,关键在那句:parentVal.concat(childVal),将 parentVal 和 childVal 合并成一个数组。所以最终结果如下:

    [
      created: function () {
        console.log('parentVal')
      },
      created: function () {
        console.log('childVal')
      }
    ]

    另外注意第三个三目运算符:

    : Array.isArray(childVal)
      ? childVal
      : [childVal]

    它判断了 childVal 是不是数组,这说明了生命周期钩子是可以写成数组的。这就是开头所说的原理!

    生命周期钩子的事件侦听器

    大家可能不知道什么叫做「生命周期钩子的事件侦听器」?,其实Vue组件是可以这么写的:

    <child
      @hook:created="childCreated"
      @hook:mounted="childMounted"
     />

    在初始化中,使用callhook(vm, 'created')函数执行created生命周期函数,接下来瞧一瞧callhook()的实现方法:

    export function callHook (vm: Component, hook: string) {
      // #7573 disable dep collection when invoking lifecycle hooks
      pushTarget()
      const handlers = vm.$options[hook]
      if (handlers) {
        for (let i = 0, j = handlers.length; i < j; i++) {
          try {
            handlers[i].call(vm)
          } catch (e) {
            handleError(e, vm, `${hook} hook`)
          }
        }
      }
      if (vm._hasHookEvent) {
        vm.$emit('hook:' + hook)
      }
      popTarget()
    }

    callhook()函数接收两个参数:

    • 实例对象;
    • 要调用的生命周期钩子的名称;

    首先缓存生命周期函数:

    const handlers = vm.$options[hook]

    如果执行 callHook(vm, created),那么就相当于:

    const handlers = vm.$options.created

    刚刚介绍过,对于生命周期钩子选项最终会被合并处理成一个数组,所以得到的handlers就是一个生命周期钩子的数组。接着执行的是这段代码:

    if (handlers) {
      for (let i = 0, j = handlers.length; i < j; i++) {
        try {
          handlers[i].call(vm)
        } catch (e) {
          handleError(e, vm, `${hook} hook`)
        }
      }
    }

    最后注意到 callHook 函数的最后有这样一段代码:

    if (vm._hasHookEvent) {
      vm.$emit('hook:' + hook)
    }

    其中 vm._hasHookEvent 是在initEvents函数中定义的,它的作用是判断是否存在「生命周期钩子的事件侦听器」,初始化值为 false 代表没有,当组件检测到存在生命周期钩子的事件侦听器时,会将vm._hasHookEvent设置为 true

    生命周期钩子的事件侦听器,就是开头说的:

    <child
      @hook:created="childCreated"
      @hook:mounted="childMounted"
     />

    使用hook:加生命周期钩子名称的方式来监听组件相应的生命周期钩子。

    总结

    1、子组件data选项函数是有参数的,而且是当前的实例对象;

    2、生命周期钩子是可以写成数组形式,按顺序执行;

    3、可以使用生命周期钩子的事件侦听器来注册生命周期函数

    「不过没在官方文档中写明的方法,不建议使用」。

    作者: zhangwinwin
    链接:挖掘隐藏在源码中的Vue技巧!
    来源:github

  • 相关阅读:
    用例图会不会
    存储过程进阶(vb.net+SQL Server2008环境)
    众说纷纭,我也说“云”
    三层架构之抽象工厂加反射实现数据库转换
    三层架构之抽象工厂加反射&mdash;&mdash;实现数据库转换
    存储过程懂不懂
    8个对于Web设计和开发人员非常有用的在线工具
    TexturePacker的使用(图片打包再一起)
    cocos2dx游戏摇杆的实现方法
    cocos2dx 矩形碰撞检测
  • 原文地址:https://www.cnblogs.com/xzsj/p/13857491.html
Copyright © 2011-2022 走看看