作者:秦志英

前言

上一篇文章中我们分析了Vue3响应式的整个流程,本篇文章我们将分析Vue3中的computed计算属性是如何实现的。

在Vue2中我们已经对计算属性了解的很清楚了,在Vue3中提供了一个computed的函数作为计算属性的API,下面我们来通过源码的角度去分析计算属性的运行流程。

computed

export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
if (isFunction(getterOrOptions)) {
getter = getterOrOptions
setter = NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set
) as any
}
  • 在最开始使用函数重载的方式允许computed函数接受两种类型的参数:第一种是一个getter函数, 第二种是一个带getset的对象。
  • 接下就是在函数内部根据传入的不同类型的参数初始化函数内部的gettersetter函数,如果传入的是一个函数类型的参数,那么getter就是这个函数,setter就是一个空的操作,如果传入的参数是一个对象,则getter就等于这个对象的get函数,setter就等于这个对象的set函数。
  • 在函数的结尾返回了一个new ComputedRefImpl,并将前面我们标准化后的参数传递给了这个构造函数。

    下面我们就来分析一下ComputedRefImpl这个构造函数。

ComputedRefImpl

class ComputedRefImpl<T> {
// 缓存结果
private _value!: T
// 重新计算开关
private _dirty = true
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
// 对传入的getter函数进行包装
this.effect = effect(getter, {
lazy: true,
// 调度执行
scheduler: () => {
if (!this._dirty) {
this._dirty = true
// 派发通知
trigger(toRaw(this), TriggerOpTypes.SET, 'value')
}
}
})
}
// 访问计算属性的时候 默认调用此时的get函数
get value() {
// 是否需要重新计算
if (this._dirty) {
this._value = this.effect()
this._dirty = false
}
// 访问的时候进行依赖收集 此时收集的是访问这个计算属性的副作用函数
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
} set value(newValue: T) {
this._setter(newValue)
}
}

ComputedRefImpl类在内部维护了_value_dirty这两个非常重要的私有属性,其中_value使用用来缓存我们计算的结果,_dirty是用来控制是否需要重现计算。接下来我们来看一下这个函数的内部运行机制。

  • 首先构造函数在初始化的时候使用了effect函数对传入getter进行了一层包装(上一篇文章中我们分析过effect函数的作用就是将传入的函数变成可响应式的副作用函数),但是这里我们在effect中传入了一些配置参数,还记得前面我们分析trigger函数的时候有这一段代码:
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
}
effects.forEach(run)

当属性值发生改变之后,会触发trigger函数进行派发更新,将所有依赖这个属性的effect函数循环遍历,使用run函数执行effect,如果effect的参数中配置了scheduler,则就执行scheduler函数,而不是执行依赖的副作用函数。当计算属性依赖的属性发生变化的时候,回执行包装getter函数的effect, 但是因为配置了scheduler函数,所以真正执行的是scheduler函数,在scheduler函数中并没有执行计算属性的getter函数求取新值,而是将_dirty设置为false,然后通知依赖计算属性的副作用函数进行更新, 当依赖计算属性的副作用函数收到通知的时候就会访问计算属性的get函数,此时会根据_dirty值来确定是否需要重新计算。

回到我们的这个构造函数中,只需要记得我们在构造函数初始化三个重要的点:第一:对传入的getter函数使用effect函数进行包装。第二:在使用effect包装的过程中,我们会执行getter函数,此时getter函数执行过程中对于访问到的属性会将当前的这个计算属性收集到对应的依赖集合中, 第三:传入了配置参数lazyscheduler,这些配置参数在当前的这个计算属性所订阅的属性发生改变的时候,用来控制计算属性的调度时机。

  • 接着我们继续分析get value,当我们访问计算属性的值时候实际上访问的就是这个函数的返回值, 它会根据_dirty的值来判断是否需要重新计算getter函数,_dirty为true需要重新执行effect函数,并将effect的值置为false,否则就返回之前缓存的_value值。在访问计算属性值的阶段会调用track函数进行依赖收集,此时收集的是访问计算属性值的副作用函数, key始终是vlaue。
  • 最后就是当设置计算属性的值的时候会执行set函数,然后调用我们传入的_setter函数。

