Vue深入浅出computed(进阶必看系列)

04/28 17:47
阅读数 279

简介

大家好,我是六六。在我们开发当中,当然少不了使用computed计算属性,都知道结果会被缓存起来,那是怎么做到的呢。当然了,在面试中也经常会问到与watch到底有什么不同,那儿接下来,我们将走进computed的内部,详细的去了解一下。

目录

  • 你知道的computed特性
  • computed原理是什么
  • computed原理具体实现
  • 详说整个computed过程
  • 面试题与扩展
  • 总结

1.你知道的computed特性:

  • 计算属性在使用的时候,要当做普通属性使用就好,不需要加()
  • 只要计算属性这个function内部所用到的data中的数据发生了变化,就会立即重新计算这个计算属性的值
  • 计算属性的求值结果,会被缓存起来,方便下次继续使用;如果计算属性方法中,所依赖的任何数据,都没有发生过变化,则不会重新对计算属性求值
  • 可以为函数或者对象

2.computed原理是什么:

学习中最常见听到的一句话就是,computed就是一个特殊的getter方法。在代理函数可以结合watcher实现缓存与收集依赖。

计算属性具有缓存性,如何知道计算属性的返回值发生变化呢?

这其实就是结合了watcher的dirty属性来分辨的,当dirty为true时,说明需要重新计算,当为false时,计算属性没有改变,不需要重新计算,直接读取缓存值就好。

模拟一下计算属性内容发生改变后:

  • 计算属性的watcher和组件内的watcher都会得到通知
  • 计算属性的watcher将自己的属性dirty设置为true
  • 下次读取计算属性时,因为dirty为true重新计算一次值
  • 组件watcher得到通知,从而执行render函数进行重新渲染

3.computed原理具体实现:

3.1 initComputed初始化

const computedWatcherOptions={lazy:true}
  function initComputed(vm, computed) {
    const watchers = vm._computedWatchers = Object.create(null)
    const isSSR = isServerRendering()
    for (const key in computed) {
      // userDef我们定义的计算属性
      const userDef = computed[key]
      // 获取getter
      const getter = typeof userDef === 'function' ? userDef : userDef.get
      // 非SSR环境
      if (!isSSR) {
      // 创建watcher实例
        watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
      }
      if (!(key in vm)) {
        defineComputed(vm, key, userDef)
      }
    }
  }
复制代码

具体讲解:

  • initComputed函数接受两个参数:vm实例和computed对象
  • 声明了一个变量watchers,并保存在vm._computedWatchers
  • 声明变量isSSR用于判断是否在SSR环境
  • 使用for..in循环computed对象,以此初始化每个计算属性
  • 用getter保存计算结果的值
  • 创建watcher实例
  • 判断计算属性的值是否已经在vm实例上定义了,如果没有,执行defineComputed函数

到这里,我们就明白了,最后我们会执行defineComputed函数,对每个计算属性进行定义和初始化。下面我们来看看这个函数。

3.2 defineComputed定义计算属性

  const sharePropertyDefinition={
    enumerable:true,
    configurable:true,
    get:noop,
    set:noop
  }
  function defineComputed(target,key,userDef){
    // 判断环境
    const shouldCache=!isServerRendering()
    // 设置的为函数
    if(typeof userDef==='function'){
      sharePropertyDefinition.get=shouldCache?createComputedGetter(key):userDef
      sharePropertyDefinition.set=noop
    }
    // 设置的为对象
    else{
      sharePropertyDefinition.get=userDef.get?shouldCache&&userDef.cache!=false?createComputedGetter(key):userDef.get:noop
      sharePropertyDefinition.set=userDef.set?userDef.set:noop
    }
    Object.defineProperty(target,key,sharePropertyDefinition)
  }
复制代码

具体讲解:

  • 首先定义sharePropertyDefinition变量,用于配合Object.defineProperty使用。
  • 函数defineComputed接受target,key,userDef三个参数
  • 使用shouldCache变量确认是否在SSR环境
  • 判断userDef是否为函数,因为我们知道我们传入的computed支持函数或者对象
  • 如果是函数,判断shouldCache,为true时执行createComputedGetter函数,并赋值给 sharePropertyDefinition.get
  • 如果不是函数,是否写了get函数,判断shouldCache,为true时执行createComputedGetter函数,并赋值给 sharePropertyDefinition.get
  • 在判断用户是否写了set函数,写了就赋值给sharePropertyDefinition,noop为空函数。
  • 在最后,执行Object.defineProperty向实例挂载属性

所以,计算属性的缓存和响应式主要在于是否将getter方法设置为createComputedGetter。因为最终挂载到get方法的就是createComputedGetter函数。

3.3createComputedGetter缓存与响应式的关键

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

具体讲解:

  • 在initComputed函数中我们定义了_computedWatchers属性,通过_computedWatchers[key]拿到我们定义的watcher,所以变量watcher就是每个watcher的实例
  • 判断watcher.dirty值是否为真,为真就重新计算一下,也就是执行watcher.evaluate()
  • 判断Dep.target的值,如果有,就执行 watcher.depend(),最后返回watcher的value值。

代码是非常简单的,但是理解起来是需要配合watcher和dep的,如果不了解可以参考我这篇文章: 深入浅出Vue变化侦测

3.4 与watcher密不可分

class Watcher{
    constructor(vm,expOrFn,cb,options){
        // 无关代码
        if(options){
            this.lazy=!!options.lazy
        }
        this.dirty = this.lazy
    }
    evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
复制代码

evaluate方法:就是从watcher重新获取一次表达式的值
depend方法:

  • this.deps[i]就是计算属性所依赖的状态
  • 调用depend方法可以将组件的watcher实例添加到dep实例中,意思就是计算属性所依赖的状态改变也会通知组件,更具体说,就是组件watcher也会观察计算属性所依赖的状态。(组件watcher是如何拿到的呢)

4.详说整个computed过程:

  • 使用watcher读取计算属性
  • 读取计算属性函数中的数据,定义响应式时,get读取的就是watcher.value
  • 计算属性和组件watcher同时观察数据的变化
  • 当数据改变后,计算属性和组件watcher都会收到通知
  • 组件watcher会重新渲染组件
  • 计算属性watcher因为数据改变,dirty属性为true,将重新计算
  • 计算属性计算的结果用于本次渲染,并缓存起来

5.面试题:watch和computed的区别是什么?

其实我觉得这两个作用是完全不一样,不知道为什么总拿来比较。

  • watch是一种行为,在状态改变之后需要做什么。
  • computed就是一种状态,也可以说多种状态初始化后的结果。

我认为把,computedfilter作为比较不是更好一些吗?都是用来初始化状态用的。

  • computed更适用于大量数据计算的结果,并且反复使用,而且不常更新。因为有缓存,大大提升性能。
  • filter适用于少量数据进行初始化处理,计算量不能太大,因为每次渲染都会计算,并且可以频繁更新。

6.总结

一开始学这个知识点确实很懵,一个知识点看了很多遍,到最后才领悟过来,不过也有很多一知半解的地方,所以每天还是要继续努力。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部