computed的简易版 - 源码系列9

接着写 computed!

目标 computed

首先看下,目标 computed 的功能:

js 复制代码
import {
  reactive,
  effect,
  watch,
  watchEffect,
  computed,
} from '../../../node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js';
// import { reactive, effect,watch,watchEffect } from './reactivity.js';
const obj = reactive({
  firstName: 'hua',
  lastName: ' yan',
});
const fullName = computed({
  get() {
    console.log('getter');
    return obj.firstName + obj.lastName;
  },
  set(newVal) {
    console.log('computed里的set', newVal);
  },
});

obj.firstName = 'hua1';
console.log(fullName);
console.log(fullName.value);
console.log(fullName.value);

const fullName2 = computed(() => {
  console.log('getter2');
  return obj.firstName + obj.lastName;
});
console.log(fullName2);
console.log(fullName2.value);
console.log(fullName2.value);

computed的应用场景是根据其他数据衍生的,比如说根据 firstName 和 lastName 衍生出 fullName computed的特点是有缓存,只有当依赖的数据发生变化时才会重新计算

分析 computed

  • computed 是一个函数,1 个参数,对象或者函数
  • 是函数的话,相当于一个 get
  • 是对象的话,能有 get 和 set
  • 返回值是ComputedRefImpl实例

写 computed

1. 可以正常返回新值

computed先分开gettersetter,然后返回ComputedRefImpl实例。 ComputedRefImpl实例属性 value 的时候,可以得到值,这里其实直接借用之前的ReactiveEffect,根据依赖属性,可以自动执行函数。

reactivity/src/computed下:

js 复制代码
import { ReactiveEffect } from './effect';
import { isFunction } from './reactive';

export function computed(getterOrSetterOptions){
  let getter
  let setter
  if (isFunction(getterOrSetterOptions)) {
    getter = getterOrSetterOptions
    setter = ()=>{console.log('空')}
  }else{
    getter = getterOrSetterOptions.get
    setter = getterOrSetterOptions.set
  }
  return new ComputedRefImpl(getter, setter)
}

export class ComputedRefImpl {
  private _value
  private effect
  constructor(getter,setter){
    this.effect = new ReactiveEffect(getter,()=>{})

  }
  get value(){
    this._value = this.effect.run()
    return this._value
  }
}

换回自己的,试试!

值变化,可以拿到新的值。但是,会多次执行依赖函数,没有缓存效果。

2. 实现缓存效果

_dirty来标识需不需要重新计算,不需要的话直接返回,就不再执行一次 effect

默认的时候,_dirty为 true,执行的时候,设置为 false。 属性变化的时候,将_dirty重新为 true。

js 复制代码
export class ComputedRefImpl {
  private _value
  private effect
  // dirty是脏的意思,代表是否需要重新计算,true表示需要重新计算
  private _dirty = true
  constructor(getter){
    this.effect = new ReactiveEffect(getter,()=>{
      // 依赖的属性发生变化的时候,会执行这里。属性变化,表明被污染,需要重新计算
      if(!this._dirty){
        this._dirty = true
      }
    })

  }
  get value(){
    if(this._dirty){
      this._value = this.effect.run()
      this._dirty = false
    }
    return this._value
  }
}

刷新,试试!

3.处理 setter

index.html 里设置 fullName 的值

js 复制代码
fullName.value = '1212';
console.log(fullName.value);

刷新下,发现报错了!

因为类只有get value,没有set value,加上就好啦啦,注意,这里还需要执行下computed的传参setter

js 复制代码
export class ComputedRefImpl {
  // ...
  constructor(getter,public setter){
    //...
  }
  set value(newVal){
    this._value = newVal;
    this.setter(newVal)
  }
}

刷新下就好了!

4.对 setter 的监测,收集依赖

但这里引发另外一个问题,fullName主动设置值的时候,是没有监测的,举个例子

index.html 里 effect 下 fullName 的值

js 复制代码
effect(() => {
  console.log('fullName变化的时候', fullName.value);
});
fullName.value = '1212';
console.log(fullName.value);

根本没变化!

最核心需要做两个方面:

  1. 读取fullName的时候,收集相应effect(拿个 Set 放进去)
  2. fullName赋值的时候,将收集的efffec挨个执行!

步骤:

  1. 先用dep属性,来收集effect
  2. get value的时候,如果当前有effect运行,那么就把这个 effect 收集到dep
  3. set value的时候,dep容器里effect挨个执行
js 复制代码
export class ComputedRefImpl {
  // computed fullName的话,收集fullName相关的effect
  public dep:Set<ReactiveEffect> = new Set()
  get value(){

    if(this._dirty){
      this._value = this.effect.run()
      this._dirty = false
    }
    // 读取value的时候,收集effect
    if(activeEffect && !this.dep.has(activeEffect)){
      this.dep.add(activeEffect)
      // effect的dep里面也要收集computed的dep
      activeEffect.deps.push(this.dep)
    }
    return this._value
  }
  set value(newVal){
    this._value = newVal;
    this.setter(newVal);
    // 设置value的时候,触发dep的effect挨个执行
    this.dep.forEach(effect => {
      if(effect!==activeEffect){
        effect.scheduler?effect.scheduler():effect.run();
      }
    });
  }
}

