作面试的不倒翁:浅谈 Vue 中 computed 实现原理

编者按:咱们会不时邀请工程师谈谈有意思的技术细节,但愿知其因此然能让你们在面试有更出色表现。也给面试官提供更多思路。前端

虽然目前的技术栈已由 Vue 转到了 React,但从以前使用 Vue 开发的多个项目实际经从来看仍是很是愉悦的,Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至我的认为在不少场景使用 Vue 比 React 开发效率更高,以前也有断断续续研读过 Vue 的源码,但一直没有梳理总结,因此在此作一些技术概括同时也加深本身对 Vue 的理解,那么今天要写的即是 Vue 中最经常使用到的 API 之一 computed 的实现原理。vue

基本介绍

话很少说,一个最基本的例子以下:面试

<div id="app">
    <p>{{fullName}}</p>
</div>
复制代码
new Vue({
    data: {
        firstName: 'Xiao',
        lastName: 'Ming'
    },
    computed: {
        fullName: function () {
            return this.firstName + ' ' + this.lastName
        }
    }
})
复制代码

Vue 中咱们不须要在 template 里面直接计算 {{this.firstName + ' ' + this.lastName}},由于在模版中放入太多声明式的逻辑会让模板自己太重,尤为当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性形成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。api

对比侦听器 watch

固然不少时候咱们使用 computed 时每每会与 Vue 中另外一个 API 也就是侦听器 watch 相比较,由于在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,全部依赖这个数据的相关数据或函数都会自动发生变化或调用。数组

虽然计算属性在大多数状况下更合适,但有时也须要一个自定义的侦听器。这就是为何 Vue 经过 watch 选项提供了一个更通用的方法来响应数据的变化。当须要在数据变化时执行异步或开销较大的操做时,这个方式是最有用的。缓存

从 Vue 官方文档对 watch 的解释咱们能够了解到,使用 watch 选项容许咱们执行异步操做(访问一个 API)或高消耗性能的操做,限制咱们执行该操做的频率,并在咱们获得最终结果前,设置中间状态,而这些都是计算属性没法作到的。微信

下面还另外总结了几点关于 computedwatch 的差别:app

  1. computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm 上的数据,因此用 watch 一样能够监听 computed 计算属性的变化(其它还有 dataprops
  2. computed 本质是一个惰性求值的观察者,具备缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而 watch 则是当数据发生变化便会调用执行函数
  3. 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据;

以上咱们了解了 computedwatch 之间的一些差别和使用场景的区别,固然某些时候二者并无那么明确严格的限制,最后仍是要具体到不一样的业务进行分析。异步

原理分析

言归正传,回到文章的主题 computed 身上,为了更深层次地了解计算属性的内在机制,接下来就让咱们一步步探索 Vue 源码中关于它的实现原理吧。函数

在分析 computed 源码以前咱们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历此对象全部的属性,并使用 Object.defineProperty 把这些属性所有转为 getter/setter,这些 getter/setter 对用户来讲是不可见的,可是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每一个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程当中把属性记录为依赖,以后当依赖项的 setter 被调用时,会通知 watcher 从新计算,从而导致它关联的组件得以更新。

Vue 响应系统,其核心有三点:observewatcherdep

  1. observe:遍历 data 中的属性,使用 Object.definePropertyget/set 方法对其进行数据劫持;
  2. dep:每一个属性拥有本身的消息订阅器 dep,用于存放全部订阅了该属性的观察者对象;
  3. watcher:观察者(对象),经过 dep 实现对响应属性的监听,监听到结果后,主动触发本身的回调进行响应。

对响应式系统有一个初步了解后,咱们再来分析计算属性。 首先咱们找到计算属性的初始化是在 src/core/instance/state.js 文件中的 initState 函数中完成的

export function initState (vm: Component) {
  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 {
    observe(vm._data = {}, true /* asRootData */)
  }
  // computed初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
复制代码

调用了 initComputed 函数(其先后也分别初始化了 initDatainitWatch )并传入两个参数 vm 实例和 opt.computed 开发者定义的 computed 选项,转到 initComputed 函数:

const computedWatcherOptions = { computed: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  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
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    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)
      }
    }
  }
}
复制代码

