小狐狸学mini-vue(一、响应式模块)

01、集成 jest 测试环境

bash 复制代码
初始化tsconfig.json 文件

npx tsc --init

编写一个 ts 函数,和编写一个测试用例,并让其通过,使用jest这个库。

安装 jest

bash 复制代码
yarn add --dev jest

因为jest默认使用的是 commonjs 规范,所以我们需要使用babel来进行转换,

bash 复制代码
yarn add --dev babel-jest @babel/core @babel/preset-env

使用ts

bash 复制代码
yarn add --dev @babel/preset-typescript


yarn add --dev @types/jest

在项目下面新建babel.config.js

js 复制代码
module.exports = {
  presets: [
    ["@babel/preset-env", { targets: { node: "current" } }],
    "@babel/preset-typescript",
  ],
};

编写一个ts函数

关闭强制检查any

tsconfig.json

ts 复制代码
"noImplicitAny": false

"types": ["jest"],

编写一个 index.ts代码

ts 复制代码
export function add(a, b) {
  return a + b;
}

编写index.spec.ts

ts 复制代码
import { add } from "..";

it("init", () => {
  expect(add(1, 4)).toBe(5);
});

添加 script脚本

json 复制代码
  "scripts": {
    "test": "jest"
  },

然后在命令行执行yarn test, 通过则表示测测试环境搭建成功了。

02、实现effect&reactive 依赖收集&触发更新

  1. 怎么实现依赖收集呢?

其实就是用一个容器,把effect里面传递的这个fn函数收集起来,就可以了。

  1. 怎么拿到当前正在执行的依赖信息呢?

在这里我们使用一个全局变量activeEffect在运行的时候来记录它。然后在别的地方使用。

  1. 用一个来抽象出effect 叫做EffectEffect.

  2. get里面进行依赖收集track, 在set里面进行trigger

目标实现下面的这两个测试用例

reactive

ts 复制代码
import { reactive } from "../reactive";

describe("reactive", () => {
  test("Object", () => {
    const original = { foo: 1 };
    const observed = reactive(original);

    expect(observed).not.toBe(original);

    // get
    expect(observed.foo).toBe(1);

    // has
    expect("foo" in observed).toBe(true);

    // ownKeys
    expect(Object.keys(observed)).toEqual(["foo"]);
  });
});

effect.spec.ts

ts 复制代码
import { reactive } from "../reactive";
import { effect } from "../effect";
describe("effect", () => {
  it("should observed basic properties", () => {
    let dummy;
    const counter = reactive({ num: 0 });

    // init setup
    effect(() => {
      dummy = counter.num;
    });
    expect(dummy).toBe(0);

    // update
    counter.num = 10;
    expect(dummy).toBe(10);
  });
});

好接下来我们实现reactive.ts

ts 复制代码
import { track, trigger } from "./effect";
export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key, receiver) {
      // 解释一下 receiver 这个参数, Proxy 或者继承 Proxy 的对象
      // 也就是代理后产生的新proxy对象
      let res = Reflect.get(target, key, receiver);
      //先读,再依赖收集
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      let result = Reflect.set(target, key, value, receiver);
      // 先设置,再触发更新
      trigger(target, key);
      return result;
    },
  });
}

effect.ts

要点

  1. ReactiveEffect包装依赖函数
  2. 存储依赖新的的targetMapMap变量,vue使用的WeakMap
  3. 全局变量activeEffect用于在依赖函数执行前记录依赖,方便后面再track函数中使用,当依赖函数执行完成之后还必须的重置为null保证activeEffect只是在依赖韩式执行的时候有用。因为js是单线程的特性。
ts 复制代码
import { add } from "../index";
// 记录正在执行的effect
let activeEffect: any = null;

// 包装依赖信息
class ReactiveEffect {
  private _fn: any;

  constructor(fn: any) {
    this._fn = fn;
  }

  run() {
    activeEffect = this;
    this._fn();
    activeEffect = null;
  }
}
// 存储依赖信息   target key  fn1, fn2
const targetMap = new Map();

export function track(target, key) {
  let depsMap = targetMap.get(target);

  // 根据 target 取出 target对象的依赖
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  // 在根据 key 取出 key 的依赖
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  // 把依赖添加到 dep 的 set 中
  dep.add(activeEffect);
}

// 找出依赖信息依次执行
export function trigger(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) return;

  let dep = depsMap.get(key);

  for (const effect of dep) {
    effect.run();
  }
}

export function effect(fn) {
  const _effect = new ReactiveEffect(fn);

  // 调用effect 传递的这个函数
  _effect.run();
}