刷新下,可以啦!

5.优化

effect 和 computed,收集和触发的逻辑是一致的,代码是尽量避开重复的,所以这里将收集和触发抽离出来。

ts 复制代码
/**
 * dep收集effect
 */
export function trackEffects(dep: Set<ReactiveEffect>) {
  if (activeEffect && !dep.has(activeEffect)) {
    // 收集effect
    dep.add(activeEffect);
    // effect同样收集下dep
    activeEffect.deps.push(dep);
  }
}
/**
 * dep执行触发effect
 */
export function triggerEffects(dep: Set<ReactiveEffect>) {
  // 防止死循环,所以这边浅拷贝
  [...dep].forEach((effect) => {
    const isRunning = activeEffect === effect;
    if (!isRunning) {
      effect.scheduler ? effect.scheduler() : effect.run();
    }
  });
}

同时调整computedeffect,刷新下页面,还是没问题滴!

将 reactivty 文件夹具体代码展示

index.ts

ts 复制代码
export * from './reactive';
export * from './effect';
export * from './apiWatch';
export * from './computed';

reactive.ts

ts 复制代码
// import { isObject } from './shared'
import { track, trigger } from './effect';
export const isObject = (param) => {
  return typeof param === 'object' && param !== null;
};
export const isFunction = (param) => {
  return typeof param === 'function';
};
const __v_isReactive = '__v_isReactive';
// 是不是响应式对象
export const isReactive = (param) => param[__v_isReactive];

// 代理对象的映射
export const reactiveMap = new WeakMap();

export function reactive(target) {
  // 如果不是对象,直接返回
  if (!isObject(target)) {
    return;
  }

  // 如果已经代理过了,直接返回
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target);
  }

  // 如果已经代理过了,__v_isReactive肯定是true,那直接返回
  if (target[__v_isReactive]) {
    return target;
  }
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      // 这里埋点,加上__v_isReactive属性,标识已经代理过了
      if (key === __v_isReactive) {
        return true;
      }
      // Reflect将target的get方法里的this指向proxy上,也就是receiver
      const res = Reflect.get(target, key, receiver);
      // 依赖收集
      track(target, key);
      // 如果是对象,递归代理
      if (isObject(res)) {
        return reactive(res);
      }
      return res;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];
      const r = Reflect.set(target, key, value, receiver);
      // 响应式对象发生变化的时候,触发effect执行
      if (oldValue !== value) {
        trigger(target, key);
      }
      return r;
    },
  });
  // 如果没有代理过,缓存映射
  reactiveMap.set(target, proxy);
  return proxy;
}

effect.ts

ts 复制代码
// track的时候,需要拿到effect,所以用下全局变量存放effect
export let activeEffect: ReactiveEffect | null = null;
// 建立类,方便存放fn,和运行
/**
 * fn是函数,收集属性依赖,scheduler是函数,属性依赖变化的时候,执行
 * 属性deps是个二维数组,结构是 [[_effect1,_effect2],[_effect3,_effect2],]
 */
export class ReactiveEffect {
  // 是否主动执行
  private active = true;
  // 新增deps
  deps = [];
  parent;
  constructor(private fn, public scheduler) {}

  run() {
    if (!this.active) {
      const res = this.fn();
      // 这里watch的时候,fn是函数返回字段,需要返回值
      return res;
    }

    this.parent = activeEffect;
    activeEffect = this;
    // 运行之前,清除依赖
    clearupEffect(this);
    const res = this.fn();
    activeEffect = this.parent;
    this.parent && (this.parent = null);
    return res;
  }
  stop() {
    if (this.active) {
      // 清除依赖
      clearupEffect(this);
      // 标记不主动执行
      this.active = false;
    }
  }
}

// 清除依賴
function clearupEffect(_effect) {
  // deps结构是 [[_effect1,_effect2],[_effect3,_effect2],],假设去掉_effect2
  const deps = _effect.deps;
  for (let i = 0; i < deps.length; i++) {
    deps[i].delete(_effect);
  }
  // 同时deps置空,保证每次effect运行都是新的属性映射
  _effect.deps.length = 0;
}

// }
export function effect(fn, options) {
  const _effect = new ReactiveEffect(fn, options?.scheduler);
  _effect.run();
  // runner是个函数,等同于_effect.run,注意绑定this
  const runner = _effect.run.bind(_effect);
  // runner还有effect属性,直接赋值就好
  runner.effect = _effect;
  return runner;
}

