【框架实现】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);
    }
  }
}
相关推荐
m0_7482550213 分钟前
前端常用算法集合
前端·算法
真的很上进27 分钟前
如何借助 Babel+TS+ESLint 构建现代 JS 工程环境?
java·前端·javascript·css·react.js·vue·html
web1309332039833 分钟前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
NiNg_1_2341 小时前
Echarts连接数据库,实时绘制图表详解
前端·数据库·echarts
如若1232 小时前
对文件内的文件名生成目录,方便查阅
java·前端·python
滚雪球~2 小时前
npm error code ETIMEDOUT
前端·npm·node.js
沙漏无语2 小时前
npm : 无法加载文件 D:\Nodejs\node_global\npm.ps1,因为在此系统上禁止运行脚本
前端·npm·node.js
supermapsupport2 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
brrdg_sefg2 小时前
WEB 漏洞 - 文件包含漏洞深度解析
前端·网络·安全
胡西风_foxww2 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest