【框架实现】vue3的computed

源码阅读与调试

计算属性computed会基于其响应式以来被缓存 ,并且在依赖的响应式数据发生变化时重新计算

创建一个computed的测试实例;

js 复制代码
const { reactive, effect, computed } = Vue

const obj = reactive({
  name: '张三'
})

const computedObj = computed(() => {
  return '姓名:' + obj.name
})

effect(() => {
  document.querySelector('#app').innerText = computedObj.value
})

setTimeout(() => {
  obj.name = '李四'
}, 2000)

reactive之前文章有详细描述,这里就不赘述了,直接来看本文的主题computed;

ts 复制代码
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  // isFunction判断是不是函数
  const onlyGetter = isFunction(getterOrOptions)
  // onlyGetter为true
  if (onlyGetter) {
    getter = getterOrOptions
    // 当前没有setter,所以它现在是一个空函数
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
  // 返回ComputedRefImpl的实例
  return cRef as any
}

第9行的getterOrOptions此时为我们computed传入的函数,如下图所示;

接着会进入new ComputedRefImpl

ts 复制代码
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true

  // _dirty变量非常重要
  public _dirty = true
  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    // 如果effect.run()执行就会触发getter方法,接着触发回调函数
    this.effect = new ReactiveEffect(getter, () => {
      // 如果this._dirty为false,执行triggerRefValue方法
      // this._dirty用来控制什么时候触发依赖
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  }
}

第20行中传入的getter就是我们在computed中传入的函数,如下图所示:

到这里computed函数就执行完成了;接着测试实例中是在effect方法中触发了computedObj的Getter,也就是class ComputedRefImpl的get方法;

ts 复制代码
export class ComputedRefImpl<T> {
  get value() {
    // self可以被看作this
    const self = toRaw(this)
    // 收集依赖
    trackRefValue(self)
    // _dirty为true
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      // 这里会执行computed中传入的函数
      self._value = self.effect.run()!
    }
    // self._value为"姓名:张三"
    return self._value
  }
  set value(newValue: T) {
    this._setter(newValue)
  }
}

第14行返回self._value,此时的self._value如下图所示:

到这里我们发现computed本质上也没有代理监听的机制,它也是通过get value和set value进行的触发;我们发现get value和set value中只有收集依赖没有触发依赖;触发依赖在class ComputedRefImpl的constructor中,也就是ReactiveEffect的回调函数里;这时我们去看一下ReactiveEffect都做了什么:

调度器相关逻辑,先来划重点:

我们继续调试跟踪代码,执行settimtout时,会触发Setter,也就是baseHandler文件中的createSetter方法;

createSetter方法=>trigger方法=>triggerEffects方法=>triggerEffect方法

看一下triggerEffects方法具体做了什么:

ts 复制代码
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    // 判断是否有computed
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

triggerEffect方法;

ts 复制代码
function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    // 判断是否有scheduler
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

第2行中effect如下图所示;它里面是包含一个scheduler的;因此它会执行scheduler的逻辑;这是会再次执行class ComputedRefImpl中constructor里的传入的函数;

scheduler也就是我们测试实例中computed传入的方法;

js 复制代码
const computedObj = computed(() => {
      return '姓名:' + obj.name
})

进入scheduler方法,也就是我们new ReactiveEffect传入的回调函数;

ts 复制代码
export class ComputedRefImpl<T> {
  constructor(
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
  }
}

第5行的this.dirty是false,会去触发依赖;会再次进入triggerRefValue方法;

triggerRefValue方法=>triggerEffects方法

ts 复制代码
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  const effects = isArray(dep) ? dep : [...dep]
  // 第一层for循环没有触发
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  // 触发第二层for循环
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

此时的effect的scheduler不存在了,fn也变成了effect中传入的方法;

fn此时如下图所示:

执行完effect,页面视图也从姓名:张三 变成了姓名:李四

触发依赖:

  1. 第一次settimeout中触发Setter;
  2. 第二次是在new ReactiveEffect的回调函数中触发;

computed的总结:

  1. computed的本质是一个ComputedRefImpl的实例;
  2. ComputedRefImpl中通过dirty变量来控制run的执行和triggerRefValue的触发;
  3. 想要访问计算属性的值,必须通过.value,因为它内部和ref一样是通过get value来进行实现的;
  4. 每次.value时都会触发trackRefValue即:收集依赖;
  5. 在触发依赖时,需要先触发computed的effect,再触发非computed的effect;