好啦,我们完美的通过了两个测试用例

03、实现effect返回runner

单侧

ts 复制代码
  it("effect has return runner function", () => {
    let foo = 10;
    const runner = effect(() => {
      foo++;
      return "foo";
    });

    // 第一次执行
    expect(foo).toBe(11);

    // 验证 runner
    const r = runner();
    expect(foo).toBe(12);
    expect(r).toBe("foo");
  });

仓库

假设现在我们有以下代码,

js 复制代码
import { reactive, effect } from "../../dist/guide-mini-vue.esm.js";

let obj1 = reactive({ a: 100, b: 200 });

let obj2 = reactive({ c: 1 });

// a 属性在 四个 effect 中用了
// c 属性 在一个 effect 中使用了

a 属性有在四个 effect 中使用了,所以有四个

effect(() => {
 // 访问属性的时候,触发各自的依赖收集,但是共同拥有一个 ReactiveEffect
 // 所以 track 函数就会走两次 所以 deps 里面就有两个 set
  console.log(obj1.a);  
  console.log(obj2.c);
});

effect(() => {
  console.log(obj1.a);
});

set 关系

deps 关系

同理我们可以推测,a 属性依赖 Set 的第一个 ReactiveEffectc 属性 依赖 set 的第一个 ReactiveEffect 是同一个对象

这里再来画一张图来整理一下targetMapReactiveEffect 之间的关系。

所以到这里是不是更加清楚 ReactEffectdeps的作用了?

收集当前副作用函数所有的依赖集合 。一个属性的依赖是一个 Set ,所有的依赖集合就是[Set1, set2]

04、实现effect的scheduler

单侧

ts 复制代码
  it("scheduler", () => {
    // 功能的描述

    // 1. 通过 effect 的第二个对象参数,传入一个 scheduler 函数
    // 2. effect 第一次执行的时候 还是会执行 fn 函数
    // 3. 当响应式对象 set 的时候不会 执行 fn 而是执行 scheduler 函数
    // 4. 在执行 scheduler 函数的时候,我们记录一下 runner , 并调用 runner 函数,那么 fn 函数会再次执行的。

    let dummy: any;
    let run: any;
    const scheduler = jest.fn(() => {
      run = runner;
    });

    const obj = reactive({ foo: 1 });
    const runner = effect(
      () => {
        dummy = obj.foo;
      },
      {
        scheduler,
      }
    );

    // 第一次执行 fn 的时候 scheduler 不会执行
    expect(scheduler).not.toHaveBeenCalled();
    // 第一次调用的时候 fn 会执行
    expect(dummy).toBe(1);

    // 当调用 set 的时候, 触发的是 scheduler 函数的执行
    obj.foo++;
    expect(scheduler).toHaveBeenCalledTimes(1);

    expect(dummy).toBe(1);

    // manually run
    run();
    expect(dummy).toBe(2);

    // 如何实现呢?

    // 首先给 effect 添加第二个参数
    // 其次 当响应式数据 set 的时候,检测如果有scheduler 则执行 scheduler 函数, 不再触发更新
  });

仓库

05、实现effect的stop

先写两个单侧

ts 复制代码
  it("stop", () => {
    let dummy: any;

    const obj = reactive({ foo: 1 });

    const runner = effect(() => {
      dummy = obj.foo;
    });
    obj.foo = 2;

    expect(dummy).toBe(2);

    stop(runner);

    obj.foo = 3;
    expect(dummy).toBe(2);
  });

  // 调用stop 之后的回调函数
  it("onStop", () => {
    const obj = reactive({ foo: 1 });
    const onStop = jest.fn();

    let dummy: any;
    const runner = effect(
      () => {
        dummy = obj.foo;
      },
      {
        onStop,
      }
    );

    stop(runner);

    // 判断 onStop 是否被调用?

    expect(onStop).toBeCalledTimes(1);
  });

shared/index.ts

ts 复制代码
export const extend = Object.assign;

记录要点:

  1. activeEffect只在执行effect函数的时候才会有,假如只是属性的访问触发的get,则是没有activeEffect的。
  2. ReaciveEffect对象也记住依赖函数 (dep)
  3. 调用stop的事情清空里面的dep依赖就好了。

06、实现readonly功能

ts 复制代码
  it("readonly", () => {
    let original = { foo: 1 };
    console.warn = jest.fn();
    const obj = readonly(original);

    expect(obj).not.toBe(original);
    expect(obj.foo).toBe(1);

    // set
    obj.foo = 2;
    expect(console.warn).toBeCalled();
  });

记录要点:

  1. 是只读的就不需要set触发依赖,那么同样也不需要进行set里面的track依赖收集。

