计算属性 API:Computed

computed 作用

根据一些依赖的响应式数据计算出新值并返回。当依赖发生变化时,计算属性可以自动重新计算获取新值

computed 函数的具体实现

计算属性在源码中就是通过 computed 函数实现的

js 复制代码
function computed(getterOrOptions) {
  // getter 函数 
  let getter 
  // setter 函数 
  let setter 
  // 标准化参数 
  if (isFunction(getterOrOptions)) { 
    // 表面传入的是 getter 函数,不能修改计算属性的值 
    getter = getterOrOptions 
    setter = (process.env.NODE_ENV !== 'production') 
      ? () => { 
        console.warn('Write operation failed: computed value is readonly') 
      } 
      : NOOP 
  } 
  else { 
    getter = getterOrOptions.get 
    setter = getterOrOptions.set 
  } 
  // 数据是否脏的 
  let dirty = true 
  // 计算结果 
  let value 
  let computed 
  // 创建副作用函数 
  const runner = effect(getter, { 
    // 延时执行 
    lazy: true, 
    // 标记这是一个 computed effect 用于在 trigger 阶段的优先级排序 
    computed: true, 
    // 调度执行的实现 
    scheduler: () => { 
      if (!dirty) { 
        dirty = true 
        // 派发通知,通知运行访问该计算属性的 activeEffect 
        trigger(computed, "set" /* SET */, 'value') 
      } 
    } 
  }) 
  // 创建 computed 对象 
  computed = { 
    __v_isRef: true, 
    // 暴露 effect 对象以便计算属性可以停止计算 
    effect: runner, 
    get value() { 
      // 计算属性的 getter 
      if (dirty) { 
        // 只有数据为脏的时候才会重新计算 
        value = runner() 
        dirty = false 
      } 
      // 依赖收集,收集运行访问该计算属性的 activeEffect 
      track(computed, "get" /* GET */, 'value') 
      return value 
    }, 
    set value(newValue) { 
      // 计算属性的 setter 
      setter(newValue) 
    } 
  } 
  return computed 
}

计算属性的整体运行流程

computed 内部两个重要的变量:

  1. dirty 表示一个计算属性的值是否是"脏的",用来判断需不需要重新计算
  2. value 表示计算属性每次计算后的结果。

看以下示例:

js 复制代码
<template> 
  <div> 
    {{ plusOne }} 
  </div> 
  <button @click="plus">plus</button> 
</template> 
<script> 
import { ref, computed } from 'vue' 
export default { 
  setup() { 
    const count = ref(0) 
    const plusOne = computed(() => { 
      return count.value + 1 
    }) 
​
    function plus() { 
      count.value++ 
    } 
    return { 
      plusOne, 
      plus 
    } 
  } 
} 
</script>

特别注意这是两个依赖收集过程:

  1. 对于 plusOne 来说,它收集的依赖是组件副作用渲染函数
  2. 对于 count 来说,它收集的依赖是 plusOne 内部的 runner 函数

注意,count 的值变化时,不会直接调用 runner 函数,而是把 runner 作为参数去执行 scheduler 函数。 这里需要回顾一下 trigger 函数内部对于 effect 函数的执行方式:

js 复制代码
const run = (effect) => { 
  // 调度执行 
  if (effect.options.scheduler) { 
    effect.options.scheduler(effect) 
  } 
  else { 
    // 直接运行 
    effect() 
  } 
}

如果通过 effect 创建副作用函数时,options 中有传入 scheduler,则执行 scheduler 函数(上面 runner 函数就是传入了 scheduler) runner 函数中传入的 scheduler 中,并没有对计算属性求新值,而是:

  1. 将 dirty 设置为 true
  2. 执行 trigger,去通知执行 plusOne 依赖的组件渲染副作用函数,从而触发组件重新渲染

组件重新渲染后,再次访问 plusOne,触发 getter,此时 dirty 为 true,所以执行 runner,求得新值 count.value + 1 这就是虽然组件没有直接访问 count,但是当我们修改 count 的值的时候,组件仍然会重新渲染的原因。

通过以上分析,我们可以得到 computed 计算属性的两个特点

  1. 延时计算,只有当我们访问计算属性的时候,它才会真正运行 computed getter 函数计算;
  2. 缓存,它的内部会缓存上次的计算结果 value,而且只有 dirty 为 true 时才会重新计算。如果访问计算属性时 dirty 为 false,那么直接返回这个 value。

和单纯使用普通函数相比,计算属性的优势是 :只要依赖不变化,就可以使用缓存的 value 而不用每次在渲染组件的时候都执行函数去计算,这是典型的空间换时间的优化思想