实现computed

实现class ComputedRefImpl

首先创建一个computed的测试实例:

js 复制代码
const { reactive, effect, computed } = Vue;

const obj = reactive({
  name: "张三",
});

const computedObj = computed(() => {
  return "姓名:" + obj.name;
});

effect(() => {
  document.querySelector("#app").innerText = computedObj.value;
});

setTimeout(() => {
  obj.name = "李四";
}, 2000);

创建packages/reactivity/src/computed.ts

ts 复制代码
import { isFunction } from "@vue/shared";
import { ReactiveEffect } from "./effect";
import { trackRefValue } from "./ref";
import { Dep } from "./dep";

export type ComputedGetter<T> = (...args: any[]) => T;

export function computed<T>(getterOrOptions: ComputedGetter<T>) {
  let getter: ComputedGetter<T>;

  const onlyGetter = isFunction(getterOrOptions);
  if (onlyGetter) {
    getter = getterOrOptions;
    const cRef = new ComputedRefImpl(getter);

    return cRef as any;
  }
}
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined;
  private _value!: T;
  private readonly effect: ReactiveEffect<T>;
  public _dirty = true;

  constructor(getter: ComputedGetter<T>) {
    this.effect = new ReactiveEffect(getter);
  }
  get value() {
    trackRefValue(this);
    return this._value;
  }
}

shared/index.ts中增加:

ts 复制代码
export const isFunction = (fn: unknown) => typeof fn === "function";

还有两个地方需要导出computed;

实现调度器及dirty相关逻辑

修改packages/reactivity/src/computed.ts;

ts 复制代码
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined;
  private _value!: T;
  private readonly effect: ReactiveEffect<T>;
  public _dirty = true;

  constructor(getter: ComputedGetter<T>) {
    this.effect = new ReactiveEffect(getter, () => {
      console.log("schedule");
      if (!this._dirty) {
        this._dirty = true;
        triggerRefValue(this);
      }
    });
    this.effect.computed = this;
  }
  get value() {
    trackRefValue(this);
    if (this._dirty) {
      this._dirty = false;
      this._value = this.effect.run();
    }
    return this._value;
  }
}

修改effect.ts;

ts 复制代码
export type EffectScheduler = (...args: any[]) => any;

export function triggerEffects(dep: Dep) {
  // 转化成数组
  const effects = isArray(dep) ? dep : [...dep];

  // 依次触发依赖
  for (const effect of effects) {
    triggerEffect(effect);
  }
}

// 触发指定依赖
export function triggerEffect(effect: ReactiveEffect) {
  // 判读是否有scheduler
  if (effect.scheduler) {
    effect.scheduler();
  } else {
    effect.run();
  }
}

export class ReactiveEffect<T = any> {
  computed?: unknown;
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null
  ) {}
}

此时测试实例就具有响应性了;

实现computed的缓存性

computed是有缓存性的,我们通过一个测试实例来看一下:

js 复制代码
const { reactive, effect, computed } = Vue

const obj = reactive({
  name: '张三'
})

const computedObj = computed(() => {
  console.log('计算属性执行')
  return '姓名:' + obj.name
})

effect(() => {
  document.querySelector('#app').innerText = computedObj.value
  document.querySelector('#app').innerText = computedObj.value
})

setTimeout(() => {
  obj.name = '李四'
}, 2000)

第一次log是computed初始化,第二次是setTimeout触发了Setter;说明vue3的computed是具有缓存性,此时我们可以用刚才实现computed来做对比实验;

triggerEffects方法:

第一遍执行,打印出effects:

第二遍执行,打印出effects:

schedule如果在run之后执行,就会疯狂执行computed中的方法,形成死循环;schedule中做了什么呢?schedule中把_dirty改成了true,并且又一次triggerRefValue;所以就会造成死循环;想到这里大家应该有解决办法了吧;只要改变一下schedule的执行顺序,问题就可以解决啦!

ts 复制代码
export function triggerEffects(dep: Dep) {
  // 转化成数组
  const effects = isArray(dep) ? dep : [...dep];

  // 依次触发依赖
  // 判断是否有computed
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect);
    }
  }

  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect);
    }
  }
}
相关推荐
M_emory_17 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito20 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员1 小时前
响应式网页设计--html
前端·html
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript
Martin -Tang3 小时前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发3 小时前
解锁微前端的优秀库
前端
王解4 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁4 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis