深入浅出Vue.js阅读——整体流程——生命周期

生命周期各阶段

在这里插入图片描述
  如图所示,生命周期可以分为四个阶段:初始化阶段、模板编译阶段、挂载阶段、卸载阶段

初始化阶段

  如图所示,new Vue()created之间的阶段叫作初始化阶段。
  这个阶段主要的目的是在实例上初始化一些属性、事件、以及响应式数据,如propsmethodsdatacomputedwatchprovideinject等。

模板编译阶段

  如图所示,在createdbeforeMount钩子函数之间的阶段是模板编译阶段。
  主要目的是将模板编译为渲染函数,只存在于完整版中。
  当使用vue-loader或者vueify时,*.vue文件内部的模板会在构建时预编译成JavaScript,所以最终打包好的包里是不需要编译器的,用运行时版本即可。由于这时模板已经预编译成渲染函数,所以在生命周期中并不存在模板编译阶段,初始化阶段的下一个生命周期是挂载阶段。

挂载阶段

  如图所示,beforeMount钩子函数到mounted钩子函数之间是挂载阶段。
  Vue.js会将其实例挂载到DOM元素上,通俗的讲,就是将模板渲染到指定的DOM元素中。在挂载的过程中,Vue.js会开启watcher来持续追踪依赖的变化。
  在已经挂载的状态下,仍然会持续追踪状态的变化。当数据状态发生变化时,Watcher会通知虚拟DOM重新渲染视图,并且会在渲染视图前触发beforeUpdate钩子函数,渲染完毕后触发updated钩子函数。
  通常,在运行时大部分时间下,Vue.js处于已经挂载状态,每当状态发生变化时,Vue.js都会通知组件使用虚拟DOM重新渲染,也就是常说的响应式。这个状态会持续到组件被销毁。

卸载阶段

  如图所示,应用调用vm.$destroy方法后,Vue.js的生命周期会进入卸载阶段。
  在这个阶段,Vue.js会将自身从父组件中删除,取消实例上所有依赖的追踪并且移除所有的事件监听器。

小结

  生命周期整体上可以分为两部分:第一部分是初始化阶段、模板编译阶段与挂载阶段,第二部分是卸载阶段。

从源码角度了解生命周期

  主要介绍初始化阶段的内部原理。

new Vue()被调用时发生了什么

  当new Vue()被调用时,会首先进行一些初始化操作,然后进入模板编译阶段,最后进入挂载阶段。
具体实现:

  function Vue(){
    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)
  }
  export default Vue

  先进行安全检查,在非生产环境下,如果没有使用new来调用Vue,则会在控制台抛出错误警告:Vue是构造函数,应该使用new关键字调用。
  然后调用this._init(options)来执行生命周期初始化流程。也就是说,生命周期的初始化流程在this,_init中实现。

_init方法的定义

  Vue.js通过调用initMixin方法将_init挂载到Vue构造函数的原型上,代码如下:

  import { initMixin } from './init';
  function Vue(){
    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);
  export default Vue

  将init.js文件导出的initMixin函数引入后,通过调用initMixin函数想Vue构造函数的原型上挂载一些方法。initMixin方法的实现代码如下:

  export function initMixin(Vue){
    Vue.prototype._init = function(options){
      // do things
    }
  }

  只是在Vue构造函数的prptotype属性上添加了一个_init方法。

_init方法的内部原理

  当new Vue()执行后,触发的一系列初始化流程都是在_initr方法中启动的。

  Vue.prototype._init = function(options){
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm,'beforeCreate');
    initInjections(vm); //在data/props前初始化inject
    initState(vm);
    initProvide(vm); //在data/props后初始化provide
    callHook(vm,'created');

    // 如果用户在实例化Vue.js时传递了el选项,则自动开启模板编译阶段与挂载阶段
    // 如果没有传递el选项,则不进入下个生命周期流程
    // 用户需要执行vm.$mount方法,手动开启模板编译阶段与挂载阶段

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

  Vue.js会在初始化流程的不同时期通过callHook函数触发生命周期钩子。
  在执行初始化流程之前,实例上挂载了$options属性。目的是将用户传递的options选项与当前构造函数的options属性及其父级实例构造函数的options属性,合并成一个新的options并赋值给$options属性。resolveConstructorOptions的作用就是获取当当前实例中构造函数的options选项及其所有父级的构造函数的options
  在生命周期beforeCreate被触发之前执行了initLifecycleinitEventsinitRender。在初始化过程中,首先初始化事件与属性,然后触发生命周期钩子beforeCreate。随后初始化provide/inject和状态,这里的状态指的是propsmethodsdatacomputed以及watch。接着触发生命周期钩子created。最后,判断用户是否在参数中提供了el选项,如果是,则调用vm.$mount方法,进入后面的生命周期阶段。
在这里插入图片描述

callHook函数的实现原理

  callHook的作用是触发用户设置的生命周期钩子,而用户设置的生命周期钩子,而用户设置的生命周期钩子会在执行new Vue()时通过参数传递给Vue.js。也就是说,可以在Vue.js的构造函数中通过options参数得到用户设置的生命周期钩子。
  用户传入的options参数最终会与构造函数的options属性合并成一个新的options并赋值到vm.$options属性中,所以我们可以通过vm.$options得到用户设置的生命周期函数。例如:通过vm.$options.created得到用户设置的created钩子函数。
  值得注意的是,Vue,js在合并options的过程中会找出options中所有key是钩子函数的名字,并将它转换成数组。
  下面列出了所有生命周期钩子的函数名:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed
  • activated
  • deactivated
  • errorCaptured
      也就是说,通过vm.$options.created获取的是一个数组,数组中包含了钩子函数,例如:
console.log(vm.$options.created);

   为什么要把生命周期钩子转换成数组?
  Vue,mixin方法会将选项写入Vue,options中,因此它会影响之后创建的所有Vue,js实例,而Vue.js在初始化时会将构造函数中的options和用户传入的options选项合并成一个新的选项并赋值给vm.$options,所以这里会发生一个现象:Vue.mixin和用户在实例化Vue.js时,如果设置了同一个生命周期钩子,那么在触发生命周期时,需要同时触发这两个函数。而转换成数组后,可以在同一个生命周期钩子列表中保存多个生命周期钩子 。
  那么我们就可以知道,callHook的实现只需要从vm.$options中获取生命周期钩子列表,遍历列表,执行每一个生命周期钩子,就可以触发钩子函数,

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

  上面的代码给出了callHook的实现原理,它接受vmhook两个参数,其中前者是Vue.js实例的this,后者是生命周期钩子的名称。
  我们使用hookvm.$options中获取钩子函数列表后赋值给handlers,随后遍历handlers,执行每一个钩子函数。
  这里使用try,,catch语句捕获钩子函数内发生的错误,并使用handleError处理错误。handleError会依次执行父组件的errorCaptured钩子函数与全局的config.errorHandler,这也是为什么生命周期钩子errorCaptured可以捕获子孙组件的错误。关于handleError与生命周期钩子errorCaptured,我们会在随后的内容中详细介绍。