嵌套的计算属性

看以下示例:

js 复制代码
const count = ref(0) 
const plusOne = computed(() => { 
  return count.value + 1 
}) 
const plusTwo = computed(() => { 
  return plusOne.value + 1 
}) 
console.log(plusTwo.value)

流程与上面同理。 依赖收集:

  1. 对于 plusOne 来说,它收集的依赖是 plusTwo 内部的 runner 函数;
  2. 对于 count 来说,它收集的依赖是 plusOne 内部的 runner 函数。

整体过程:

  1. 修改 count 的值时,它会派发通知先运行 plusOne 内部的 scheduler 函数,把 plusOne 内部的 dirty 变为 true
  2. 然后再次派发通知,运行 plusTwo 内部的 scheduler 函数,把 plusTwo 内部的 dirty 设置为 true。
  3. 当我们再次访问 plusTwo 的值时,发现 dirty 为 true,就会执行 plusTwo 的 runner 函数去执行 plusOne.value + 1,进而执行 plusOne 的 runner 函数即 count.value + 1 + 1,求得最终新值 2。

得益于 computed 这种巧妙的设计,无论嵌套多少层计算属性都可以正常工作

计算属性的执行顺序

计算属性内部创建副作用函数的时候会配置 computed 为 true,标识这是一个 computed 的 effect,用于在 trigger 阶段的优先级排序。 trigger 阶段执行 effect 的过程:

js 复制代码
const add = (effectsToAdd) => { 
  if (effectsToAdd) { 
    effectsToAdd.forEach(effect => { 
      if (effect !== activeEffect || !shouldTrack) { 
        if (effect.options.computed) { 
          computedRunners.add(effect) 
        } 
        else { 
          effects.add(effect) 
        } 
      } 
    }) 
  } 
} 
const run = (effect) => { 
  if (effect.options.scheduler) { 
    effect.options.scheduler(effect) 
  } 
  else { 
    effect() 
  } 
} 
computedRunners.forEach(run) 
effects.forEach(run)

由上述代码得知,会先执行 computed 的 effect,然后执行普通的 effect 为何要如此设计?因为考虑到下面这种特殊场景(同时读取 plusOne 和 count):

js 复制代码
import { ref, computed } from 'vue' 
import { effect } from '@vue/reactivity' 
const count = ref(0) 
const plusOne = computed(() => { 
  return count.value + 1 
}) 
effect(() => { 
  console.log(plusOne.value + count.value) 
}) 
function plus() { 
  count.value++ 
} 
plus()
​

plus++ 之后,运行后的结果:

1 
3 
3

如果不先执行 computed effect 而先执行普通 effect 的话,则输出的是 1 2 3 这里是因为 computed 的 effect 并不会立即执行 runner,而是先将 dirty 设置为 true,然后触发 computed effect 执行重新读取 plusOne 的值时才更新。如果先执行普通 effect,则第二次输出的时候 dirty 还为 false,此时读取 plusOne 时读不到最新值,所以会输出 2。 这就是设计 computed 的 effect 先执行的原因

相关推荐
涔溪30 分钟前
vue2+Three.js或WebGL上传预览CAD文件
javascript·vue.js·webgl
专注VB编程开发20年36 分钟前
如何保存网站CSS和JS中的图片?网页另存为本地显示不正常
前端·javascript·css
丶重明39 分钟前
【2024】前端学习笔记9-内部样式表-外部导入样式表-类选择器
前端·笔记·学习
又写了一天BUG1 小时前
关于在vue2中给el-input等输入框的placeholder加样式
前端·javascript·vue.js
计算机学姐1 小时前
基于微信小程序的智慧物业管理系统
java·vue.js·spring boot·mysql·微信小程序·小程序·mybatis
秋月霜风1 小时前
Web爬虫应用功能及需求设计
前端·爬虫·python
蜡笔小新星1 小时前
前端框架对比和选择
前端·javascript·vue.js·经验分享·学习·前端框架
Crazy Struggle1 小时前
.NET 8 + Vue/UniApp 高性能前后端分离框架
vue.js·uni-app·.net·后台管理系统
一直在学习的小白~2 小时前
中间添加一条可以拖拽的分界线,来动态调整两个模块的宽度
前端·javascript·react.js
此白非彼白`2 小时前
vue使用PDF.JS踩的坑--部署到服务器上显示pdf.mjs viewer.mjs找不到资源
javascript·vue.js·pdf·asp.net