- A+
所属分类:Web前端
专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核?推荐?
欢迎各位ITer关注点赞收藏???
语法
传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象
const count = ref(1) const plusOne = computed(() => count.value + 1) console.log(plusOne.value) // 2 plusOne.value++ // 错误!
或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态
const count = ref(1) const plusOne = computed({ get: () => count.value + 1, set: (val) => { count.value = val - 1 }, }) plusOne.value = 1 console.log(count.value) // 0
源码实现
-
@issue1 computed参数兼容只传getter方法和handler对象的情况
-
@issue2 缓存特性,只要依赖的变量值没有发生变化,就取缓存中的值
_dirty
作为缓存标识,如果依赖的变量值有变化,则将_dirty
值置为 true,后续读取计算属性时,重新执行getter;否则直接取_value
值 -
@issue3 嵌套effect,firstname -> 计算属性fullName -> effect,下一章节详细介绍
import { isFunction } from '@vue/shared' import { ReactiveEffect, trackEffects, triggerEffects } from './effect' /** * @issue1 computed参数兼容只传getter方法和handler对象 * @issue2 缓存,只要依赖的变量值没有发生变化,就取缓存中的值 * @issue3 嵌套effect,firname -> fullName -> effect */ class ComputedRefImpl { public effect public _dirty = true // 默认应该取值的时候进行计算 public _value public dep = new Set() public __v_isReadonly = true public __v_isRef = true constructor(public getter, public setter) { // 我们将用户的getter放到effect中,这里面firstname和lastname就会被这个effect收集起来 this.effect = new ReactiveEffect(getter, () => { // 稍后依赖的属性firstname、lastname变化了,会执行此调度函数 if (!this._dirty) { this._dirty = true // 实现一个触发更新 @issue3 triggerEffects(this.dep) } }) } // 类中的访问器属性 底层就是Object.defineProperty // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/get get value() { // 做依赖收集 @issue3 trackEffects(this.dep) // @issue2 if (this._dirty) { // 说明这个值是脏的 this._dirty = false this._value = this.effect.run() } return this._value } set value(newValue) { this.setter(newValue) } } export const computed = getterOrOptions => { let onlyGetter = isFunction(getterOrOptions) let getter let setter // @issue1 if (onlyGetter) { getter = getterOrOptions setter = () => { console.warn('no set') } } else { getter = getterOrOptions.get setter = getterOrOptions.set } return new ComputedRefImpl(getter, setter) }
trackEffects 和 triggerEffects 方法如下
export function trackEffects(dep) { // 收集dep 对应的effect if (activeEffect) { let shouldTrack = !dep.has(activeEffect) // 去重了 if (shouldTrack) { dep.add(activeEffect) // 存放的是属性对应的set activeEffect.deps.push(dep) // 让effect记录住对应的dep, 稍后清理的时候会用到 } } } export function triggerEffects(effects) { effects = new Set(effects); for (const effect of effects) { if (effect !== activeEffect) { // 如果effect不是当前正在运行的effect if (effect.scheduler) { effect.scheduler() } else { effect.run(); // 重新执行一遍 } } } }
嵌套 effect
让我们分析一下这个测试用例
const { effect, reactive, computed } = VueReactivity const state = reactive({ firname: '李', lastname: '柏成' }) const fullName = computed(() => { // defineProperty中的getter return state.firstname + state.lastname }) effect(() => { app.innerHTML = fullName.value }) setTimeout(() => { state.firstname = '王' }, 1000) // 1. firstname要依赖于计算属性的effect // 2. 计算属性收集了外层effect // 3. 依赖的值变化了会触发计算属性effect重新执行, 计算属性重新执行的时候会触发外层effect来执行 // computed 特点:缓存 console.log('fullName.value', fullName.value) console.log('fullName.value', fullName.value)
- 当执行到 renderEffect 时,默认先执行一次 effect.run(),activeEffect --> renderEffect,并运行 this.fn() -->
app.innerHTML = fullName.value
effect(() => { app.innerHTML = fullName.value })
- 当访问 fullName.value 时,在 getter 方法中执行 trackEffects(this.dep),计算属性fullName 依赖收集 当前的 activeEffect(renderEffect)
- 当运行
this._value = this.effect.run()
时,activeEffect --> computedEffect,并运行 this.fn() --->return state.firstname + state.lastname
- 访问了state.firstname,属性 firstname 依赖收集当前的 activeEffect(computedEffect)
- 访问了state.lastname,属性 lastname 依赖收集当前的 activeEffect(computedEffect)
- 一秒钟后,firstname 发生了变化。。。firstname变化触发更新 triggerEffects --> computedEffect.scheduler()
- 在计算属性 scheduler 中,触发更新 triggerEffects(this.dep) --> renderEffect.run() ,最终重新渲染页面
app.innerHTML = fullName.value