Skip to content

计算属性实现 ​

本节目标:理解“计算属性实现”的核心思路,并能把它用于实际开发或面试表达。 计算属性的特点:缓存,如果依赖项没有变化,不会重新运行;所以计算属性中有一个缓存的标识,如果这个依赖有变化,要重新执行get,没有变化就不重新执行get; 这个属性是_dirty:是否是脏的;

计算属性是一个effect,依赖的属性变化了,会更新dirty的值; 计算属性可以放到一个effect中使用;

当计算属性依赖的值变化了,get函数会执行,同时依赖于计算属性的effect也会执行;

依赖的值变化了会触发计算属性effect重新执行, 计算属性重新执行的时候会触发外层effect来执行;

计算属性套计算属性也是一样的;
新建文件reactivity/src/computed.ts
import { isFunction } from "@vue/shared"
import { ReactiveEffect, trackEffects } from "./effect";
export const computed = (getterOrOptions) => {
  // 整理参数;
  // 先定义两个变量存储我们最终需要的值;
  // 根据用户传入的参数的不同,给予相应赋值;
  let getter;
  let setter;
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions;
    setter = () => { console.warn('no set') }
  } else {
    getter = getterOrOptions.get;
    setter = getterOrOptions.set;
  }
  // 调用computed函数实际上是创建一个ReactiveEffect实例,它不同于effect函数,默认不会执行;
  // effect函数默认会执行一次,即创建一个ReactiveEffect实例,然后调用实例run方法;
  // ReactiveEffect构造函数接受一个函数和一个scheduler作为参数;
  return new ComputedRefImpl(getter, setter);
}

到此,computed()执行完毕;也就是说如果我们不对computed()进行取值或赋值操作,computed的get和set是不会再执行的;实际工作中,计算属性如果没有被使用,是不会执行的;比如在模板中使用,且模板默认的v-if条件是true,如果是false,也相当于没有使用;

在packages/shared/src/index.ts中添加几个工具方法
export const isObject = (value) => {
  return typeof value === 'object' && value !== null;
}
export const isString = (value) => {
  return typeof value === 'string'
}
export const isNumber = (value) => {
  return typeof value === 'number'
}
export const isFunction = (value) => {
  return typeof value === 'function'
}
export const isArray = Array.isArray;
export const assign = Object.assign

computed返回的computedRefImpl实例只有一个属性,这个属性是访问器属性,只有get和set方法来定义对值的访问和修改; 当在计算属性上取Value属性时,会触发ComputedRefImpl实例的get value方法,在这个方法中,我们调用我们创建的ReactiveEffect的run方法,就是执行一下getter函数,这里会进行依赖收集,当前effect就是使用计算属性value的effect;我们把这个effect放到了计算属性实例的deps上,下次,我们在set方法中并这些effect依次执行;

class ComputedRefImpl {
  // 放在实例上便于调用
  public effect;
  public _dirty = true;
  public __v_readonly = true;
  public __v_isRef = true;
  public _value;
  // 默认使用空Set
  public dep = new Set();
  constructor(getter, public setter) {
    this.effect = new ReactiveEffect(getter, () => {
      // 使用了调度器,当依赖项变化时,执行这个调度器函数
    })
  }
  // 类中的属性访问器,底层就是Object.defineProperty
  // 计算属性使用的时候是使用.value来使用的,所以使用计算属性的地方
  // 会调用这个实例的get方法,在get方法中,我们调用实例上的effect的run方法
  // 这个run方法会进行依赖收集,
  // 也就是说取计算属性的value属性,会调用我们当初传入的getter函数;
  get value() {
    // 做依赖收集
    trackEffects(this.dep);
     // 使用_dirty来进行缓存逻辑;
    if (this._dirty) {
      this._dirty = false;
      this._value = this.effect.run();
    }
    return this._value;
  }
  set value(newValue) {
    // 直接调用setter
    this.setter(newValue)
  }
}

我们执行计算属性的getter函数时,已经进行了依赖收集;现在,我们修改计算属性的依赖项,自然会让ComputedRefImpl的effect这个effect重新执行getter函数,但由于有调度器的存在,我们执行的是我们创建effect时传入的调度器函数;

让依赖计算属性的effect,在计算属性变化时,能再执行,其它非effect实现,自然不用更新; 所以,我们在get value()方法中,进行依赖收集,如同我们在new Proxy的get方法中所做的一样;目的是一样的,让value记住effect,然后设置value的值时,重新执行effect。

我们将这个effect存在了实例的dep属性上,它的值是一个Set,里面存放了它影响到的effect;

get value() {
  // 做依赖收集
  ==trackEffects====(====this====.====dep====);==
  // 使用_dirty来进行缓存逻辑;
  if (this._dirty) {
    this._dirty = false;
    this._value = this.effect.run();
  }
  return this._value;
}
class ComputedRefImpl {
  // 放在实例上便于调用
  public effect;
  public _dirty = true;
  public __v_readonly = true;
  public __v_isRef = true;
  public _value;
  ==//== ==默认使用空====Set==
  ==public== ==dep== ===== ==new== ==Set====();==
trackEffects方法
export function trackEffects(dep) {
  if (activeEffect) {
    let shouldTrack = !dep.has(activeEffect); // 去重了
    if (shouldTrack) {
      dep.add(activeEffect);
      // 存放的是属性对应的set
      activeEffect.deps.push(dep);
    }
  }
}
接下来,当计算属性值变化时,我们要让dep里的effect执行;
constructor(getter, public setter) {
  this.effect = new ReactiveEffect(getter, () => {
    // 使用了调度器,当依赖项变化时,执行这个调度器函数
    // 重置
    if (!this._dirty) {
      this._dirty = true;
      ==//== ==实现触发更新==
      ==triggerEffects====(====this====.====dep====)==
    }
  })
}
triggerEffects代码
export function triggerEffects(effects) {
  effects = new Set(effects);
  effects.forEach(effect => {
    // 我们在执行effect的时候,又要执行自己,那我们需要屏蔽掉,不要无限调用
    if (effect !== activeEffect) {
      if (effect.scheduler) {
        // 如果用户传入了调度函数,则用用户的
        effect.scheduler();
      } else {
        effect.run() // 否则默认刷新视图
      }
    }
  });
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <script src="./reactivity.global.js"></script>
  <div id="app"></div>
  <script>
    let waiting = false;
    const { effect, reactive, computed } = VueReactivity;
    const state = reactive({ firstname: '姜', lastname: '文' });
    const fullName = computed(() => {
      console.log('runner')
      return state.firstname + state.lastname
    })
    effect(() => {
      app.innerHTML = fullName.value
    })
    setTimeout(() => {
      state.firname = '珠'
    }, 1000)
  </script>
</body>
</html>

本站总访问 — 次