reactive.ts

ts 复制代码
export function readonly(raw: any) {
  return new Proxy(raw, readonlyHandler);
}

baseHandlers.ts

ts 复制代码
import { track, trigger } from "./effect";

function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    let res = Reflect.get(target, key, receiver);
    //先读,再依赖收集
    if (!isReadonly) {
      track(target, key);
    }
    return res;
  };
}

function createSetter(isReadonly = false) {
  return function set(target, key, value, receiver) {
    let result = Reflect.set(target, key, value, receiver);
    // 先设置,再触发更新
    if (!isReadonly) {
      trigger(target, key);
    }
    return result;
  };
}

const get = createGetter();
const set = createSetter();

const readonlyGetter = createGetter(true);
export const reactiveHandler = {
  get,
  set,
};

export const readonlyHandler = {
  get: readonlyGetter,
  set(target, key, value, receiver) {
    console.warn(`不能修改 ${String(key)},因为他是readonly的`);
    return true;
  },
};

07、实现isReactive和isReadonly

记录要点:

  1. 在调用isReactive函数的时候,随意访问一下,被代理对象上面的一个属性,都会触发get方法,我们利用这一点,在函数内部访问不同的keyget里面做判断,并根据baseHandle函数的参数isReadonly进行区分,是否是readonly的。
ts 复制代码
  it("test isReactive", () => {
    const original = { foo: 1 };
    const obj = reactive(original);

    expect(original).not.toBe(obj);
    expect(obj.foo).toBe(1);
    expect(isReactive(original)).toBe(false);
    expect(isReactive(obj)).toBe(true);
  });

  it("test isReadonly", () => {
    const original = { foo: 1 };
    const obj = readonly(original);

    expect(original).not.toBe(obj);
    expect(obj.foo).toBe(1);
    expect(isReadonly(obj)).toBe(true);
    expect(isReadonly(original)).toBe(false);
  });

reactive.ts

ts 复制代码
export function isReactive(value: any) {
  // 两个 !! 是将 假值转化为false
  return !!value[ReactiveFlag.IS_REACTIVE_FLAG];
}

export function isReadonly(value: any) {
  return !!value[ReactiveFlag.IS_READONLY_FLAG];
}

basehandle.ts

ts 复制代码
function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    let res = Reflect.get(target, key, receiver);

    if (key === ReactiveFlag.IS_REACTIVE_FLAG) {
      return !isReadonly;
    } else if (key === ReactiveFlag.IS_READONLY_FLAG) {
      return isReadonly;
    }
    //先读,再依赖收集
    if (!isReadonly) {
      track(target, key);
    }
    return res;
  };
}

08、优化stop功能

记录要点:

1.添加一个变量shouldTrack用来控制是否需要收集依赖?因为如果执行了obj.foo++,则不仅会触发set也会触发get操作。shouldTrack默认是关闭的(false)在执行effect函数之前开启,在依赖函数执行完成之后重置为false

  1. 并且在收集依赖的函数(track)中进行判断,如果shouldTrack = false,则直接return掉。
ts 复制代码
  it("enhanced stop", () => {
    let dummy: any;
    const obj = reactive({ foo: 1 });
    const runner = effect(() => {
      dummy = obj.foo;
    });

    stop(runner); // 这里我们明明
    obj.foo++;   
    expect(dummy).toBe(1);
  });

effect.ts

ts 复制代码
  run() {
    // 运行 run 的时候,可以控制 要不要执行后续收集依赖的一步
    // 目前来看的话,只要执行了 fn 那么就默认执行了收集依赖
    // 这里就需要控制了

    // 执行 fn  但是不收集依赖
    if (!this.active) {
      return this._fn();
    }
    // 可以进行收集依赖了
    shouldTrack = true;

    // 记录全局正在执行的依赖
    activeEffect = this;
    let r = this._fn();
    //重置
    shouldTrack = false;
    // activeEffect = null; //???????
    return r;
  }

09、实现 reactive 和 readonly 嵌套对象转换功能

记录要点:如果通过Reflect.get()得到的是一个对象,则需要递归代理结果,并返回。

ts 复制代码
  it("deep reactive", () => {
    const original = {
      foo: {
        bar: 1,
      },
      array: [{ bar: 2 }],
    };
    const obj = reactive(original);

    // 检测里面的 对象是不是一个 reactive对象
    expect(isReactive(obj.foo)).toBe(true);

    expect(isReactive(obj.array)).toBe(true);

    expect(isReactive(obj.array[0])).toBe(true);
  });