从这段代码开始咱们观察这几部分:

  1. 获取计算属性的定义 userDefgetter 求值函数

    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    复制代码

    定义一个计算属性有两种写法,一种是直接跟一个函数,另外一种是添加 setget 方法的对象形式,因此这里首先获取计算属性的定义 userDef,再根据 userDef 的类型获取相应的 getter 求值函数。

  2. 计算属性的观察者 watcher 和消息订阅器 dep

    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
    )
    复制代码

    这里的 watchers 也就是 vm._computedWatchers 对象的引用,存放了每一个计算属性的观察者 watcher 实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher 均指代同一个意思但注意和 Watcher 构造函数区分),Watcher 构造函数在实例化时传入了 4 个参数:vm 实例、getter求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 {computed:true} 项,代表这是一个计算属性而不是非计算属性的观察者,咱们来到 Watcher 构造函数的定义:

    class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        if (options) {
          this.computed = !!options.computed
        } 
    
        if (this.computed) {
          this.value = undefined
          this.dep = new Dep()
        } else {
          this.value = this.get()
        }
      }
      
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          value = this.getter.call(vm, vm)
        } catch (e) {
          
        } finally {
          popTarget()
        }
        return value
      }
      
      update () {
        if (this.computed) {
          if (this.dep.subs.length === 0) {
            this.dirty = true
          } else {
            this.getAndInvoke(() => {
              this.dep.notify()
            })
          }
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      evaluate () {
        if (this.dirty) {
          this.value = this.get()
          this.dirty = false
        }
        return this.value
      }
    
      depend () {
        if (this.dep && Dep.target) {
          this.dep.depend()
        }
      }
    }
    复制代码

    为了简洁突出重点,这里我手动去掉了咱们暂时不须要关心的代码片断。 观察 Watcherconstructor ,结合刚才讲到的 new Watcher 传入的第四个参数 {computed:true} 知道,对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是建立了该属性的消息订阅器。

    export default class Dep {
      static target: ?Watcher;
      subs: Array<Watcher>;
    
      constructor () {
        this.id = uid++
        this.subs = []
      }
    
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
    
      depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      }
    
      notify () {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    
    Dep.target = null
      
    复制代码

    Dep 一样精简了部分代码,咱们观察 WatcherDep 的关系,用一句话总结

    watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 经过 notify 遍历了 dep.subs 通知每一个 watcher 更新。

  3. defineComputed 定义计算属性

    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)
      }
    }
    复制代码

    由于 computed 属性是直接挂载到实例对象中的,因此在定义以前须要判断对象中是否已经存在重名的属性,defineComputed 传入了三个参数:vm实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)。 而后继续找到 defineComputed 定义处:

    export function defineComputed ( target: any, key: string, userDef: Object | Function ) {
      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' &&
          sharedPropertyDefinition.set === noop) {
        sharedPropertyDefinition.set = function () {
          warn(
            'Computed property "${key}" was assigned to but it has no setter.',
            this
          )
        }
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    复制代码

    在这段代码的最后调用了原生 Object.defineProperty 方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition,初始化为:

    const sharedPropertyDefinition = {
      enumerable: true,
      configurable: true,
      get: noop,
      set: noop
    }
    复制代码

    随后根据 Object.defineProperty 前面的代码能够看到 sharedPropertyDefinitionget/set 方法在通过 userDefshouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinitionget 函数也就是 createComputedGetter(key) 的结果,咱们找到 createComputedGetter 函数调用结果并最终改写 sharedPropertyDefinition 大体呈现以下:

    sharedPropertyDefinition = {
        enumerable: true,
        configurable: true,
        get: function computedGetter () {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                watcher.depend()
                return watcher.evaluate()
            }
        },
        set: userDef.set || noop
    }
    复制代码

    当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 而后执行 wather.depend() 收集依赖和 watcher.evaluate() 计算求值。

分析完全部步骤,咱们再来总结下整个流程:

  1. 当组件初始化的时候,computeddata 会分别创建各自的响应系统,Observer遍历 data 中每一个属性设置 get/set 数据拦截
  2. 初始化 computed 会调用 initComputed 函数
    1. 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用做后续收集依赖(好比渲染函数的 watcher 或者其余观察该计算属性变化的 watcher
    2. 调用计算属性时会触发其Object.definePropertyget访问器函数
    3. 调用 watcher.depend() 方法向自身的消息订阅器 depsubs 中添加其余属性的 watcher
    4. 调用 watcherevaluate 方法(进而调用 watcherget 方法)让自身成为其余 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,而后执行 getter 求值函数,当访问求值函数里面的属性(好比来自 dataprops 或其余 computed)时,会一样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操做完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。
  3. 当某个属性发生变化,触发 set 拦截函数,而后调用自身消息订阅器 depnotify 方法,遍历当前 dep 中保存着全部订阅者 wathcersubs 数组,并逐个调用 watcherupdate 方法,完成响应更新。

文 / 亦然

一枚向往诗与远方的 coder

编 / 荧声

本文已由做者受权发布,版权属于创宇前端。欢迎注明出处转载本文。本文连接:knownsec-fed.com/2018-09-12-…

想要订阅更多来自知道创宇开发一线的分享,请搜索关注咱们的微信公众号:创宇前端(KnownsecFED)。欢迎留言讨论,咱们会尽量回复。

感谢您的阅读。

相关文章
相关标签/搜索