示例流程

至此计算属性的执行流程就分析完毕了,我们来结合一个示例来完整的过一遍整个流程:

<template>
<div>
<button @click="addNum">add</button>
<p>计算属性:{{computedData}}</p>
</div>
</template> <script>
import { ref, watch,reactive, computed } from 'vue'
import { effect } from '@vue/reactivity'
export default {
name: 'App',
setup(){
const testData = ref(1)
const computedData = computed(() => {
return testData.value++
})
function addNum(){
testData.value += 10
}
return {
addNum,
computedData
}
},
} </script>

下面是一张流程图,当点击页面中的按钮改变testData的value值时,发生的变化流程就是下面的红线部分。

  • 首先初始化页面的时候,testData经过ref()之后变成响应式数据,会对访问testData.value的值进行依赖收集,当testData.value的值发生变化的话,会对依赖这个值的依赖集合进行派发更新
  • computed中传入了一个getter函数,getter函数内部有对testData.value的访问,此时当前的这个计算属性的副作用函数就订阅了testData.value的值,computed返回了一个值,而页面中的组件有对computed返回值的访问,页面的渲染副作用函数就订阅了computed的返回值,所以这个页面中有两个依赖集合。
  • 当我们点击页面中的按钮,会改变testData.value的值,此时会通知订阅计算属性的副作用函数进行更新操作,由于我们在生成计算属性副作用的时候配置了scheduler,所以执行的是scheduler函数,scheduler函数并没有立即执行getter函数进行重新计算,而是将ComputedRefImpl类内部的私有变量_dirty设置为true,然后通知订阅当前计算属性的副作用函数进行更新操作。
  • 组件中的渲染副作用函数执行更新操作的时候会访问到get value函数,函数内部会根据_dirty值来判断是否需要重新计算,由于前面的scheduler函数将_dirty设置为true所以此时会调用getter函数的副作用函数effect,这个时候才会重新计算并将结果返回,页面数据更新。

总结

计算属性两个最大的特点就是

  • 延时计算 计算属性所依赖的值发生改变的时候并不会立即执行getter函数去重新计算新的结果,而是打开重新计算的开关并通知订阅计算属性的副作用函数进行更新。如果当前的计算属性没有依赖集合就不执行重新计算逻辑,如果有依赖触发计算属性的get,这个时候才会调用this.effect()进行重新计算。
  • 缓存结果 当依赖的属性没有发生改变的,访问计算属性会返回之前缓存在_value中的值。

对 Electron 感兴趣?请关注我们的开源项目 Electron Playground,带你极速上手 Electron。

我们每周五会精选一些有意思的文章和消息和大家分享,来掘金关注我们的 晓前端周刊


我们是好未来 · 晓黑板前端技术团队。

我们会经常与大家分享最新最酷的行业技术知识。

欢迎来 知乎掘金SegmentfaultCSDN简书开源中国博客园 关注我们。