ts 复制代码
  it("deep readonly", () => {
    const original = {
      foo: {
        bar: 1,
      },
      array: [
        {
          var: 10,
        },
      ],
    };

    let obj = readonly(original);

    expect(isReadonly(obj.foo)).toBe(true);

    expect(isReadonly(obj.array)).toBe(true);

    expect(isReadonly(obj.array[0])).toBe(true);
  });

baseHandler.ts

ts 复制代码
function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    ......
    //如果得到的是对象,那么还需要递归
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}

10、实现 shallowReadonly 功能

记录要点:

  1. 被他代理的对象是浅层的,并且不能被set,所以我们要创建一个shallowReadonlyGet
ts 复制代码
  it("shallowReadonly", () => {
    const original = {
      n: {
        foo: 1,
      },
    };
    const obj = shallowReadonly(original);

    expect(isReadonly(obj)).toBe(true);

    expect(isReadonly(obj.n)).toBe(false);
  });

实现

ts 复制代码
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    let res = Reflect.get(target, key, receiver);
    .....

    // 如果是shallow 的则直接 return
    if (shallow) {
      return res;
    }
    ......
  };
}

11、实现 isProxy

要点总结:

  1. 只需要判断传进来的对象是否符合isReactive或者isReadonly即可
ts 复制代码
  it("isProxy", () => {
    const original = {
      foo: 1,
    };
    const obj = reactive(original);

    const readonlyObj = readonly({ value: 100 });

    expect(isProxy(obj)).toBe(true);
    expect(isProxy(readonlyObj)).toBe(true);
  });

实现

ts 复制代码
export function isProxy(raw: any) {
  return isReactive(raw) || isReadonly(raw);
}

12、实现ref

注意要点:

  1. ref里面必须有一个key对应一个deps
  2. ref接收的是一个对象时候,它内部会调用reactive进一步处理。
  3. ref的参数一般都是一个单值,单值的话,我们就没办法使用Proxy来进行代理了,所以作者就使用了一个类,在内部实现get valueset value,这也就是为什么单值需要.value的原因了。
js 复制代码
describe("ref", () => {
  it("should create a ref", () => {
    const foo = ref(1);
    expect(foo.value).toBe(1);
  });

  it("ref can effect", () => {
    const foo = ref(1);

    let dummy = 0;

    effect(() => {
      dummy = foo.value;
    });

    expect(dummy).toBe(1);
    foo.value = 2;
    expect(dummy).toBe(2);

    foo.value = 2;

    expect(dummy).toBe(2);
  });

  it("should support properties reactive", () => {
    const foo = ref({
      bar: 1,
    });

    let dummy = 0;

    effect(() => {
      dummy = foo.value.bar;
    });

    expect(dummy).toBe(1);
    foo.value.bar = 2;
    expect(dummy).toBe(2);

    foo.value.bar = 2;

    expect(dummy).toBe(2);
  });
});

实现

js 复制代码
import { track, trackEffects, triggerEffects } from "./effect";
import { isObject } from "../shared/index";
import { reactive } from "./reactive";
class RefImpl {
  private _value: any;

  // 依赖函数存放的位置是在  ref 的 deps 属性上
  private deps: Set<any> = new Set();

  constructor(value) {
    // 在初始化 ref 的时候要判断是不是一个object
    this._value = isObject(value) ? reactive(value) : value;
  }

  get value() {
    // 收集依赖
    trackEffects(this.deps);
    return this._value;
  }

  set value(newValue) {
    this._value = isObject(newValue) ? reactive(newValue) : newValue;
    // 触发依赖
    triggerEffects(this.deps);
  }
}

export function ref(value: any) {
  return new RefImpl(value);
}

13、实现isRef 和 unRef

要点记录:

  1. ref类上面加个表示就可以区分实例对象是不是ref类型的

这两个功能很简单

js 复制代码
  it("isRef", () => {
    const foo = ref(1);
    expect(isRef(2)).toBe(false);
    expect(isRef(foo)).toBe(true);
  });

  it("unRef", () => {
    const foo = ref(1);
    expect(unRef(foo)).toBe(1);
    expect(unRef(1)).toBe(1);
  });

实现:

js 复制代码
export function isRef(ref) {
  return !!ref.__v_isRef;
}

export function unRef(ref) {
  return isRef(ref) ? ref.value : ref;
}

14、实现proxyRefs 功能

要点记录

  1. 如果原来的值是一个ref 那么重新赋值的时候,就要改原来值的 .value
