Vue3源码解析计划之计算属性为什么比普通函数妙?

开发 前端
计算属性是Vue开发中一个非常实用的API,它允许用户自定义一个计算方法,然后根据一些依赖的响应式数据计算出新值并返回。当依赖发生变化时,计算属性会自动重新计算获取新值,使用方便。

[[440389]]

本文转载自微信公众号「前端万有引力」,作者 一川 。转载本文请联系前端万有引力公众号。

写在前面

计算属性是Vue开发中一个非常实用的API,它允许用户自定义一个计算方法,然后根据一些依赖的响应式数据计算出新值并返回。当依赖发生变化时,计算属性会自动重新计算获取新值,使用方便。我们看出计算属性本质上是对依赖的计算,为什么不直接使用函数呢?在Vue3中的计算属性又是如何实现的呢?

计算属性 computed

我们先简单看个例子,我们看到再设置了计算属性addOne后,直接改变addOne.value的值会报错,只能通过改变原始值count.value才不会报错。这是因为:

  • 如果传递给computed的是一个函数,那就是一个getter函数,只能获取它的值,而不能直接修改它
  • 在getter函数中,根据响应式对象重新计算出新值,叫做计算属性,这个响应式对象叫做计算属性的依赖
  1. const count = ref(1); 
  2. const addOne = computed(()=>count.value+1); 
  3. console.log(addOne.value);//2 
  4. addOne.value++;//error 
  5. count.value++; 
  6. console.log(count.value);//3 

那么,我们应该如何修改addOne.value值呢?那就是在computed中设置set函数,进行自定义修改值。

  1. const count = ref(1); 
  2. const addOne = computed({ 
  3.  get:()=>count.value+1, 
  4.   set:val=>count.value=val-1 
  5. }); 
  6. addOne.value = 1; 
  7. console.log(count.value);//0 

我们研究源码:

  1. export function computed<T>( 
  2.   getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T> 
  3. ) { 
  4.   let getter: ComputedGetter<T> 
  5.   let setter: ComputedSetter<T> 
  6.  
  7.   // 如果传入是 function 说明是只读 computed 
  8.   if (isFunction(getterOrOptions)) { 
  9.     getter = getterOrOptions 
  10.     setter = __DEV__ 
  11.       ? () => { 
  12.           console.warn('Write operation failed: computed value is readonly'
  13.         } 
  14.       : NOOP 
  15.   } else { 
  16.     // 不是方法说明是自定义的 getter setter  
  17.     getter = getterOrOptions.get 
  18.     setter = getterOrOptions.set 
  19.   } 
  20.  
  21.   let dirty = true 
  22.   let value: T 
  23.   let computed: ComputedRef<T> 
  24.  
  25.   // 创建 effect, 我们在看 effect 源码时知道了传入 lazy 代表不会立即执行,computed 表明 computed 上游依赖改变的时候,会优先 trigger runner effect, scheduler 表示 effect trigger 的时候会调用 scheduler 而不是直接调用 effect 
  26.   const runner = effect(getter, { 
  27.     lazy: true
  28.     // mark effect as computed so that it gets priority during trigger 
  29.     computed: true
  30.     scheduler: () => { 
  31.       // 在触发更新时把dirty置为true, 不会立即更新  
  32.       if (!dirty) { 
  33.         dirty = true 
  34.         trigger(computed, TriggerOpTypes.SET'value'
  35.       } 
  36.     } 
  37.   }) 
  38.  
  39.   // 构造一个 computed 返回 
  40.   computed = { 
  41.     __v_isRef: true
  42.     // expose effect so computed can be stopped 
  43.     effect: runner, 
  44.     get value() { 
  45.       // dirty为ture, get操作时,执行effect获取最新值 
  46.       //  
  47.       if (dirty) { 
  48.         value = runner() 
  49.         dirty = false 
  50.       } 
  51.       // dirty为false, 表示值未更新,直接返回  
  52.       track(computed, TrackOpTypes.GET, 'value'
  53.       return value 
  54.     }, 
  55.     set value(newValue: T) { 
  56.       setter(newValue) 
  57.     } 
  58.   } as any 
  59.   return computed 

computed计算属性有两个特点:

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

那么,计算属性的优势是:只要依赖不变化,就可以使用缓存的value而不用每次再渲染组件的时候都执行函数去计算。

做个嵌套计算的小例子,我们看到:对于addOne而言,它收集的依赖是组件副作用渲染函数,而对于count而言,它收集的依赖是addTwo内部的runner函数。当我们修改count值,会进行派发通知,先运行addOne中的setter函数,此时addOne中的dirty值变成true,然后trigger函数再次派发通知;接着运行addTwo中的setter函数,此时把addTwo中的dirty值设置为true;当我们再次访问addTwo中的值时,发现dirty值为true,就会执行addTwo的computed函数,会先去执行addOne.value + 1,再去执行addOne的computed函数中的count.value + 1。这样就得到最后打印出来的值为2。

  1. const count = ref(0); 
  2. const addOne = computed(()=>{ 
  3.  return count.value + 1;//1 
  4. })  
  5.  
  6. const addTwo = computed(()=>{ 
  7.  return addOne.value + 1;//2 
  8. }) 
  9.  
  10. console.log(addTwo.value);//2 

得益于computed计算属性的巧妙设计,无论嵌套多少层都能够正常运行。

  1. import {ref,computed} from "vue"
  2. import {effect} from "@vue/reactivity"
  3.  
  4. const count = ref(0); 
  5. const addOne = computed(()=>{ 
  6.  return count.value + 1; 
  7. }) 
  8.  
  9. effect(()=>console.log(addOne.value+count.value)) 
  10.  
  11. function add(){ 
  12.  count.value++; 
  13.  
  14. add(); 

我们看到上面代码最终输出结果是:1 3 3

当我们第一次执行addOne的computed计算属性时,count.value值还是0,而addOne.value的值为1,将会触发并执行effect,此时打印出来还是1。而后执行add()函数,会进行修改count.value的值,会触发并执行effect函数,因为addOne也是effect的依赖,addOne的runners函数也是count.value的依赖,count.value值的修改会执行runners函数,会再次执行addOne的依赖,接着会触发effect函数,因此会输出两次3。

computed函数返回的对象实际上劫持的是value属性的getter和setter,但是为什么我们在组件的模板中访问一个计算属性变量,不用手动在后面加.value呢?

参考文章

  • 《Vue3核心源码解析》
  • 《Vue中文社区》
  • 《Vue3中文文档》

写在最后

 

理解计算属性的工作机制,能够搞明白计算属性嵌套场景代码的执行顺序,知道计算属性的两个特点--延时计算和缓存,在组件的开发中合理使用计算属性。

 

责任编辑:武晓燕 来源: 前端万有引力
相关推荐

2021-12-12 18:31:35

VNode组件Vue3

2021-01-20 14:25:53

Vue3CSS前端

2021-08-23 13:25:25

Vue3CSS前端

2023-12-11 07:34:37

Computed计算属性Vue3

2024-05-27 08:39:17

Vue3变量响应式

2023-07-26 17:40:50

2021-09-22 07:57:23

Vue3 插件Vue应用

2021-11-26 05:59:31

Vue3 插件Vue应用

2021-12-13 00:54:14

组件Vue3Setup

2020-09-17 07:08:04

TypescriptVue3前端

2024-07-04 08:56:35

Vue3项目Pinia

2022-01-26 11:00:58

源码层面Vue3

2021-12-01 08:11:44

Vue3 插件Vue应用

2021-11-30 08:19:43

Vue3 插件Vue应用

2023-11-28 09:03:59

Vue.jsJavaScript

2021-09-27 06:29:47

Vue3 响应式原理Vue应用

2022-03-24 20:42:19

Vue3API 设计Vue

2021-07-29 12:05:18

Vue3Api前端

2020-09-19 21:15:26

Composition

2021-12-02 05:50:35

Vue3 插件Vue应用
点赞
收藏

51CTO技术栈公众号