构建reactive函数,获取proxy实例
整个reactive函数,本质是返回了一个proxy实例,我们先去实现这个reactive函数,得到proxy实例;
1.创建packages/reactivity/src/reactive.ts模块:
import { mutableHandlers } from "./baseHandlers";
export const reactiveMap = new WeakMap<object, any>();
export function reactive(target: object) {
return createReactiveObject(target, mutableHandlers, reactiveMap);
}
function createReactiveObject(
target: object,
baseHandlers: ProxyHandler<any>,
proxyMap: WeakMap<object, any>
) {
const existingProxy = proxyMap.get(target);
// 已经被代理了直接返回
if (existingProxy) {
return existingProxy;
}
// 没有被代理则生成proxy实例
const proxy = new Proxy(target, baseHandlers);
// 缓存proxy
proxyMap.set(target, proxy);
return proxy;
}
2.创建packages/reactivity/src/baseHandlers.ts模块;
export const mutableHandlers: ProxyHandler<object> = {};
3.创建packages/reactivity/src/index.ts;
export { reactive } from "./reactive";
5.创建packages/vue/src/index.ts;
export { reactive } from "@vue/reactivity";
在终端执行npm run build;生成dist目录下的文件,此时我们的reactive函数就被成功导出了;
接下来我们去创建对应的测试实例;创建packages/vue/examples/reactivity/reactive.html;
// 导入刚才打的包
<script src="../../dist/vue.js"></script>
<script>
const { reactive } = Vue;
const obj = reactive({
name: "张三",
});
</script>
运行之后打印:
实现createGetter和createSetter方法
在packages/reactivity/src/baseHandlers.ts中修改:
import { track, trigger } from "./effect";
const get = createGetter();
const set = createSetter();
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
};
function createGetter() {
return function get(target: object, key: string | symbol, receiver: object) {
const res = Reflect.get(target, key, receiver);
// 依赖收集
track(target, key);
return res;
};
}
function createSetter() {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
) {
const result = Reflect.set(target, key, value, receiver);
// 触发依赖
trigger(target, key, value);
return result;
};
}
新建packages/reactivity/src/effect.ts
/**
* 收集依赖
* @param target
* @param key
*/
export function track(target: object, key: unknown) {
console.log("收集依赖");
}
/**
* 触发依赖
* @param target
* @param key
* @param newValue
*/
export function trigger(target: object, key: unknown, newValue: unknown) {
console.log("触发依赖");
}
接下来测试一下,是否会触发track和trigger方法;记得要重新build;
<script>
const { reactive } = Vue;
const obj = reactive({
name: "张三",
});
console.log("obj--->", obj.name);
obj.name = "李四";
</script>
track和trigger方法被成功打印出来了;
小插曲:给项目加上热更新
给package.json新加一条指令;
"scripts": {
"dev": "rollup -c -w",
}
构建effect函数,生成ReactiveEffect实例
创建好reactive实例之后,接下来我们需要触发effect;
effect(() => {
document.querySelector("#app").innerText = obj.name;
});
在effect中,生成了ReactiveEffect实例,并且触发了getter;接下来实现effect方法;
1.在packages/reactivity/src/effect.ts中,创建effect方法;
export function effect<T = any>(fn: () => T) {
const _effect = new ReactiveEffect(fn);
_effect.run();
}
export let activeEffect: ReactiveEffect | undefined;
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {}
run() {
activeEffect = this;
return this.fn();
}
}
2.导出effect方法;
去修改我们的测试实例;
<script>
const { reactive, effect } = Vue;
const obj = reactive({
name: "张三",
});
effect(() => {
document.querySelector("#app").innerText = obj.name;
});
</script>
此时页面中展示出来了张三;
实现track和trigger方法
依赖收集和依赖触发原理
- track用来依赖收集;
- trigger用来依赖触发;
响应性是:当响应性数据触发setter时执行fn函数;如果想要实现这个目的,那么必须在getter时去收集当前的fn函数,以便在setter时可以执行对应的fn函数;
export const reactiveMap = new WeakMap<object, any>();
WeakMap的key必须是一个对象,并且key是一个弱引用的;举个例子
const obj = reactive({
name: "张三",
});
effect(() => {
document.querySelector("#app").innerText = obj.name;
});
1.WeakMap:
1.key:响应性对象 (此时为obj)
2.value:Map对象
1.key:响应性对象的指定属性 (name)
2.value:指定对象的指定属性的执行函数 (fn函数)
实现track依赖收集函数
依赖收集的本质就是实现activeEffect和targetMap的绑定:
type KeyToDepMap = Map<any, ReactiveEffect>;
const targetMap = new WeakMap<any, KeyToDepMap>();
/**
* 收集依赖
* @param target
* @param key
*/
export function track(target: object, key: unknown) {
// activeEffect是fn函数
if (!activeEffect) return;
console.log("收集依赖");
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
depsMap.set(key, activeEffect);
console.log("targetMap--->", activeEffect, targetMap);
}
此时targetMap的值为:
实现trigger触发依赖函数
所有的依赖关系都保存到了targetMap之中,所以依赖触发也就是从targetMap中读取对应的effect,然后执行对应的fn函数;
/**
* 触发依赖
* @param target
* @param key
* @param newValue
*/
export function trigger(target: object, key: unknown, newValue: unknown) {
console.log("触发依赖");
const depsMap = targetMap.get(target);
if (!depsMap) {
return;
}
const effects = depsMap.get(key) as ReactiveEffect;
if (!effects) {
return;
}
effects.fn();
}
修改一个测试实例;
<script>
const { reactive, effect } = Vue;
const obj = reactive({
name: "张三",
});
effect(() => {
document.querySelector("#app").innerText = obj.name;
});
setTimeout(() => {
obj.name = "李四";
}, 4000);
</script>
初级vue3的响应性如何应对多个effect?
问题描述
我们有这样一个测试实例,它有多个effect,那我们的初级vue3的响应性如何应对呢?
<body>
<div id="app">
<p id="p1"></p>
<p id="p2"></p>
</div>
</body>
<script>
const { reactive, effect } = Vue;
const obj = reactive({
name: "张三",
});
effect(() => {
document.querySelector("#p1").innerText = obj.name;
});
effect(() => {
document.querySelector("#p2").innerText = obj.name;
});
setTimeout(() => {
obj.name = "李四";
console.log(name);
}, 3000);
</script>
在我们的初级vue3响应性代码里,收集依赖时一个key只对应了一个effect,而在我们的测试实例中需要的是一个key对应两个effect;所以初级vue3响应性代码已经不能满足需求了,需要升级。
export function track(target: object, key: unknown) {
// 只能完成一个key对应一个effect
depsMap.set(key, activeEffect);
}
如何一个key对应多个effect呢?大家是不是都想到用数组;如下图所示,我们可以构建一个Set(值不会重复的"数组")类型的对象,作为Map的value;
解决方案
新建packages/reactivity/src/dep.ts;
import { ReactiveEffect } from "./effect";
export type Dep = Set<ReactiveEffect>;
export const createDep = (effects?: ReactiveEffect[]): Dep => {
return new Set<ReactiveEffect>(effects);
};
修改effect.ts;
import { Dep, createDep } from "./dep";
import { isArray } from "@vue/shared";
type KeyToDepMap = Map<any, Dep>;
export function track(target: object, key: unknown) {
let dep = depsMap.get(key);
if (!dep) {
// 如果dep不存在,建立起联系
depsMap.set(key, (dep = createDep(dep)));
}
trackEffects(dep);
}
/**
* 利用dep依次跟踪指定key的所有effect
*/
export function trackEffects(dep: Dep) {
// aciveEffect强行放入dep?
dep.add(activeEffect!);
}
export function trigger(target: object, key: unknown, newValue: unknown) {
const dep: Dep | undefined = depsMap.get(key);
if (!dep) {
return;
}
// 依次触发
triggerEffects(dep);
}
/**
*
* @param dep 依次触发dep中保存的依赖
*/
export function triggerEffects(dep: Dep) {
// 转化成数组
const effects = isArray(dep) ? dep : [...dep];
// 依次触发依赖
for (const effect of effects) {
triggerEffect(effect);
}
}
// 触发指定依赖
export function triggerEffect(effect: ReactiveEffect) {
effect.run();
}
再试一下上面的测试实例,发现多个effect也能成功运行啦!
初级vue3响应性还有哪些局限呢?
初级vue3响应性能接受基本数据类型吗?
看一下我们的测试实例;
<script>
const { reactive, effect } = Vue;
const obj = reactive("张三");
console.log("obj--->", obj);
</script>
发现报错了!我们的响应性是由Proxy实现的,Proxy第一个参数target必须是一个对象,如果我们的target不是一个对象类型,此时就会报错;
如果我们对reactive的对象进行解构,它还会有相应性吗?
<script>
const { reactive, effect } = Vue;
const obj = reactive({
name: "张三",
});
let { name } = obj;
effect(() => {
document.querySelector("#app").innerText = name;
});
setTimeout(() => {
name = "李四";
}, 4000);
</script>
可以看到李四被打印出来了,视图还是没有变;解构之后不是Proxy,因此失去了响应性;
ps: 本文代码仓库地址:gitee.com/minjie05/vu…