js 复制代码
  it("proxyRefs", () => {
    const user = {
      age: ref(10),
      name: "张三",
    };

    const proxyUser = proxyRefs(user);

    expect(user.age.value).toBe(10);
    // 如果是 ref 则会自动的返回 ref 的 value
    expect(proxyUser.age).toBe(10);
    expect(proxyUser.name).toBe("张三");

    // 设置值,也分两种情况

    // 设置的值不是 ref

    proxyUser.age = 20;

    expect(proxyUser.age).toBe(20);
    expect(user.age.value).toBe(20);
    // 设置的是 ref

    proxyUser.age = ref(30);
    expect(proxyUser.age).toBe(30);
    expect(user.age.value).toBe(30);
  });

实现

js 复制代码
export function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    get(target, key) {
      return unRef(Reflect.get(target, key));
    },
    set(target, key, value) {
      // 如果原来的值是一个ref 那么重新赋值的时候,就要改原来值的 .value
      if (isRef(target[key]) && !isRef(value)) {
        return (target[key].value = value);
      } else {
        return Reflect.set(target, key, value);
      }
    },
  });
}

15、实现computed功能

特性:

  1. 调用ComputedRefImpl, 同时将getter函数传递给ReactiveEffect(getter), 对其进行依赖收集。
  2. computed 属性是有缓存的, 只有在访问.value属性的时候,才会调用effect.run()
  3. 因为缓存功能的存在,所以在内部需要收集getter函数的依赖信息,当发现有依赖信息变化之后调用schduler函数,重新将dirty变量重置为true
  4. 当用户调用.value = xxx的时候就会再次触发scheduler函数,重新设置dirty = true的值。
  5. 下一次再调用.value的时候,检测dirty变量发现是"脏的",则就再需要重新调用getter函数,获取最新的值。从而达到既可以缓存,又可以在依赖的值发生变化的时候,下一次取的时候拿到最新的值。
js 复制代码
describe("computed", () => {
  it("happy path", () => {
    const user = reactive({
      age: 1,
    });

    const age = computed(() => {
      return user.age;
    });

    expect(age.value).toBe(1);
  });

  it("should computed lazily", () => {
    // 在没访问.value 之前, getter函数是不会被调用的

    const user = reactive({
      age: 1,
    });

    const getter = jest.fn(() => {
      return user.age;
    });

    const value = computed(getter);

    // lazy 延迟执行
    expect(getter).not.toHaveBeenCalled();

    // 访问.value属性,触发 getter函数执行
    expect(value.value).toBe(1);
    expect(getter).toBeCalledTimes(1);

    // 重新赋值, getter 函数还是值调用一次
    user.age = 2;
    expect(getter).toBeCalledTimes(1);

    // 访问 .value 属性
    expect(value.value).toBe(2);
    expect(getter).toBeCalledTimes(2);

    // 测试缓存 效果

    value.value;
    expect(getter).toBeCalledTimes(2);
  });
});

代码实现

ts 复制代码
import { ReactiveEffect } from "./effect";
class ComputedRefImpl {
  private _getter: any;
  private _dirty: any = true; // 默认值是 true 表示不脏的
  private _value: any;
  private _effect: ReactiveEffect;

  constructor(getter) {
    this._getter = getter;
    // 第一次不会执行 scheduler 函数  ,当 响应式数据被 set 的时候, 不会触发 effect 函数, 而是执行 scheduler 函数
    this._effect = new ReactiveEffect(getter, () => {
      // set 的时候把 标记脏不脏的放开 ,
      if (!this._dirty) {
        this._dirty = true;
      }
    });
  }

  get value() {
    // 用一个变量来标记是否 读取过 computed 的值
    if (this._dirty) {
      this._dirty = false;
      this._value = this._effect.run();
    }
    return this._value;
  }
}

export function computed(getter) {
  return new ComputedRefImpl(getter);
}
相关推荐
蟾宫曲1 小时前
在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
前端·npm·vue3·vite·element-plus·计时器
秋雨凉人心1 小时前
简单发布一个npm包
前端·javascript·webpack·npm·node.js
liuxin334455661 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
qq13267029401 小时前
运行Zr.Admin项目(前端)
前端·vue2·zradmin前端·zradmin vue·运行zradmin·vue2版本zradmin
LCG元3 小时前
Vue.js组件开发-使用vue-pdf显示PDF
vue.js
魏时烟3 小时前
css文字折行以及双端对齐实现方式
前端·css
哥谭居民00014 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
烟波人长安吖~4 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
2401_882726484 小时前
低代码配置式组态软件-BY组态
前端·物联网·低代码·前端框架·编辑器·web
web130933203984 小时前
ctfshow-web入门-文件包含(web82-web86)条件竞争实现session会话文件包含
前端·github