小狐狸学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);
}
相关推荐
辻戋15 分钟前
从零实现React Scheduler调度器
前端·react.js·前端框架
徐同保17 分钟前
使用yarn@4.6.0装包,项目是react+vite搭建的,项目无法启动,报错:
前端·react.js·前端框架
Qrun1 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp1 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.2 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl4 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫6 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友6 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理8 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻8 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js