Vue3源码解析(computed-计算属性)的更多相关文章

  1. vuex 源码解析(三) getter属性详解

    有时候我们需要从store中的state中派生出一些状态,例如: <div id="app"> <p>{{reverseMessage}}</p> ...

  2. vue 源码解析computed

    计算属性 VS 侦听属性 Vue 的组件对象支持了计算属性 computed 和侦听属性 watch 2 个选项,很多同学不了解什么时候该用 computed 什么时候该用 watch.先不回答这个问 ...

  3. Netty 出站缓冲区 ChannelOutboundBuffer 源码解析(isWritable 属性的重要性)

    目录: 前言 ChannelOutboundBuffer 介绍 addMessage 方法 addFlush 方法 flush0 方法 缓冲区扩展思考 总结 每个 ChannelSocket 的 Un ...

  4. HashMap 源码解析(一)之使用、构造以及计算容量

    目录 简介 集合和映射 HashMap 特点 使用 构造 相关属性 构造方法 tableSizeFor 函数 一般的算法(效率低, 不值得借鉴) tableSizeFor 函数算法 效率比较 tabl ...

  5. [源码解析] 深度学习流水线并行 GPipe(3) ----重计算

    [源码解析] 深度学习流水线并行 GPipe(3) ----重计算 目录 [源码解析] 深度学习流水线并行 GPipe(3) ----重计算 0x00 摘要 0x01 概述 1.1 前文回顾 1.2 ...

  6. [源码解析] 深度学习流水线并行 PipeDream(2)--- 计算分区

    [源码解析] 深度学习流水线并行 PipeDream(2)--- 计算分区 目录 [源码解析] 深度学习流水线并行 PipeDream(2)--- 计算分区 0x00 摘要 0x01 前言 1.1 P ...

  7. HTK计算mfcc/filter_bank源码解析

    HTK计算mfcc/filter_bank源码解析 HTK可以用简单的 HCopy -C config -s scp 求取mfcc或者filter_bank 关于mfcc的原理在 http://my. ...

  8. [源码解析] PyTorch 流水线并行实现 (4)--前向计算

    [源码解析] PyTorch 流水线并行实现 (4)--前向计算 目录 [源码解析] PyTorch 流水线并行实现 (4)--前向计算 0x00 摘要 0x01 论文 1.1 引论 1.1.1 数据 ...

  9. [源码解析] PyTorch 流水线并行实现 (5)--计算依赖

    [源码解析] PyTorch 流水线并行实现 (5)--计算依赖 目录 [源码解析] PyTorch 流水线并行实现 (5)--计算依赖 0x00 摘要 0x01 前文回顾 0x02 计算依赖 0x0 ...

随机推荐

  1. Spring中@Translational注解失效场景

    今天面试被问到@Translational注解什么场景下失效,我一脸懵逼,说的恍恍惚惚的,下来我就总结一下@Translational注解失效的场景! @Transactional 注解相信大家并不陌 ...

  2. cakephp中sql查询大于

    $list = $this->Capital->find('all', array('conditions'=>array('amount >'=>0)));

  3. window下开启关闭mysql服务

    cmd:管理员模式 net start mysql net stop mysql

  4. zabbix的搭建及操作(1)server-client架构

    实验环境 Server端     Centos7:192.168.10.10  server.zabbix.com    可连外网 Agent 端     Centos7:192.168.10.20  ...

  5. js 表格上checkbox 全选

    <table class="layui-table"> <thead> <tr> <th width="75"> ...

  6. Idea中如何导入jar包

    1.首先在idea左上角找到" File ",然后找到 "Project structure" 2.接着选择 " java ",选择后接着会 ...

  7. IDEA创建web工程(超简单)

    Idea创建Web工程 以新建模块为例. 新建Maven项目 勾选[Create from artchetype] 选择[org.apache.maven.archetypes:maven-arche ...

  8. I/O中的 同步异步,阻塞非阻塞

    I/O中的同步和异步的概念和线程中不太一样. I/O写的时候,默认是写到页高速缓存就返回的,然后异步刷到磁盘上.而同步的I/O指的是改动写到磁盘上之后才会返回结果.可以通过fsync(),和fdata ...

  9. 深度学习论文翻译解析(十五):Densely Connected Convolutional Networks

    论文标题:Densely Connected Convolutional Networks 论文作者:Gao Huang Zhuang Liu Laurens van der Maaten  Kili ...

  10. Day 1-决胜IT十八招-前言

    走资讯这一行转眼间八年多了,从大学的时候,我有长达十年的时间思索在从事软体开發这一行到底怎麽存活下来,这思考下来,为自己总算找到一个出口来,这十八招只是其一的绝学,见阵这一行干软体开發的变化也很大,从 ...