// 本质是找到属性对应的effect,但属性存在于对象里,所以两层映射
// 响应性对象 和 effect的映射,对象属性和effect的映射
// targetMap = { obj:{name:[effect],age:[effect]} }
export const targetMap: WeakMap<
  object,
  Map<string, Set<ReactiveEffect>>
> = new WeakMap();

// 让属性 订阅 和自己相关的effect,建立映射关系
export function track(target, key) {
  if (!activeEffect) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  trackEffects(dep);
  // 这属性track过了
  // if (dep.has(activeEffect)) {
  //   return;
  // }
  // // 核心代码,属性 订阅 effect (本质就是建立映射关系),上面一坨就是判断加初始化
  // dep.add(activeEffect);
  // // 新增deps
  // activeEffect.deps.push(dep);
}

/**
 * dep收集effect
 */
export function trackEffects(dep: Set<ReactiveEffect>) {
  if (activeEffect && !dep.has(activeEffect)) {
    // 收集effect
    dep.add(activeEffect);
    // effect同样收集下dep
    activeEffect.deps.push(dep);
  }
}
/**
 * dep执行触发effect
 */
export function triggerEffects(dep: Set<ReactiveEffect>) {
  [...dep].forEach((effect) => {
    const isRunning = activeEffect === effect;
    if (!isRunning) {
      effect.scheduler ? effect.scheduler() : effect.run();
    }
  });
}

// 属性值变化的时候,让相应的effect执行
export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (!dep) {
    return;
  }
  // 触发执行
  triggerEffects(dep);
}

apiWatch.ts

ts 复制代码
import { ReactiveEffect } from './effect';
import { isReactive, isFunction } from './reactive';
// watch字段,提取成一个函数
function watchFunction(source, cb) {
  // source是函数-返回字段,()=>obj.name,就是fn直接执行,收集属性
  // scheduler只有在属性发生变化的时候,才会执行
  let oldValue;
  let newValue;
  const _effect = new ReactiveEffect(source, () => {
    // scheduler执行的时候,属性值已经发生变化,最新值通过run方法获取,等同于source()
    newValue = _effect.run();
    cb(newValue, oldValue);
    oldValue = newValue;
  });
  // watch的时候,首先这里执行一次,收集依赖 ()=>obj.name
  oldValue = _effect.run();
}

function watchReactive(source, cb) {
  // TODO 循环引用没看明白 之后再补吧
  for (const key in source) {
    watchFunction(
      () => source[key],
      () => {
        cb(source, source);
      }
    );
  }
}

export function watchEffect(cb) {
  const _effect = new ReactiveEffect(cb, null);
  _effect.run();
}
export function watch(source, cb) {
  isFunction(source) && watchFunction(source, cb);
  isReactive(source) && watchReactive(source, cb);
}

computed.ts

ts 复制代码
import {
  ReactiveEffect,
  activeEffect,
  trackEffects,
  triggerEffects,
} from './effect';
import { isFunction } from './reactive';

export function computed(getterOrSetterOptions) {
  let getter;
  let setter;
  if (isFunction(getterOrSetterOptions)) {
    getter = getterOrSetterOptions;
    setter = () => {
      console.log('空');
    };
  } else {
    getter = getterOrSetterOptions.get;
    setter = getterOrSetterOptions.set;
  }
  return new ComputedRefImpl(getter, setter);
}

export class ComputedRefImpl {
  private _value;
  private effect;
  // dirty是脏的意思,代表是否需要重新计算,true表示需要重新计算
  private _dirty = true;
  // computed fullName的话,收集fullName相关的effect
  public dep: Set<ReactiveEffect> = new Set();
  constructor(getter, public setter) {
    this.effect = new ReactiveEffect(getter, () => {
      // 依赖的属性发生变化的时候,会执行这里。属性变化,表明被污染,需要重新计算
      if (!this._dirty) {
        this._dirty = true;
      }
    });
  }
  get value() {
    if (this._dirty) {
      this._value = this.effect.run();
      this._dirty = false;
    }
    // 读取value的时候,收集effect
    trackEffects(this.dep);

    return this._value;
  }
  set value(newVal) {
    if (newVal === this._value) {
      return;
    }
    this._value = newVal;
    this.setter(newVal);
    // 设置value的时候,触发dep的effect挨个执行
    triggerEffects(this.dep);
  }
}
相关推荐
别拿曾经看以后~11 分钟前
【el-form】记一例好用的el-input输入框回车调接口和el-button按钮防重点击
javascript·vue.js·elementui
我要洋人死14 分钟前
导航栏及下拉菜单的实现
前端·css·css3
川石课堂软件测试16 分钟前
性能测试|docker容器下搭建JMeter+Grafana+Influxdb监控可视化平台
运维·javascript·深度学习·jmeter·docker·容器·grafana
科技探秘人25 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人26 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR31 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香33 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969336 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai41 分钟前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
problc1 小时前
Flutter中文字体设置指南:打造个性化的应用体验
android·javascript·flutter