errorCaptured与错误处理

  errorCaptured钩子函数的作用是捕获来自子孙组件的错误,此钩子函数会收到三个参数:错误对象、发生错误的组件实例和包含错误来源的字符串。然后此钩子函数可以返回false,阻止该错误继续向上传播。
  传播规则如下:

  • 默认情况下,如果全局的config.errorHandler被定义,那么所有的错误都会发送给它,这样这些错误可以在单个位置报告给分析服务。
  • 如果一个组件继承的链路或其父级从属链路中存在多个errorCaptured钩子,则它们将会被相同的错误逐个唤起。
  • 如果errorCaptured钩子函数自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的config.errorHandler
  • 一个errorCaptured钩子函数能够返回false来阻止错误继续向上传播。本质上是说:“这个错误已经被搞定,应该被忽略”。它会阻止其他被这个错误唤起的errorCaptured钩子函数和全局的config.errorHandler
      事实上,errorCaptured钩子函数与Vue.js的错误处理有着千丝万缕的关系。Vue.js会捕获所有用户代码抛出的错误,然后会使用一个名叫handleError的函数来处理这些错误。
      用户编写的所有函数都是Vue.js调用的,例如用户在代码中注册的事件、生命周期钩子、渲染函数、函数类型的data属性、vm.$watch的第一个参数(函数类型)、nextTick和指令等。
      Vue.js在调用这些函数时,会使用try...catch语句来捕获有可能发生的错误。当错误发生并且被try...catch语句捕获后,Vue.js会使用handleError函数来处理错误,该函数会依次触发父组件链路上的每一个父组件中定义的errorCaptured钩子函数。如果全局的config.errorHandler被定义,那么所有的错误也会同时发送给config.errorHandler。也就是说,错误的传播规则是在handleError函数中实现的。
      handleError函数的实现原理并不复杂。根据前面的传播规则,我们先实现第一个需求:将所有错误发送给config. errorHandler,相关代码如下:
  export function handleError(err, vm, info) {
    // 这里的config.errorHandler就是Vue.config.errorHandler
    if (config.errorHandler) {
      try {
        return config.errorHandler.call(null, err, vm, info)
      } catch (e) {
        logError(err);
      }
    }
  }
  function logError(err){
    console.error(err);
  }

  这里先判断Vue.config. errorHandler是否存在,如果存在,则调用它,并将错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串通过参数的方式传递给它,并且使用try...catch语句捕获错误。如果全局错误处理的函数也发生报错,则在控制台打印其中抛出的错误。不论用户是否使用Vue.config.errorHandler捕获错误, Vue.js都会将错误信息打印在控制台。
  接下来实现第二个功能:如果一个组件继承的链路或其父级从属链路中存在多个errorCaptured钩子函数,则它们将会被相同的错误逐个唤起。在实现第二个功能之前,我们先调整一下代码的架构:

  export function handleError(err, vm, info) {
    globalHandleError(err, vm, info);
  }

  function globalHandleError (err, vm, info) {
    // 这里的config.errorHandler就是Vue.config.errorHandler
    if (config.errorHandler) {
      try {
        return config.errorHandler.call(null, err, vm, info)
      } catch (e) {
        logError(err);
      }
    }
  }

  function logError(err){
    console.error(err);
  }

  上面的代码片将全局错误处理相关的代码放到globalHandleError函数中。下面实现第二个功能:

  export function handleError (err, vm, info){
    if(vm){
      let cur = vm;
      while ((cur=cur.$parent)) {
        const hooks = cur.$options.errorCaptured;
        if (hooks) {
          for (let i=0;i<hooks.length;i++) {
            hooks[i].call(cur, err, vm, info)
          }
        }
      }
    }
    globalHandleError(err, vm, info)
  }

  通过while语句自底向上不停地循环获取父组件,知道根组件。
  在循环中,通过cur.$options.errorCaptured属性读出errorCaptured钩子函数列表,遍历钩子函数列表依次执行列表中的每一个errorCaptured钩子函数。
  自底向上的每一层都会读出当前层组件的errorCaptured钩子函数列表,并依次执行列表中的每一个钩子函数。当组件循环到根组件时,从属链路中的多个errorCaptured钩子函数就都被触发完了,此时,我们就不难理解为什么errorCaptured可以捕获来自子孙组件抛出的错误了。
  如果errorCaptured钩子函数自身抛出了一个错误,那么这个新错误和原本被捕获的错误都会发送给全局的config.errorHandler

  export function handleError (err, vm, info){
    if(vm){
      let cur = vm;
      while ((cur=cur.$parent)) {
        const hooks = cur.$options.errorCaptured;
        if (hooks) {
          for (let i=0;i<hooks.length;i++) {
            try {
              hooks[i].call(cur, err, vm, info);
            } catch (e) {
              globalHandleError(e, cur, 'errorCaptured hook');
            }
          }
        }
      }
    }
    globalHandleError(err, vm, info)
  }

  可以看到,只需要使用try...catch语句捕获钩子函数可能发出的错误,并通过执行globalHandleError将捕获到的错误发送给全局错误处理函数config.errorHandler即可。因为这个错误是钩子函数自身抛出的新错误,所以不影响自底向上执行钩子函数的流程。而原有的错误则会在自底向上这个循环结束后,将错误传递给全局错误处理钩子函数,就像代码中所写的那样。
  接下来实现最后一个功能:一个errorCaptured钩子函数能够返回false来阻止错误继续·向上传播。它会阻止其他被这个错误唤起的errorCaptured钩子函数和全局的config.errorHandler

  export function handleError (err, vm, info){
    if(vm){
      let cur = vm;
      while ((cur=cur.$parent)) {
        const hooks = cur.$options.errorCaptured;
        if (hooks) {
          for (let i=0;i<hooks.length;i++) {
            try {
              const capture = hooks[i].call(cur, err, vm, info) === false;
              if(capture) return;
            } catch (e) {
              globalHandleError(e, cur, 'errorCaptured hook');
            }
          }
        }
      }
    }
    globalHandleError(err, vm, info)
  }

  从代码中可以看到,改动并不是很大,但是很巧妙。这里使用capture保存钩子函数执行后的返回值,如果返回值false,则使用return语句停止程序继续执行。其巧妙的地方在于代码中的逻辑是先自底向上传递错误,之后再执行globalHandleError将错误发送给全局错误处理钩子函数。所以只要在自底向上这个循环中的某一层执行了return语句,程序就会立即停止执行,从而实现功能。因为一旦钩子函数返回了false, handleError函数将会执行return语句终止程序执行,所以错误向上传递和全局的config.errorHandler都会被停止。

初始化实例属性

  在Vue.js的整个生命周期中,初始化实例属性是第一步,需要实例话的属性既有Vue.js内部需要用到的属性,也有提供给外部使用的属性。
注意:以$开头的属性是提供给用户使用的外部属性,以_开头的属性是提供给内部使用的内部属性。
  Vue.js通过initLifecycle函数向实例中挂载属性,该函数接受Vue.js实例作为参数。所以在函数中,只需要向Vue,js实例设置属性即可达到向Vue.js实例挂载属性的目的。代码如下:

  export function initLifecycle (vm) {
    const options = vm.$options;

    // 找出第一个非抽象类
    let parent = options.parent;
    if (parent && !options.abstract) {
      while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent;
      }
      parent.$children.push(vm);
    }

    vm.$parent = parent;
    vm.$root = parent ? parent.$root : vm;

    vm.$children = [];
    vm.$refs = {};

    vm._watcher = null;
    vm._isDestroyed = false;
    vm._isBeingDestroyed = false;
  }

  可以看到,其逻辑并不复杂,只是在Vue.js实例上设置一些属性并提供一个默认值。
  稍微有点复杂的是vm.$parent属性,它需要找到第一个非抽象类型的父级,所以代码中会,进行判断:如果当前组件不是抽象组件并且存在父级,那么需要通过while来自底向上循环;如果父级是抽象类,那么继续向上,直到遇到第一个非抽象类的父级时,将它赋值给vm.$parent属性。
  另一个值得注意的是vm.$children属性,它会包含当前实例的直接子组件。该属性的值是从子组件中主动添加到父组件中的。上面代码中的parent.$children.push(vm),就是将当前实例添加到父组件实例的$children属性中。
  最后一个值得注意的属性是vm.$froot,它表示当前组件树的根Vue.js实例。这个属性的实现原理很巧妙,也很好理解。如果当前组件没有父组件,那么它自己其实就是根组件,它的$root属性是它自己,而它的子组件的vm.$root属性是沿用父级的$root,所以其直接子组件的$root属性还是它,其孙组件的$root属性沿用其直接子组件中的$root属性,以此类推。因此,我们会发现这其实是自顶向下将根组件的$root依次传递给每一个子组件的过程。

初始化事件

  初始化事件是指将父组件在模板中使用的v-on注册的事件添加到子组件的事件系统中。
  在Vue.js中,父组件可以在使用子组件的地方用v-on来监听子组件触发的事件。例如:

  <div id="counter-event-example">
    <p>{{total}}</p>
    <button-counrter v-on:increment = 'incrementTotal'></button-counrter>
    <button-counrter v-on:increment = 'incrementTotal'></button-counrter>
  </div>
  Vue.component('button',{
    template:'<button v-on:click = "incrementCounter" >{{counter}}</button>',
    data:function(){
      return {
        counter:0
      }
    },
    methods: {
      incrementCounter:function(){
        this.counter += 1;
        this.$emit('increment');
      }
    },
  })
  new Vue({
    el:'#counter-event-example',
    data:{
      total:0,
    },
    methods: {
      incrementTotal:function() {
        this.total +=1;
      }
    },
  })

  父组件的模板里使用v-on监听子组件中触发的increment事件,并在子组件中使用this.$emit触发该事件。
为什么不使用注册模板中的浏览器事件?
  你可能会有疑问,为什么不使用注册模板中的浏览器事件?对于这个问题,我们需要先简单介绍一下模板编译和虚拟DOM。在模板编译阶段,可以得到某个标签上的所有属性,其中就包括使用v-on或@注册的事件。在模板编译阶段,我们会将整个模板编译成渲染函数,而渲染函数其实就是一些嵌套在一起的创建元素节点的函数。创建元素节点的函数是这样的: _c(tagName, data, children)。当渲染流程启动时,渲染函数会被执行并生成一份VNode,随后虚拟DOM会使用VNode进行对比与渲染。在这个过程中会创建一些元素,但此时会判断当前这个标签究竟是真的标签还是一个组件:如果是组件标签,那么会将子组件实例化并给它传递一些参数,其中就包括父组件在模板中使用v-on注册在子组件标签上的事,件;如果是平台标签,则创建元素并插入到DOM中,同时会将标签上使用v-on注册的事件注册到浏览器事件中。
  简单来说,如果v-on写在组件标签上,那么这个事件会注册到子组件Vue.js事件系统中;如果是写在平台标签上,例如div,那么事件会被注册到浏览器事件中。
  我们会发现,子组件在初始化时,也就是初始化Vue.js实例时,有可能会接收父组件向子组件注册的事件。而子组件自身在模板中注册的事件,只有在渲染的时候才会根据虚拟DOM的对比结果来确定是注册事件还是解绑事件。
  所以在实例初始化阶段,被初始化的事件指的是父组件在模板中使用v-on监听子组件内触发的事件。
  Vue.js通过initEvents函数来执行初始化事件相关的逻辑,其代码如下:

export function initEvents (vm) {
  vm._event = Object.create(null);
  // 初始化父组件附加的事件
  const listeners = vm.$options._parentListeners;
  if (listeners) {
    updateComponentListenerts(vm, listeners)
  }
}

  首先在vm上新增_events属性并将它初始化为空对象,用来存储事件。事实上,所有使用vm.$on注册的事件监听器都会保存到vm._events属性中。
  在模板编译阶段,当模板解析到组件标签时,会实例化子组件,同时将标签上注册的事件解析成object并通过参数传递给子组件。所以当子组件被实例化时,可以在参数中获取父组件向自己注册的事件,这些事件最终会被保存在vm.$options._parentListeners中。
  用前面的例子中举例,vm.$options._parentListeners是下面的样子:

{increment: function(){}}

  通过前面的代码可以看到,如果vm.$options._parentListeners不为空,则调用updateComponentListeners方法,将父组件向子组件注册的事件注册到子组件实例中。
  updateComponentListeners的逻辑很简单,只需要循环vm.$options._parentListeners并使用vm.$on把事件都注册到this._events中即可。updateComponentListeners函数的源码如下:

  let target; 
  function add (event, fn, once) {
    if (once) {
      target.$once(event, fn);
    } else {
      target.$on(event, fn);
    }
  }

  function remove (event, fn) {
    target.$off(event, fn)
  }

  export function updateComponentListeners (vm, listeners, oldListeners) {
    target = vm;
    updateListeners(listeners, oldListeners || {}, add, remove, vm)
  }

  其中封装了addremove这两个函数,用来新增和删除事件。此外,还通过updatelisteners函数对比listenersoldListeners的不同,并调用参数中提供的addremove进行相应的注册事件和卸载事件的操作。它的实现思路并不复杂:如果listeners对象中存在某个key(也就是事件名)在oldListeners中不存在,那么说明这个事件是需要新增的事件;反过来,如果oldListeners中存在某些key (事件名)在listeners中不存在,那么说明这个事件是需要从事件系统中移除的。
  updateListeners函数的实现如下:

  export function updateListeners (on, oldOn, add, remove, vm) {
    let name, cur, old, event;
    for (name in on) {
      cur = on[name];
      old = oldOn[name];
      event = normalizeEvent (name);
      if (isUndef(cur)) {
        process.env.NODE_ENV !== 'production' && warn(
          `Invalid handler for event "${event.name}":got` + String(cur),
          vm
        )
      } else if (isUndef(old)) {
        if (isUndef(cur, fns)) {
          cur = on[name] = createFnInvoker(cur);
        }
        add(event.name, cur, event.once, event.capture, event.passive)
      } else if (cur !== old) {
        old.fns = cur;
        on[name] = old;
      }
    }
    for (name in oldOn) {
      if (isUndef(on[name])) {
        event = normalizeEvent(name);
        remove(event.name, oldOn[name], event.capture);
      }
    }
  } 

  该函数接收5个参数,分别是onoldOn,add, removevm。其主要逻辑是比对onoldOn来分辨哪些事件需要执行add注册事件,哪些事件需要执行remove删除事件。
  上面代码大致可以分为两部分,第一部分是循环on,第二部分是循环oldOn。第一部分的主,要作用是判断哪些事件在oldOn中不存在,调用add注册这些事件。第二部分的作用是循环oldOn,判断哪些事件在on中不存在,调用remove移除这些事件。
  在循环on的过程中,有如下三个判断:

  • 判断事件名对应的值是否是ubdefinednull,如果是,则在控制台发出警告。
  • 判断事件名在oldOn中是否存在,如果不存在,则调用add注册事件。
  • 如果事件名在oldoldOn中都存在,但是它们并不相同,则将事件回调替换成on中的回调,并且把on中的回调引用指向真实的事件系统中注册的事件,也就是oldOn中对应的事件。
    注意:代码中的isUndef函数用于判断传入的参数是否为undefinednull
      此外,代码中还有normalizeEvent函数,它的作用是什么?
      Vue.js的模板中支持事件修饰符,例如capture, oncepassive等,如果我们在模板中注册事件时使用了事件修饰符,那么在模板编译阶段解析标签上的属性时,会将这些修饰符改成对应的符号加在事件名的前面,例如<child v-on: increment.once="a"x</child>。此时vm.Soptions._parentListeners是下面的样子:
{~increment: function () {}}

  可以看到,事件名的前面新增了一个~符号,这说明该事件的事件修饰符是once,我们通过这样的方式来分辨当前事件是否使用了事件修饰符。而normalizeEvent函数的作用是将事件修饰符解析出来,其代码如下:

  const normalizeEvent = name => {
    const passive = name.charAt(0) === '&';
    name = passive ? name.slice(1) : name;
    const once = name.charAt(0) === '~';
    name = once ? name.slice(1) : name;
    const capture = name.charAt(0) === '!';
    name = capture ? name.slice(1) : name;
    return {
      name,
      once,
      capture,
      passive
    }
  }

  可以看到,如果事件有修饰符,则会将它截取出来。最终输出的对象中保存了事件名以及一些事件修饰符,这些修饰符为true说明事件使用了此事件修饰符。

初始化inject

  injectprovide通常是成对出现的,我们使用Vue.js开发应用时很少用到它们。这里先简单介绍它们的作用。

provide/inject的使用方式

说明provideinject主要为高阶插件/组件库提供用例,并不推荐直接用于程序代码中。
  injectprovide选项需要一起使用,它们允许祖先组件向其所有子孙后代注入依赖,并在其上下游关系成立的时间里始终生效(不论组件层次有多深),如果你熟悉React,会发现这与它的上下文特性很相似provide选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性,可以使用ES2015 Symbol作为key,但是这只在原生支持SymbolReflect.ownkeys的环境下可工作。
  inject选项应该是一个字符串数组或对象,其中对象的key是本地的绑定名, value是一个key (字符串或Symbol)或对象,用来在可用的注入内容中搜索。
  如果是对象,那么它有如下两个属性。

  • name: 它是可用在注入内容中的用来搜索的key(字符串或Symbol)。
  • default:它是在降级情况下使用的value
    ==说明:==可用的注入内容指的是祖先组件通过provide注入了内容,子孙组件可以通过inject获取祖先组件注入的内容。
    示例如下:
  var Provider = {
    provide:{
      foo:'bar'
    }
  }

  var Child = {
    inject: ['foo'],
    created() {
      console.log(this.foo); // =>"bar"
    },
  }

  如果使用ES6的Symbol作为key,则provide函数和inject对象如下所示:

const s = Symbol()

const Provider = {
  provide () {
    return {
      [s]: 'foo'
    }
  }
}

const Child = {
  inject: { s },
}

  并且可以在data/props中访问注入的值。例如,使用一个注入的值作为props的默认值:

  const Child = {
    inject: ['foo'],
    props: {
      bar: {
        default () {
          return this.foo;
        }
      }
    }
  }

  或者使用一个注入的值作为数据入口:

  const Child = {
    inject: ['foo'],
    data () {
      return {
        bar: this.foo
      }
    }
  }

  在Vue.js 2.5.0+版本中,可以通过设置inject的默认值使其变成可选项:

const Child = {
  inject: {
    foo: {default: 'foo'}
  }
}

  如果它需要从一个不同名字的属性注入,则使用from来表示其源属性。

  const Child = {
    inject : {
      foo: {
        from: 'bar',
        default: 'foo'
      }
    }
  }

  上面代码表示祖先组件注入的名字是bar,子组件将内容注入到foo中,在子组件中可以通过this.foo来访问内容。
  inject的默认值与props的默认值类似,我们需要对非原始值使用一个工厂方法:

  const Child = {
    inject: {
      foo: {
        from: 'bar',
        default: ()=>{}
      }
    }
  }

inject的内部原理

  虽然injectprovide是成对出现的,但是二者在内部的实现是分开处理的,先处理inject后处理provideinjectdata/props之前初始化,而providedata/props后面初始化。这样做的目的是让用户可以在data/props中使用inject所注入的内容。也就是说,可以让data/props依赖inject,所以需要将初始化inject放在初始化data/props的前面。
  通过前面的介绍我们得知,通过provide注入的内容可以被所有子孙组件通过inject得到。
  很明显,初始化inject,就是使用inject配置的key从当前组件读取内容,读不到则读取它的父组件,以此类推。它是一个自底向上获取内容的过程,最终将找到的内容保存到实例(this)中,这样就可以直接在this上读取通过inject导入的注入内容。
  初始化inject的方法叫做initInjections,代码如下:

export function initInjections (vm) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    observerState.shouldConvert = false;
    Object.keys(result).forEach(key => {
      defineReactive(vm, key, result[key])
    })
    observerState.shouldConvert = true;
  }
} 

其中,resolveInject函数的作用是通过用户配置的inject, 自底向上搜索可用的注入内容,并将搜索结果返回。上面的代码将注入结果保存到result变量中。
  接下来,循环result并依次调用defineReactive函数将它们设置到Vue.js实例上。
  代码中有一个细节需要注意,在循环注入内容前,有一行代码是;

observerState.shouldConvert = false;

在循环结束后,有一行代码是:

observerState.shouldConvert = true;

其作用是通知defineReactive函数不要将内容转换成响应式。其原理也很简单,在将值转换成响应式之前,判断observerstate. shouldconvert属性即可。
  接下来,我们主要看resolveInject的实现原理,它是如何自底向上搜索可用的注入内容"的呢?
  事实上,实现这个功能的主要思想是:读出用户在当前组件中设置的injectkey,然后循环key,将每一个key从当前组件起,不断向父组件查找是否有值,找到了就停止循环,最终将所有key对应的值一起返回即可。
  按照上面的思想,resolveInject函数最初的代码时下面这样的:

  export function resolveInject (inject, vm) {
    if (inject) {
      const result = Object.create(null);
      // do things
      return result;
    }
  }

  第一步要做的事情是获取injectkey, provide/inject可以支持symbol,但它只在原·生支持SymbolReflect.ownkeys的环境下才可以工作,所以获取key需要考虑到Symbol的情况,此时代码如下:

  export function resolveInject (inject, vm) {
    if (inject) {
      const result = Object.create(null);
      const keys = hasSymbol
          ? Reflect.ownKeys(inject).filter(key => {
            return Object.getOwnPropertyDescriptor(inject, key).enumerable
          })
          : Object.keys(inject);
      return result;
    }
  }

  如果浏览器原生支持Symbol,那么使用Reflect.ownkeys读取出inject的所有key;如果浏览器原生不支持Symbol,那么使用object.keys获取key,其区别是Reflect.ownkeys可以读取Symbol类型的属性,而object.keys读不出来。由于通过Reflect.ownkeys读出的key包括不可枚举的属性,所以代码中需要使用filter将不可枚举的属性过滤掉。
  Reflect.ownkeys有一个特点,它可以返回所有自有属性的键名,其中字符串类型和Symbol类型都包含在内。而object.getOwnPropertyNamesobject.keys返回的结果不会包含Symbol类型的属性名, object.getOwnPropertySymbols方法又只返回symbol类型的属性
  所以,如果浏览器原生支持symbol,那么Reflect.ownkeys是比较符合我们目标的一个API,它的返回值会包含所有类型的属性名,我们唯一需要做的事就是使用filter将不可枚举的属性过滤掉。
  如果浏览器元素不支持Symbol,那么object.keys是比较符合目标的API,因为它仅返回自身可枚举的全部属性名,而object. getownpropertyNames会把不可枚举的属性名也返回。
  得到了用户设置的inject的所有属性名之后,就可以循环这些属性名, 自底向上搜索值。这可以使用while循环实现,其代码如下:

  export function resolveInject (inject, vm) {
    if (inject) {
      const result = Object.create(null);
      const keys = hasSymbol
          ? Reflect.ownKeys(inject).filter(key => {
            return Object.getOwnPropertyDescriptor(inject, key).enumerable
          })
          : Object.keys(inject);
      for (let i = 0; i < keys.length; i++) {
        const key = key[i];
        const provideKey = inject[key].from;
        let source = vm;
        while (source) {
          if (source._provided && provideKey in source._provided) {
            result[key] = source._provided[provideKey]
            break;
          }
          source = source.$parent;
        }
      }
      return result;
    }
  }

  在上述代码中,最外层使用for循环key,在循环体内可以依次得到每一次key值,并通过from属性得到provide源属性。然后通过源属性使用while循环来搜索内容。最开始source等于当前组件实例,如果原始属性在source_provided中能找到对应的值,那么将其设置到result中,并使用break跳出循环。否则,将source设置为父组件实例进行下一轮循环,以此类推。
注意:当使用provide注入内容时,其实是将内容注入到当前组件实例的_provide中,所以inject可以从父组件实例的_provide中获取注入的内容。通过这样的方式,最终会在祖先组件中搜索到inject中设置的所有属性的内容。
  inject其实还支持数组的形式,如果用户将inject的值设置为数组,那么inject中是没有from属性的,此时这个逻辑是不是有问题?
  其实是没问题的,因为当Vue.js被实例化时,会在上下文(this)中添加$options属性,这会把用户提供的数据规格化,其中就包括inject
  也就是说, Vue.js在实例化的第一步是规格化用户传入的数据,如果inject传递的内容是数组,那么数组会被规格化成对象并存放在from属性中。
  例如,用户设置的inject是这样的:

{
	inject: [foo]
}

被规格化之是下面这样的:

{
	inject: {
		foo: {
			from: 'foo'
		}	
	}
}

  不论是数组形式还是对象中使用from属性的形式,本质上其实是让用户设置原属性名与当,前组件中的属性名。如果用户设置的是数组,那么就认为用户是让两个属性名保持一致。
  现在,我们就可以搜索所有祖先组件注入的内容了。但是通过前面的介绍,我们知道inject是支持默认值的。也就是说,在所有祖先组件实例中都搜索不到注入内容时,如果用户设置了默,认值,那么将使用默认值。
  要实现这个功能,我们只需要在while循环结束时,判断source是否为false,相关代码如下:

  export function resolveInject (inject, vm) {
    if (inject) {
      const result = Object.create(null);
      const keys = hasSymbol
          ? Reflect.ownKeys(inject).filter(key => {
            return Object.getOwnPropertyDescriptor(inject, key).enumerable
          })
          : Object.keys(inject);
      for (let i = 0; i < keys.length; i++) {
        const key = key[i];
        const provideKey = inject[key].from;
        let source = vm;
        while (source) {
          if (source._provided && provideKey in source._provided) {
            result[key] = source._provided[provideKey]
            break;
          }
          source = source.$parent;
        }
        if (!source) {
          if ('default' in inject[key]) {
            const provideDefault = inject[key].default;
            result[key] = typeof provideDefault === 'function' 
              ? provideDefault.call(vm)
              : provideDefault
          } else if (process.env.NODE_ENV !== 'production') {
            warn(`Injection "${key}" not found`, vm)
          }
        }
      }
      return result;
    }
  }

  上面代码新增了默认值相关的逻辑,如果!sourcetrue,那么判断inject [key]中是否存在default属性。如果存在,则当前key的结果是默认值。这里有一个细节需要注意,那就是默认值支持函数,所以需要判断默认值的类型是不是函数,是则执行函数,将函数的返回值设置给result [key]
  如果inject [key]中不存在default属性,那么会在非生产环境下的控制台中打印警告。

初始化状态

  当我们使用Vuejs开发应用时,经常会使用一些状态,例如props, methods, data, computedwatch。在Vue.js内部,这些状态在使用之前需要进行初始化。本节将详细介绍初始化这些状态的内部原理。
  通过本节的学习,我们将理解什么是props,为什么methods中的方法可以通过this访问,dataVue.js内部是什么样的, computed是如何工作的,以及watch的原理等。
initState函数的代码如下:

export function initState (vm) {
  vm._watchers - [];
  const opts = vm.$options;
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts, methods);
  if (opts.data) {
    initData(vm)
  } else {
    observer(vm._data = {}, true /* asRootData */)
  }
  if (obs.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

  在上面的代码中,首先在vm上新增一个属性_watchers,用来保存当前组件中所有的watcher实例。无论是使用vm.$watch注册的watcher实例还是使用watch选项添加的watcher实例,都会添加到vm._watchers中。
  在之前介绍过,可以通过vm.watchers得到当前Vue.js实例中所注册的所有watcher实例,并将它们依次卸载。
  接下来要做的事情很简单,先判断vm.$options中是否存在props属性,如果存在,则调用initProps初始化props.
  然后判断vm.$options中是否存在methods属性,如果存在,则调用initMethods初始化methods。接着判断vm.$options中是否存在data属性:如果存在,则调用initData初始化data;如果不存在,则直接使用observe函数观察空对象。
注意observer函数的作用是将数据转换成响应式的。
  data初始化之后,会判断vm.$options中是否存在computed属性,如果存在,则调用,initComputed初始化computed。最后判断vm.$options中是否存在watch属性,如果存在,则调用initwatch初始化watch.
  用户在实例化Vue.js时使用了哪些状态,哪些状态就需要被初始化,没有用到的状态则不用初始化。例如,用户只使用了data,那么只需要初始化data即可。如果你足够细心,就会发现初始化的顺序其实是精心安排的。先初始化props,后初始化data,这样就可以在data中使用props中的数据了。在watch中既可以观察props,也可以观察data,因为它是最后被初始化的。
  下图给出了初始化状态的结构图。初始化状态可以分为5个子项,分别是初始化props、初始化methods、初始化data、初始化computed和初始化watch,下面我们将分别针对这5个子项进行详细介绍。
在这里插入图片描述

初始化props

  props的实现原理大体上是这样的:父组件提供数据,子组件通过props字段选择自己需要哪些内容,Vue.js内部通过子组件的props选项将需要的数据筛选出来之后添加到子组件的上下文中。
  为了更清晰地理解props的原理,我们简单介绍Vuejs组件系统的运作原理。
  事实上, Vue.js中的所有组件都是Vue.js实例,组件在进行模板解析时,会将标签上的属性解析成数据,最终生成渲染函数。而渲染函数被执行时,会生成真实的DOM节点并渲染到视图中。但是这里面有一个细节,如果某个节点是组件节点,也就是说模板中的、某个标签的名字是组件名,那么在虚拟DOM渲染的过程中会将子组件实例化,这会将模板解析时从标签属性上解析出的数据当作参数传递给子组件,其中就包含props数据。

  1. 规格化props
      子组件被实例化时,会先对props进行规格化处理,规格化之后的props为对象的格式。
    说明:props可以通过数组指定需要哪些属性。但在Vue.js内部,数组格式的props将被规格化成对象格式。
    规格化props代码如下:
function normalizeProps (options, vm) {
  const props = options.props;
  if (!props) return;
  const res = {};
  let i, val, name;
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type:null};
      }
    }
  }else if (isPlianObject(props)) {
    for (const key in props) {
      val = props[key];
      name = camelize(val);
      res[name] = isPlianObject(val)
        ? val
        : { type: val}
    }
  }else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, `+
      `but got ${toRawType(props)}. `,
      vm
    )
  }
  options.props = res;
}

  在上述代码中,首先判断是否有props属性,如果没有,说明用户没有使用props接收任何数据,那么不需要规格化,直接使用return语句退出即可。然后声明了一个变量res,用来保存规格化后的结果。
  随后是规格化props的主要逻辑。先检查props是否为一个数组。如果不是,则调用isPlainobject函数检查它是否为对象类型,如果都不是,那么在非生产环境下在控制台中打印警告。如果props是数组,那么通过while语句循环数组中的每一项,判断props名称的类型是否是String类型:如果不是,则在非生产环境下在控制台中打印警告;如果是,则调用camelize函数将props名称驼峰化,即可以将a-b这样的名称转换成aB.
  也就是说,如果在父组件的模板中使用这样的语法:

<child user-name="mgd"></child>

  那么在子组件的props选项中需要使用userName:

{
	props: ['usreName']
}

  而使用user-name是不行的。例如:下面这样设置props选项是无法得到props数据的:

{
	props: ['usre-name']
}

  随后将props名当作属性,设置到res中,值为{type:null}:

if (Array.isArray(props)) {
  i = props.length;
  while (i--) {
    val = props[i];
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = {type: null};
    }else if (process.env.NODE_ENV !== 'production') {
      warn ('porps must be strings when using array syntax.')
    }
  }
}

  总结一下,上面做的事情是将Array类型的props规格化成Object类型。
  如果props的类型不是Array而是Object,那么根据props的语法可以知道,props对象中的值可以是一个基础的类型函数,例如:

{
	propA: Number
}

  也有可能是一个数组,提供多个可能的类型,例如:

{
	propB: [String, Number]
}

  还可能是一个对象类型的高级选项,例如:

{
	porpC: {
		type: String,
		required: true
	}
} 

  所以代码中的逻辑是使用for...in语句循环props
  在循环中得到keyval之后,判断val的类型是否是object:如果是,则在res上设置key为名的属性,值为val;如果不是,那么说明val的值可能是基础的类型函数或者是一个数组提供多个可能的类型。那么在res上设置key为名、值为{type: val}的属性,代码如下:

if (isPlianObject(props)) {
  for (const key in props) {
    val = props[key];
    name = camelize(val);
    res[name] = isPlianObject(val)
      ? val
      : { type: val}
  }
}

  规格化之后的props的类型既有可能是基础的类型函数,也有可能是数组。在这后面断言props是否有效时会用到。
2. 初始化props
  正如前面我们介绍的,初始化props的内部原理是:通过规格化之后的props从其父组件传入的props数据中或从使用new创建实例时传入的propsData参数中,筛选出需要的数据保存在vm._props中,然后在vm上设置一个代理,实现通过vm.x访问vm.props.x的目的。
  初始化props的方法叫作initProps,其代码如下:

function initProps (vm, propsOptions) {
  const propsData = vm.$options.propsData || {};
  const props = vm._props = {};
  // 缓存props的值
  const key = vm.$options._propKey = [];
  const isRoot = !vm.$parent;

  // root实例的props属性应该被转换成响应式数据
  if (!isRoot) {
    toggleObserving(false);
  }
  for (const key in propsOptions) {
    keys.push(key);
    const value = ValidateProp(key, propsOptions, propsData, vm);
    defineReactive(props, key, value);
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  } 
  toggleObserving(true);
}

  initProps函数接收两个参数:vmpropsOptions,前者是Vue.js实例,后者是规格化之后的props选项。
  随后在函数中声明了4个变量propsData, props, keysisRoot。变量propsData中保存的是通过父组件传入或用户通过propsData传入的真实props数据。变量props是指向vm._props的指针,也就是所有设置到props变量中的属性最终都会保存到vm.props中。变量keys是指向vm.$options._propkeys的指针,其作用是缓存props对象中的key,将来更新props时只需要遍历vm.$options._propkeys数组即可得到所有propskey,变量isRoot的作用是判断当前组件是否是根组件。
  接下来,会判断当前组件是否是根组件,如果不是,那么不需要将props数据转换成响应式数据。这里toggleObserving函数的作用是确定并控制defineReactive函数调用时所传入的value参数是否需要转换成响应式的。toggleObserving是一个闭包函数,所以能通过调用它并传入一个参数来控制observer/index.js文件的作用域中的变量shouldObserve。这样当数据将要被转换成响应式时,可以通过变量shouldObserve来判断是否需要将数据转换成响应式的。
  然后循环propsOptions,在循环体中先将key添加到keys中,然后调用validateProp函数将得到的props数据通过defineReactive函数设置到vm._props中。
  最后判断这个keyvm中是否存在,如果不存在,则调用proxy,在vm上设置一个以key为属性的代理,当使用vm[key]访问数据时,其实访问的是vm._props [key].
  关于proxy函数,会在之后介绍它的内部原理。
  这里的重点是validateProp函数是如何获取props内容的。validateProp的代码如下:

export function ValidateProp (key, propOptions, propsData, vm) {
  const prop = propOptions[key];
  const absent = !hasOwn(propsData, key);
  let value = propsData[key];
  // 处理布尔类型的props
  if (isType(Boolean, prop.type)) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false;
    }else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {
      value = true;
    }
  }

  // 检查默认值
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key);
    // 因为默认值是新的数据,所以需要将它转换成响应式的
    const prevShouldConvert = observerState.shouldConvert;
    observerState.shouldConvert = true;
    observe(value);
    observerState.shouldConvert = prevShouldConvert;
  } 
  if (process.env.NODE_ENV !== 'production') {
    assertProp(prop, key, value, vm, absent);
  }
  return value;
}

ValidateProp函数接受如下四个参数:

  • key: propOptions中的属性名
  • propOptions: 子组件用户设置的props选项
  • propsData: 父组件或用户提供的props数据
  • vm: Vue.js上下文实例,this的别名

  函数中先声明3个变量prop, absentvalue,变量prop保存的内容是当前这个keyprop选项。变量absent表示当前的key在用户提供的props选项中是否存在。变量value表示使用当前这个key在用户提供的props选项中获取的数据。也就是说,这3个变量分别保存当前这个keyprop选项、prop数据以及一个布尔值(用来判断prop数据是否存在),事实上,变量value中可能存在正确的值,也有可能不存在。
  函数的剩余代码主要解决特殊情况。首先,解决布尔类型prop的特殊情况。
  先使用isType方法判断proptype属性是否是布尔值,如果是,那么开始处理布尔值类型的prop数据。布尔值的特殊情况比其他类型多,其他类型的propvalue有数据时,不需要进行特殊处理,只有在没有数据的时候检查默认值即可,而布尔值类型的prop有两种额外的场景需要处理。
  一种情况是key不存在,也就是说父组件或用户并没有提供这个数据,并且props选项中也没有设置默认值,那么这时候需要将value设置成false。另一种情况是key存在,但value是空字符串或者valuekey相等。
注意:这里的valuekey相等除了常见的a="a"这种方式的相等外,还包含userName="user-name"这种方式。
  也就是说,在下面这些使用方式下,子组件的prop都将设置为true:

<child name></child>
<child name="name"></child>
<child userName="user-name"></child>

  解决布尔类型prop的特殊情况的代码如下:

if (isType(Boolean, prop.type)) {
  if (absent && !hasOwn(prop, 'default')) {
    value = false;
  }else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {
    value = true;
  }
}

  这里的hyphenate函数会将key进行驼峰转换,也就是说userName转换完之后是user-name,所以属性为userName的值如果是user-name,那么也会将value设置为true.
  所以当子组件props选项中的userName属性为布尔类型时,其实下面这种情况也会将value设置为true:

<child user-name="user-name"><child>

  除了布尔值需要特殊处理之外,其他类型的prop只需要处理一种情况,并不需要进行额外的特殊处理。那就是如果子组件通过props选项设置的keyprops数据中并不存在,这时props选项中如果提供了默认值,则需要使用它,并将默认值转换成响应式数据。代码如下:

if (value === undefined) {
  value = getPropDefaultValue(vm, prop, key)
  // 因为默认值的是新数据,所以需要将它转换 成响应式的
  const prevShouldObserve = shouldObserve;
  toggleObserving(true);
  observe(value);
  toggleObserving(prevShouldObserve);
}

  这里使用getPropDefaultvalue函数获取prop的默认值,随后使用observe函数将获取的默认值转换成响应式的。而toggleobserving函数可以决定observer被调用时,是否会将value转换成响应式的。因此,代码中先使用toggleobserving(true),然后调用observe,再调用toggleobserving (prevShouldobserve)将状态恢复成最初的状态。
  随后,会在validateProp函数中判断当前运行环境是否是生产环境,如果不是,会调用assertProp来断言prop是否有效:

if (process.env.NODE_ENV !== 'production') {
  assertProp(prop, key, value, vm, absent)
}

assertProp函数的代码如下:

function assertProp (prop, name, value, vm, absent) {
  if (prop.required && observe) {
    warn ( 
      'Missing required prop: "'+ name +'"',
      vm
    )
    return;
  }
  if (value == null && !prop.required) {
    return;
  }
  let type = prop.type;
  let valid = !type || type ===true;
  const expectedTypes = [];
  if (type) {
    if (!Array.isArray(type)) {
      type = [type];
    }
    for (let i=0; i < type.length && !valid; i++) {
      const assertedType = assertedType(value, type[i]);
      expectedTypes.push(assertedType.expectedType || '');
      valid = assertedType.valid;
    }
  }
  if (!valid) {
    warn (
      `Invalid prop: type check failed for prop "${name}".` + 
      `Expected ${expectedTypes.map(capitalize).join(', ')}` + 
      `, got ${toRawType(value)}.`,
      vm
    )
    return;
  }
  const validator = prop.validator;
  if (validator) {
    if (!validator(value)) {
      warn (
        'Invalid prop: custom validator check failed for prop "' + name +'".',
        vm
      )
    }
  }
}

  虽然assertProp函数的代码看起来有点长,但其实逻辑并不复杂。首先它接收5个参数,分别是propnamevaluevmabsent,它们的含义如下:

参数含义
propprop选项
nameprops中prop选项的key
valueprop数据(propData)
vm上下文(this)
absentprop属性不存在key属性

  这个函数最先处理必填项,如果prop中设置了必填项( required为true )并且prop数据中没有这个key属性,那么在控制台输出警告,并使用return语句终止函数运行。这里prop.required表示prop选项中设置了必填项, absent表示该数据不存在。
  随后处理没有设置必填项并且value不存在的情况,这种情况是合法的,直接返回undefined即可。这里有一个技巧,即value == null用的是双等号。在双等号中, nullundefined是相等的,也就是说valuenullundefined都会为true.
  接下来校验类型,其中声明了3个变量typeexpectedTypesvalid, type就是prop中用来校验的类型, valid表示是否校验成功。
  通常情况下, type是一个原生构造函数或一个数组,或者用户没提供type。如果用户提供了原生构造函数或者数组,因为!type的缘故,变量valid默认等于false;如果用户没设置type.那么valid默认等于true,即当作校验成功处理。
  但有一种特例,那就是当type等于true的时候。Vue.jsprops支持这样的语法props: { somep rop: true },这说明prop一定会校验成功。所以当这种语法出现的时候,由于type===true,所以valid变量的默认值就是true.
  变量expectedTypes是用来保存type的列表,当校验失败,在控制台打印警告时,可以将变量expectedTypes中保存的类型打印出来。
  接下来将校验类型。如果用户提供了type,那么判断type是否是一个数组,如果不是,就将它转换成数组
  接下来循环type数组,并调用assertType函数校验value, assertType函数校验后会返回一个对象,该对象有两个属性validexpectedType,前者表示是否校验成功,后者表示类型,例如: {valid: true, expectedType: "Boolean"}
  然后将类型添加到expectedTypes中,并将valid变量设置为assertedType.valid.
  当循环结束后,如果变量validtrue,就说明校验成功。循环中的条件语句有这样一句话: !valid,即type列表中只要有一个校验成功,循环就结束,认为是成功了。
  现在已经校验完毕,接下来只需要判断validfalse时在控制台打印警告即可。
  可以看到,此时会将expectedTypes打印出来,但是在打印之前先使用map将数组重新调整了一遍,而capitalize函数的作用是将字符串的一个字母改成大写。
  我们知道, prop支持自定义验证函数,所以最后要出来自定义验证函数。在代码中,首先判断用户是否设置了validator,如果设置了,就执行它,否则调用warn函数在控制台打印警告。
  当prop断言结束后,我们回到validateProp函数,执行了最后一行代码,将value返回。

初始化methods

  初始化methods时,只需要循环选项中的methods对象,并将每个属性依次挂载到vm上即可,相关代码如下:

function initMethods (vm, methods) {
  const props = vm.$options.props;
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        warn (
          `Mthods "${key}" has an undefined value in the component definition. ` + 
          `Did you reference the funciton correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn (
          `Nethod "${key}" has already been defined as a prop.`,
          vm
        ) 
      }
      if ((key in vm) && isReserved(key)) {
        warn (
          `Method "${key}" conflicts with an existing Vue instance method. ` + 
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = methods[key] == null ? noop :bind(methods[key], vm)
  }
}

  这里先声明一个变量props,用来判断methods中的方法是否和props发生了重复,然后使用for...in语句循环methods对象。
  在循环中,主要逻辑分为两部分:

  • 校验方法是否合法
  • 将方法挂载到vm
  1. 校验方法是否合法
      在循环中会判断执行环境,在非生产环境下需要校验methods并在控制台发出警告。
      当methods的某个方法只有key没有value时,会在控制台发出警告。如果methods中的·某个方法已经在props中声明过了,会在控制台发出警告。如果methods中的某个方法已经存在于vm中,并且方法名是以$_开头的,也会在控制台发出警告。这里isReserved函数的作用是判断字符串是否是以$_开头。
  2. 将方法挂载到vm
      将方法赋值到vm中很简单,详见initMethods方法的最后一行代码。其中会判断方法(methods [key])是否存在:如果不存在,则将noop赋值到vm[key]中;如果存在,则将该方法通过bind改写它的this后,再赋值到vm[key]中。
      这样,我们就可以通过vm.x访问到methods中的x方法了。

