[摘要]本篇文章给大家带来的内容是关于Vue中computed的实现原理是什么?有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。虽然目前的技术栈已由 Vue 转到了 React,但从之前使用 ...
本篇文章给大家带来的内容是关于Vue中computed的实现原理是什么?有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。
虽然目前的技术栈已由 Vue 转到了 React,但从之前使用 Vue 开发的多个项目实际经历来看还是非常愉悦的,Vue 文档清晰规范,api 设计简洁高效,对前端开发人员友好,上手快,甚至个人认为在很多场景使用 Vue 比 React 开发效率更高,之前也有断断续续研读过 Vue 的源码,但一直没有梳理总结,所以在此做一些技术归纳同时也加深自己对 Vue 的理解,那么今天要写的便是 Vue 中最常用到的 API 之一 computed 的实现原理。
基本介绍
话不多说,一个最基本的例子如下:
<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 的设计初衷也正是用于解决此类问题。
对比侦听器 watch
当然很多时候我们使用 computed 时往往会与 Vue 中另一个 API 也就是侦听器 watch 相比较,因为在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。从 Vue 官方文档对 watch 的解释我们可以了解到,使用  watch  选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。
下面还另外总结了几点关于 computed 和 watch 的差异:
- computed是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而- watch是监听已经存在且已挂载到- vm上的数据,所以用- watch同样可以监听- computed计算属性的变化(其它还有- data、- props)
 
- computed本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问- computed属性,才会计算新的值,而- watch则是当数据发生变化便会调用执行函数
 
- 从使用场景上说,- computed适用一个数据被多个数据影响,而- watch适用一个数据影响多个数据;
 
以上我们了解了 computed 和 watch 之间的一些差异和使用场景的区别,当然某些时候两者并没有那么明确严格的限制,最后还是要具体到不同的业务进行分析。
原理分析
言归正传,回到文章的主题 computed 身上,为了更深层次地了解计算属性的内在机制,接下来就让我们一步步探索 Vue 源码中关于它的实现原理吧。
在分析 computed 源码之前我们先得对 Vue 的响应式系统有一个基本的了解,Vue 称其为非侵入性的响应式系统,数据模型仅仅是普通的 JavaScript 对象,而当你修改它们时,视图便会进行自动更新。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。Vue 响应系统,其核心有三点:observe、watcher、dep:
- observe:遍历- data中的属性,使用 Object.defineProperty 的- get/set方法对其进行数据劫持;
 
- dep:每个属性拥有自己的消息订阅器- dep,用于存放所有订阅了该属性的观察者对象;
 
- 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 函数(其前后也分别初始化了 initData 和 initWatch )并传入两个参数 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)
      }
    }
  }
}从这段代码开始我们观察这几部分:
- 获取计算属性的定义 - userDef和- getter求值函数
 - const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get- 定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 - set和- get方法的对象形式,所以这里首先获取计算属性的定义- userDef,再根据- userDef的类型获取相应的- getter求值函数。
 
 
- 计算属性的观察者 - 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()
    }
  }
}- 为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
 观察- Watcher的- constructor,结合刚才讲到的- 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同样精简了部分代码,我们观察- Watcher和- Dep的关系,用一句话总结
 - watcher中实例化了- dep并向- dep.subs中添加了订阅者,- dep通过- notify遍历了- dep.subs通知每个- watcher更新。
 
- 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前面的代码可以看到- sharedPropertyDefinition的- get/set方法在经过- userDef和- shouldCache等多重判断后被重写,当非服务端渲染时,- sharedPropertyDefinition的- get函数也就是- 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()计算求值。
 
分析完所有步骤,我们再来总结下整个流程:
- 当组件初始化的时候,- computed和- data会分别建立各自的响应系统,- Observer 遍历- data中每个属性设置- get/set数据拦截
 
- 初始化 - computed会调用- initComputed函数
 
- 注册一个 - watcher实例,并在内实例化一个- Dep消息订阅器用作后续收集依赖(比如渲染函数的- watcher或者其他观察该计算属性变化的- watcher)
 
- 调用计算属性时会触发其- Object.defineProperty的- get访问器函数
 
- 调用 - watcher.depend()方法向自身的消息订阅器- dep的- subs中添加其他属性的- watcher
 
- 调用 - watcher的- evaluate方法(进而调用- watcher的- get方法)让自身成为其他- watcher的消息订阅器的订阅者,首先将- watcher赋给- Dep.target,然后执行- getter求值函数,当访问求值函数里面的属性(比如来自- data、- props或其他- computed)时,会同样触发它们的- get访问器函数从而将该计算属性的- watcher添加到求值函数中属性的- watcher的消息订阅器- dep中,当这些操作完成,最后关闭- Dep.target赋为- null并返回求值函数结果。
 
- 当某个属性发生变化,触发 - set拦截函数,然后调用自身消息订阅器- dep的- notify方法,遍历当前- dep中保存着所有订阅者- wathcer的- subs数组,并逐个调用- watcher的- update方法,完成响应更新。
 
相关推荐:
javascript - 刷票器的实现原理
thinkphp 控制器中 display()步骤实现的原理
以上就是Vue中computed的实现原理是什么?的详细内容,更多请关注php中文网其它相关文章!
				
				
					
微信
					分享
				
网站建设是一个广义的术语,涵盖了许多不同的技能和学科中所使用的生产和维护的网站。
  关键词:Vue中computed的完成原理是啥?