初始化data

  提到data,相信大家都不陌生,我们在使用Vue.js开发项目的过程中经常会用它来保存一·此数据。那么, data内部究竟是怎样的呢?
  简单来说, data中的数据最终会保存到vm._data中。然后在vm上设置一个代理,使得通过vm.x可以访问到vm.data中的 属性。最后由于这些数据并不是响应式数据,所以需要调用observe函数将data转换成响应式数据。于是, data就完成了初始化。
  但在真正的代码中,需要增加判断一些条件,如果发现data的使用方式不正确,那么会在控制台打印出警告。初始化data的代码如下:

 function initData () {
  let data  = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlianObject(data)) {
    data = {};
    process.env.NODE_ENV !== 'production' && warn (
      `data functions should return an object:\n` + 
      `https://vuejs.org/v2/guide/componment.html#data-Must-Be-a-Function`,
      vm
    )
  }
  // 将data代理到Vue.js实例上
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods,key)) {
        warn (
          `Methods "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn (
        `The data property "${key}" is already declared as a prop. ` + 
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // 观察数据
  observer(data, true /* asRootData */)
 }

  在上述代码中,我们首先从选项中得到data,并将其保存在data变量中。然后需要判断data的类型,如果是函数,则需要执行函数并将返回值赋值给变量datavm.data。这里我们并没有见到函数data被执行,而是看到了函数getData被执行。其实,函数getData中的逻辑也是调用data函数并将值返回,只不过getData中有一些细节处理,比如使用try...catch语句捕获data函数中有可能发生的错误等。
  最终得到的data值应该是object类型,否则就在非生产环境下在控制台打印出警告,并为data设置默认值,也就是空对象。
  接下来要做的事情是将data代理到实例上。代码中首先声明了3个变量: keys, propsmethods。接着循环data,其中先判断当前执行环境,如果不是生产环境,那么判断当前循环的key是否存在于methods中,如果存在,说明数据重复了,在控制台打印警告。然后以同样的方式判断props中是否存在某个属性与key相同,如果发现确实有相同的属性,那么在非生产环境下在控制台打印警告。
  只有props中不存在当前与key相同的属性时,才会将属性代理到实例上,前提是属性名不能以$_开头。如果data中的某个keymethods发生了重复,依然会将data代理到实例中,但如果与props发生了重复,则不会将data代理到实例中。
  代码中调用了proxy函数实现代理功能。该函数的作用是在第一个参数上设置一个属性名为第三个参数的属性。这个属性的修改和获取操作实际上针对的是与第二个参数相同属性名的属性。proxy的代码如下:

 const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
 }
 export function proxy (target, sourceKey, key) {
   sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key];
   }
   sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
   }
   Object.defineProperty(target, key, sharedPropertyDefinition)
 }

  这里先声明了一个变量sharedPropertyDefinition作为默认属性描述符。接下来声明了proxy函数,此函数接收3个参数: targetsourcekeykey。随后在代码中设置了getset属性,相当于给属性提供了gettersetter方法。在getter方法中读取了this[sourcekey][key],在setter方法中设置了this [sourcekey] [key]属性。最后,使用object.defineProperty方法为target定义一个属性,属性名为key,属性描述符为sharedPropertyDefinition
  通过这样的方式将vm._data中的方法代理到vm上。所有属性都代理后,执行observe函,数将数据转换成响应式的。关于如何将数据转换成响应式数据,在之前介绍过。

初始化computed

  大家肯定对计算属性computed不陌生,在实际项目中我们会经常用它。但对于刚入门的新手来说,它不是很好理解,它和watch到底有哪些不同呢?本节将详细介绍其内部原理。
  简单来说, computed是定义在vm上的一个特殊的getter方法。之所以说特殊,是因为在vm上定义getter方法时, get并不是用户提供的函数,而是Vuejs内部的一个代理函数。在代理函数中可以结合watcher实现缓存与收集依赖等功能。
  我们知道计算属性的结果会被缓存,且只有在计算属性所依赖的响应式属性或者说计算属性的返回值发生变化时才会重新计算。那么,如何知道计算属性的返回值是否发生了变化?这其实是结合watcherdirty属性来分辨的:当dirty属性为true时,说明需要重新计算“计算属性”的返回值;当dirty属性为false时,说明计算属性的值并没有变,不需要重新计算。
  当计算属性中的内容发生变化后,计算属性的Watcher与组件的Watcher都会得到通知。计算属性的Watcher会将自己的dirty属性设置为true,当下一次读取计算属性时,就会重新计算一次值。然后组件的watcher也会得到通知,从而执行render函数进行重新渲染的操作。由于要重新执行render函数,所以会重新读取计算属性的值,这时候计算属性的watcher已经把自己的dirty属性设置为true,所以会重新计算一次计算属性的值,用于本次渲染。
  简单来说,计算属性会通过Watcher来观察它所用到的所有属性的变化,当这些属性发生变化时,计算属性会将自身的watcherdirty属性设置为true,说明自身的返回值变了。
  下图给出了计算属性的内部原理。在模板中使用了一个数据渲染视图时,如果这个数据恰好是计算属性,那么读取数据这个操作其实会触发计算属性的getter方法(初始化计算属性时在vm上设置的getter方法)。
在这里插入图片描述
这个getter方法被触发时会做两件事。

  • 计算当前计算属性的值,此时会使用watcher去观察计算属性中用到的所有其他数据的变化。同时将计算属性的watcherdirty属性设置为false,这样再次读取计算属性时将不再重新计算,除非计算属性所依赖的数据发生了变化。
  • 当计算属性中用到的数据发生变化时,将得到通知从而进行重新渲染操作。

注意:如果是在模板中读取计算属性,那么使用组件的Watcher观察计算属性中用到的所有数据的变化。如果是用户自定义的watch,那么其实是使用用户定义的watcher观察计算属性中用到的所有数据的变化。其区别在于当计算属性函数中用到的数据发生变化时,向谁发送通知。
  以上两件事做完后,就可以实现当数据发生变化时计算属性清楚缓存,组件收到通知去重新渲染视图。
说明:计算属性的一个特点是有缓存。计算属性函数所依赖的数据在没有发生变化的情况下,会反复读取计算属性,而计算属性并不会反复执行。
初始化计算属性的具体实现:

 const computedWatcherOptions = {lazy: true};

 function initComputed (vm, computed) {
  const watchers = vm._comoutedWatcher = Object.create(null);

  // 计算属性在SSR环境中,只是一个普通的getter方法
  const isSSR = isServerRendering();
  for ( const key in computed) {
    const userDef = computed[key];
    const getter = typeof userDef === 'function' ? userDef : userDef.get;
    if (process.env.NODE_ENV !== 'production' && getter == null ) {
      warn (
        `Getter is missing for computed property "${key}".`,
        vm
      )
    } 
    // 在非SSR环境中,为计算属性创建内部观察器
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn (
          `The computed property "${key}" is already defined in data.`,
          vm
        )
      }else if (vm.$options.props && key in vm.$options.props) {
        warn (
          `The computed property "${key}" is already defined as a prop.`,
          vm
        ) 
      }
    }
  }
 }

  在上述代码中,我们先声明了一个变量computedWatcherOptions,其作用和它的名字相同,是一个Watcher选项。在实例化Watcher时,通过参数告诉Watcher类应该生成一个供计算属性使用的watcher实例。
  initComputed函数的作用是初始化计算属性,它接受两个参数:

  • vm:Vue.js实例上下文(this)
  • computed: 计算属性对象

  随后在vm上新增了_computedWatchers属性并且声明了变量watchers,其值为一个空的,对象,而_computedwatchers属性用来保存所有计算属性的watcher实例。
说明Object. create(null)创建出来的对象没有原型,它不存在_proto_属性。
  随后声明的变量isSSR用于判断当前运行环境是否是SSR(服务端渲染), isServerRendering工具函数执行后,会返回一个布尔值用于判断是否是服务端渲染环境。
  接下来,使用for..in循环computed对象,依次初始化每个计算属性。在循环中先声明变量userDef来保存用户设置的计算属性定义,然后通过userDef获取getter函数。这里只需要判断用户提供的计算属性是否是函数,如果是函数,则将这个函数当作getter,否则默认将用户提供的计算属性当作对象处理,获取对象的get方法。这时如果用户传入的计算属性不合法也就是说既不是函数,也不是对象,或者提供了对象但没有提供get方法,就在非生产环境下在控制台打印警告以提示用户。
  随后判断当前环境是否是服务端渲染环境,如果不是,就需要创建watcher实例。Watcher在整个计算属性内部原理中非常重要,后面我们会介绍它的作用。创建watcher实例时有一个细节需要注意,即第二个参数的getter其实是用户设置的计算属性的get函数。
  最后,判断当前循环到的计算属性的名字是否已经存在于vm中:如果存在,则在非生产环境下的控制台打印警告,如果不存在,则使用definecomputed函数在vm上设置一个计算属性。这里有一个细节需要注意,那就是当计算属性的名字已经存在于vm中时,说明已经有了一个重名的data或者props,也有可能是与methods重名,这时候不会在vm上定义计算属性。
  但在Vue.js中,只有与dataprops重名时,才会打印警告。如果与methods重名,并不会在控制台打印警告。所以如果与methods重名,计算属性会悄悄失效,我们在开发过程中应该尽量避免这种情况。此外,还需要说明一下defineComputed函数,它有3个参数: vmkeyuserDef。其完整代码如下:

 const sharePropertyDefinition = {
   enumerable: true,
   configurable: true,
   get: noop,
   set: noop
 }
 export function defineComputed (target, key, userDef) {
   const shouldCache = !isServerRendering();
   if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop;
   } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
   }
   if (process.env.NODE_ENV !== 'production' && 
        sharePropertyDefinition.set === noop ) {
      sharePropertyDefinition.set = function () {
        warn (
          `Computed property "${key}" was assigned to but is has no setter.`,
          this
        )
      }
   }
   Object.defineProperty(target, key, sharePropertyDefinition)
 } 

  在上述代码中,先定义了变量sharedPropertybefinition,它的作用与之前介绍的proxy函数所使用的sharedPropertyDefinition变量相同。事实上,在源码中,这两个函数使用的其实是同一个变量,这个变量是一个默认的属性描述符,它经常与Object.defineProperty配合使用。
  接着,函数defineComputed接收3个参数target, keyuserDef,其意思是在target上定义一个key属性,属性的gettersetter根据userDef的值来设置。
  然后函数中声明了变量shouldCache,它的作用是判断computed是否应该有缓存。这里调用isServerRendering函数来判断当前环境是否是服务端渲染环境。因此,变量shouldCache只有在非服务端渲染环境下才为true。也就是说,只有在非服务端渲染环境下,计算属性才有缓存
  接下来,判断userDef的类型。Vuejs支持用户设置两种类型的计算属性:函数和对象。例如

  var vm = new Vue({
    data: {a:1},
    computed: {
      // 仅读取
      aDouble: function () {
        return this.a * 2
      },
      // 读取和设置
      aPlus: {
        get: function () { 
          return this.a + 1
        },
        set: function () {
          this.a = v - 1
        }
      }
    }
  })

  所以在定义计算属性时,需要判断userDef的类型是函数还是对象。如果是函数,则将函数理解为getter函数。如果是对象,则将对象的get方法作为getter方法, set方法作为setter方法。
  这里有一个细节需要注意,我们要通过判断shouldcache来选择将get设置成userDef这种普通的getter函数,还是设置为计算属性的getter函数。其区别是如果将sharedPropertyDefinition.get设置为userDef函数,那么这个计算属性只是一个普通的getter方法,没有缓存。当计算属性中所使用的数据发生变化时,计算属性的Watcher也不会得到任何通知,使用计算属性的watcher也不会得到任何通知。它就是一个普通的getter,每次读取操作都会执行一遍函数。这种情况通常在服务端渲染环境下生效,因为数据响应式的过程在服务器上是多余的。如果将sharedPropertybefinition. get设置为计算属性的getter,那么计算属性将具备缓存和观察计算属性依赖数据的变化等响应式功能。稍后,我们再介绍createComputedGetter的实现。
  由于用户并没有设置setter函数,所以将sharedPropertyDefinition.set设置为noop.而noop是一个空函数,如果userDef的类型不是函数,那么假设它是对象类型。在else语句中先设置sharedPropertyDefinition.get,后设置sharedPropertyDefinition.set。设置sharedPropertyDefinition. get时需要判断userDef.get是否存在。如果不存在,则将sharedPropertyDefinition.get设置成noop。如果存在,那么逻辑和前面介绍的相同,如果shouldCachetrue并且用户没有明确地将userDef.cache设置为false,则调用createComputedGetter函数将sharedPropertyDefinition. get设置成计算属性的getter函数,否则将sharedPropertyDefinition.get设置成普通的getter函数userDef.get
  设置完getter后设置setter。这简单很多,只需要判断userDef. set是否存在,如果存在,则将sharedPropertyDefinition. set设置为userDef.set,否则设置为noop.如果用户在没有设置setter的情况下对计算属性进行了修改操作, Vue.js会在非生产环境,下在控制台打印警告。其实现原理很简单,如果用户没有设置setter函数,那么为计算属性设置一个默认的setter函数,并且当函数执行时,打印出警告即可。
  在defineComputed函数的最后,我们调用Object.defineProperty方法在target对象上设置key属性,其中属性描述符为前面我们设置的sharedPropertyDefinition。计算属性就是这样被设置到vm上的。
  通过前面的介绍,我们发现计算属性的缓存与响应式功能主要在于是否将getter方法设置为createcomputedGetter函数执行后的返回结果。下面我们介绍createComputedGetter函数是如何实现缓存以及响应式功能的,其代码如下:

  function createComputedGetter (key) {
    return function computedGetter () {
      const watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate()
        }
        if (Dep.target) {
          watcher.depend()
        }
        return watcher.value;
      }
    }
  }

  这个函数是一个高阶函数,它接收一个参数key并返回另一个函数computedGetter
  通过前面的介绍知道,最终被设置到getter方法中的函数其实是被返回的computedGetter函数。在非服务端渲染环境下,每当计算属性被读取时, computedGetter函数都会被执行。
  在computedGetter函数中,先使用keythis._computedwatchers中读出watcher并赋值给变量watcher。而this._computedwatchers属性保存了所有计算属性的watcher实例。
  如果watcher存在,那么判断watcher.dirty是否为true。前面我们介绍watcher.dirty属性用于标识计算属性的返回值是否有变化,如果它为true,说明计算属性所依赖的状态发生了变化,它的返回值有可能也会有变化,所以需要重新计算得出最新的结果。
  计算属性的缓存就是通过这个判断来实现的。每当计算属性所依赖的状态发生变化时,会将watcher.dirty设置为true,这样当下一次读取计算属性时,会发现watcher.dirtytrue此时会重新计算返回值,否则就直接使用之前的计算结果。
  随后判断Dep.target是否存在,如果存在,则调用watcher.depend方法。这段代码的目的在于将读取计算属性的那个watcher添加到计算属性所依赖的所有状态的依赖列表中。换句话说,就是让读取计算属性的那个watcher持续观察计算属性所依赖的状态的变化。
  使用计算属性的同学大多会有一个疑问:为什么我在模板里只使用了一个计算属性,但是把,计算属性中用到的另一个状态给改了,模板会重新渲染,它是怎么知道自己需要重新渲染的呢?
  这是因为组件的Watcher观察了计算属性中所依赖的所有状态的变化。当计算属性中所依赖的状态发生变化时,组件的Watcher会得到通知,然后就会执行重新渲染操作。
  之前介绍watcher时,并没有介绍其dependevaluate方法。事实上,其中定义了dependevaluate方法专门用于实现计算属性相关的功能,代码如下:

  export default class Watcher {
    constructor (vm, expOrFn, cb, options) {
      // 隐藏无关代码
      if (options) {
        this.lazy = !!options.lazy;
      } else {
        this.lazy = false;
      }

      this.dirty = this.lazy;

      this.value = this.lazy
        ? undefined
        : this.get()
    }

    evaluate () {
      this.value = this.get();
      this.dirty = false;
    }

    depend () {
      let i = this.deps.length;
      while (i--) {
        this.dep[i].depend();
      }
    }
  }

  可以看到, evaluate方法的逻辑很简单,就是执行this.get方法重新计算一下值,然后将this.dirty设置为false.
  虽然depend方法的代码不多,但它的作用并不简单。从代码中可以看到, watcher.depend方法会遍历this.deps属性(该属性中保存了计算属性用到的所有状态的dep实例,而每个属性的dep实例中保存了它的所有依赖),并依次执行dep实例的depend方法。
  执行dep实例的depend方法可以将组件的watcher实例添加到dep实例的依赖列表中。换句话说, this.deps是计算属性中用到的所有状态的dep实例,而依次执行了dep实例的depend方法就是将组件的Watcher依次加入到这些dep实例的依赖列表中,这就实现了让组件的Watcher观察计算属性中用到的所有状态的变化。当这些状态发生变化时,组件的watcher会收到通知,从而进行重新渲染操作。
  前面我们介绍的计算属性原理是Vue.js在2.5.2版本中的实现。Vue.js在2.5.17版本中,对计算属性的实现方式做了一个改动,这个改动使得计算属性的原理有一些不太一样的地方,这是因为现有的计算属性存在着一个问题.
  前面我们介绍组件的Watcher会观察计算属性中用到的所有数据的变化。这就导致一个问题:如果计算属性中用到的状态发生了变化,但最终计算属性的返回值并没有变,这时计算属性依然会认为自己的返回值变了,组件也会重新走一遍渲染流程。只不过最终由于虚拟DOM的Dif中发现没有变化,所以在视觉上并不会发现UI有变化,其实渲染函数会被执行。
  也就是说,计算属性只是观察它所用到的所有数据是否发生了变化,但并没有真正去校验它自身的返回值是否有变化,所以当它所使用的数据发生变化后,它就认为自己的返回值也会有变化,但事实并不总是这样。有人在Vue.jsGitHub Issues里提出了这个问题,地址为https://github.com/vuejs/vue/issues/7767,同时,他还给出了一个案例来演示这个问题,地址: https:/sfiddle.net72gzmayL/
  为了解决这个问题,作者把计算属性的实现做了一些改动,改动后的逻辑是:组件的watcher不再观察计算属性用到的数据的变化,而是让计算属性的Watcher得到通知后,计算一次计算属性的值,如果发现这一次计算出来的值与上一次计算出来的值不一样,再去主动通知组件的Watcher进行重新渲染操作。这样就可以解决前面提到的问题,只有计算属性的返回值真的变了,才会重新执行渲染函数。
  下图给出了新版计算属性的内部原理。与之前最大的区别就是组件的watcher不再观察数据的变化了,而是只观察计算属性的watcher (把组件的watcher实例添加到计算属性的watcher实例的依赖列表中),然后计算属性主动通知组件是否需要进行渲染操作。
在这里插入图片描述
  此时计算属性的getter被触发时做的事情发生了变化,它会做下面两件事:

  • 使用组件的Watcher观察计算属性的Watcher,也就是把组件的Watcher添加到计算属性的Watcher的依赖列表中,让计算属性的watcher向组件的watcher发送通知。
  • 使用计算属性的watcher观察计算属性函数中用到的所有数据,当这些数据发生变化时,向计算属性的watcher发送通知。

注意:如果是在模板中读取计算属性,那么使用组件的Watcher观察计算属性的Watcher;如果是用户使用vm.$watch定义的Watcher,那么其实是使用用户定义的Watcher观察计算属性的watcher。其区别是当计算属性通过计算发现自己的返回值发生变化后,计算属性的watcher向谁发送通知。
  修复这个问题的Pull Requests地址为: https://github.com/vuejs/vue/pull/7824,下面来看一下,这个Pull Requests都有哪些修改。首先createComputedGetter函数中的内容发生了变化,改动后的代码如下:

  function createComputedGetter (key) {
    return function computedGetter () {
      const watcher = this._computedWatchers && this._computedWatchers[key]
      if (watcher) {
        watcher.depend()
        return watcher.evaluate()
      }
    }
  }

  改动后的函数依然是一个高阶函数,依然返回computedGetter函数,但是computedGetter函数中的内容发生了变化。从代码上看,改动后的代码比改动前的代码少了很多。
  computedGetter函数依然是先使用keythis._computedwatchers中读出watcher并赋值给变量watcher。随后判断watcher是否存在,如果存在,则执行watcher.depend()watcher.evaluate(),并将watcher. evaluate()的返回值当作计算属性函数的计算结果返回出去
  depend方法被执行后,会将读取计算属性的那个watcher添加到计算属性的watcher的依赖列表中,这可以让计算属性的watcher向使用计算属性的watcher发送通知。
  Watcher的代码变成了下面的样子:

  export default class Watcher {
    constructor (vm, expOrFn, cb, options) {

      if (options) {
        this.computed = !!options.computed;
      } else {
        this.computed = false;
      }
      this.dirty = this.computed;

      if (this.computed) {
        this.value = undefined;
        this.dep = new Dep()
      } else {
        this.value = this.get();
      }
    }
    update () {
      if (this.computed) {
        if (this.dep.subs.length === 0) {
          this.dirty = true;
        } else {
          this.getAndInvoke(()=>{
            this.dep.notify();
          })
        }
      }
    }
    getAndInvoke (cb) {
      const value = this.get();
      if (
        value !== this.value || 
        isObject(value) ||
        this.deep
      ) {
        const oldValue = this.value;
        this.value = value;
        this.dirty = false;
        if (this.user) {
          try {
            cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          cb.call(this.vm, value, oldValue);
        }
      }
    }

    evaluate () {
      if (this.dirty) {
        this.value = this.get();
        this.dirty = false;
      }
      return this.value;
    } 

    depend () {
      if (this.dep && Dep.target) {
        this.dep.depend();
      }
    }
  }

  可以看到, evaluate方法稍微有点改动,但并不是很大。先通过dirty属性判断返回值是否发生了变化,如果发生了变化,就执行get方法重新计算一次,然后将dirty属性设置为false,表示数据已经是最新的,不需要重新计算,最后返回本次计算出来的结果。
  depend方法的改动有点大,这一次不再是将Dep. target添加到计算属性所用到的所有数据的依赖列表中,而是改成了将Dep.target添加到计算属性的依赖列表中。this.dep用于在实例化Watcher时进行判断,如果为计算属性用的Watcher,则实例化一个dep实例并将其放在this.dep 属性上。
  当计算属性中用到的数据发生变化时,计算属性的Watcherupdate方法会被执行,此时会判断当前watcher是不是计算属性的Watcher,如果是,那么有两种模式,一种是主动发送通知,另一种是将dirty设置为true。行业术语中,这两种方式分别叫作activatedlazy.
  从代码中可以看出,分辨这两种模式可以使用依赖的数量, activated模式要求至少有一个依赖。其实也可以理解,如果没有任何依赖,那么主动去向谁发送通知呢?
  大部分情况下都是有依赖的,这个依赖有可能是组件的watcher,这取决于谁读取了计算属性。
  我们假设这个依赖是组件的Watcher,那么当计算属性所使用的数据发生变化后,会执行计算属性的Watcherupdate方法。随后可以看到,发送通知的代码是在this. getAndInvoke函数的回调中执行的。可以很明确地告诉你,这个函数的作用是对比计算属性的返回值。只有计算属性的返回值真的发生了变化,才会执行回调,从而主动发送通知让组件的watcher去执行重新渲染逻辑。

初始化watch

  初始化状态的最后一步是初始化watcher。在initState函数的最后,有这样一行代码:

  if (opts.watch && opts.watch !==nativeWatcher) {
    initWatch(vm, opts.watch);
  }

  只有当用户设置了watch选项并且watch选项不等于浏览器原生的watch时,才进行初始化watch的操作。之所以使用这样的语句(opts.watch !== nativewatch)判断,是因为Firefox浏览器中的object.prototype上有一个watch方法。当用户没有设置watch时,在Firefox浏览器下的opts.watch将是object.prototype.watch函数,所以通过这样的语句可以避免这种问题.
  代码中通过调用initwatch函数并传递两个参数vmopts.watch来初始化watch选项。这里我们先简单回顾watch的使用方式。

  • 类型:{ [key: string]: string | Function | Object |Array }
  • 介绍: 一个对象,其中键是需要观察的表达式,值是对应的回调函数,也可以是方法名或者包含选项的对象。Vue.js实例将会在实例化时调用vm.$watch()遍历watch对象的每一个属性。
  • 示例:
  var vm = new Vue({
    data: {
      a: 1,
      b: 2,
      c: 3,
      d: 4,
      e: {
        f: {
          g: 5
        }
      }
    },
    watch: {
      a: function (val, oldValue) {
        console.log("new: %s, old: %s, val, oldVal");
      },
      // 方法名
      b: 'someMethod',
      // 深度watcher
      c: {
        handler: function (val, oldVal) {/* ... */}
      },
      // 该回调将会在侦听开始之后被立即调用
      d: {
        handler: function (val, oldVal) {/*  */},
        immediate:true
      },
      e: [
        function handle1 (val, oldVal) {/*  */},
        function handle2 (val, oldVal) {/*  */}
      ],
      // watch vm.e.f's value: {g: 5}
      'e.f': function (val, oldVal) {/*  */}
    }
  })
  vm.a = 2 // => new: 2,old: 1

  初始化watch选项的实现思路并不复杂,前面也略微提到了。watch选项的功能和vm.$watch是相同的,所以只需要循环watch选项,将对象中的每一项依次调用vm.$watch方法来观察表达式或computedVue.js实例上的变化即可。
  由于watch选项的值同时支持字符串、函数、对象和数组类型,不同的类型有不同的用法,所以在调用vm.$watch之前需要对这些类型做一些适配。initwatch函数的代码如下:

  function initWatch (vm, watch) {
    for (const key in watch) {
      const handler = watch[key];
      if (Array.isArray(handler)) {
        for (let i=0; i<handler.length; i++) {
          createWatcher(vm, key, handler[i]);
        }
      } else {
        createWatcher(vm, key, handler);
      }
    }
  }

  它接收两个参数vmwatch,后者是用户设置的watch对象。随后使用for...in循环遍历watch对象,通过key得到watch对象的值并赋值给变量handler.
  此时变量handler的类型是不确定的,watch选项的值其实可以大致分为两类:数组和其他。数组中的每一项可以是其他任意类型,所以代码中先处理数组的情况。如果handler的类型是数组,那么遍历数组并将数组中的每一项依次调用createwatcher函数来创建watcher。如果不是数组,那么直接调用createwatcher函数创建一个watcher.
  createwatcher函数主要负责处理其他类型的handler并调用vm.$watch创建watcher观察表达式,其代码如下:

  function createWatcher (vm, expOrFn, handler, $options) {
    if (isPlianObject(handler)) {
      options = handler;
      handler = handler.handler;
    } 
    if (typeof handler === 'string') {
      handler = vm[handler];
    }
    return vm.$watch(expOrFn, handler, options)
  }

  它接受如下4个参数:

  • vm: Vue.js实例上下文
  • expOrFn: 表达式或计算属性函数
  • handler: watch对象的值
  • options: 用于传递给vm.$watch的选项对象。

  执行createwatcher函数时,handler的类型有三种可能:字符串、函数和对象。如果handler的类型是函数,那么不用特殊处理,直接把它传递给vm.$watch即可。如果是对象,那么说明用户设置了一个包含选项的对象,因此将options的值设置为handler,并且将变量handler设置为handler对象的handler方法。如果handler的类型是字符串,那么从vm中取出方法,将它赋值给handler变量即可。
  针对不同类型的值处理完毕后, handler变量是回调函数, optionsvm.$watch的选项,所以接下来只需要调用vm.$watch即可完成初始化watch的任务。

初始化provide

  状态初始化的下一步是初始化provide,本节中我们将介绍provide的内部原理。
  provide选项应该是一个对象或者是返回一个对象的函数。该对象包含可注入其子孙的属·性。在该对象中,你可以使用ES2015 symbol作为key,但是它只在原生支持symbolReflect.ownkeys的环境下工作。初始化provide时,只需要将provide选项添加到vm._provided即可,相关代码如下:

  export function initProvide (vm) {
    const provide = vm.$options.provide;
    if (provide) {
      vm._provide = typeof provide === 'function'
        ? provide.call(vm)
        : provide
    }
  }

  这里首先判断provide的类型是否是函数,如果是,则执行函数,将返回值赋值给vm._provided,否则直接将变量provide赋值给vm.provided.

总结

  本章详细介绍了new Vue()被执行时Vue.js的背后发生了什么。
  Vuejs的整体生命周期可以分为4个阶段:初始化阶段、模板编译阶段、挂载阶段和卸载阶段。初始化阶段结束后,会触发created钩子函数。在created钩子函数与beforeMount钩子函数之间的这个阶段是模板编译阶段,这个阶段在不同的构建版本中不一定存在。挂载阶段在beforeMount钩子函数与mounted期间。挂载完毕后, Vuejs处于已挂载阶段。已挂载阶段会持续追踪状态的变化,当数据(状态)发生变化时, watcher会通知虚拟DOM重新渲染视图。在渲染视图前触发beforeUpdate钩子函数,渲染完毕后触发updated钩子函数。当vm.$destroy被调用时,组件进入卸载阶段。卸载前会触发beforeDestroy钩子函数,卸载后会触发destroyed钩子函数。
  new Vue ()被执行后, Vuejs进入初始化阶段,然后选择性进入模板编译与挂载阶段。在初始化阶段,会分别初始化实例属性、事件、provide/inject以及状态等,其中状态又包含props, methodsdata,computedwatch.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值