前置准备
初始化项目:
bash
pnpm init
tsc --init
我们使用vite
来构建项目:
bash
pnpm i vite
修改 package.json
:
json
"scripts": {
"dev":"vite"
},
vite的入口文件是根目录下的index.html
,构建所需文件如下图:
然后在index.html
中引入main.ts
即可.
响应式原理
响应式原理是 Vue 的核心,在 Vue2 中,响应式原理是通过Object.defineProperty
去实现的,但这个方法存在一些局限性:
- 不能劫持对象新增的属性(提供了
$set
解决) - 监听不到数组的变化(重写了数组的7个方法)
- 存在性能问题,需要递归所有属性,监听大量对象时开销较大
为了解决上述缺陷,Vue3使用了Proxy
去替代Object.defineProperty
实现响应式更新。
reactive
reactive
是响应式核心API,具体实现思路有三步:
- 通过
proxy
劫持 get 和 set - 在 get 中收集依赖,添加副作用函数
- 在 set 中触发依赖更新,执行相关副作用函数
effect(副作用函数)
effect
接收一个函数,它的作用是收集当前的副作用函数,并根据传入的配置项决定是否在初始化时调用。
ts
//配置项
interface Options {
scheduler?: Function; //控制副作用函数的执行时机
lazy?: boolean; //是否在初始化时执行
}
ts
let activeEffect: Function;
export const effect = (fn: Function, options?: Options) => {
const _effect = () => {
activeEffect = _effect;
return fn();
};
_effect.options = options;
if (options && options.lazy) {
return _effect;
} else {
_effect();
return _effect;
}
};
track(依赖收集)
有了副作用函数,我们就要构建监听的对象的和副作用函数之间的依赖映射关系,在vue3中使用了WeakMap
去实现,相比与Map它有以下好处:
- 键只能是对象,不会造成内存泄漏
- 在频繁添加、删除键值对的场景下,
WeakMap
的性能要优于普通的Map
。 WeakMap
的键是弱引用,不会干扰垃圾回收机制,如果该对象需要回收,WeakMap
中的键会自动消失。WeakMap
的数据只能由键对象访问,外部无法直接遍历,保证了数据的私有性。
假设我们传入的对象为:{name:"theshy",age:"24"}
,监听的属性为name
,那么我们要构建的数据结构如下图:
ts
const targetMap = new WeakMap();
export const track = (target: object, key: string | symbol) => {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
//设置key为传入的对象,value为Map
targetMap.set(target, depsMap);
}
let deps: Set<Function> = depsMap.get(key);
if (!deps) {
deps = new Set();
//设置key为监听的属性,value为依赖集
depsMap.set(key, deps);
}
//收集副作用函数`
deps.add(activeEffect);
};
trigger(依赖更新)
最后是依赖更新,我们只需根据targetMap去除副作用函数的集合,然后依次执行副作用函数即可(如果有调度函数就传给对应的调度函数,然后在调度函数内决定执行时机)
ts
export const trigger = (target: object, key: string | symbol, value: any) => {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps: Set<any> = depsMap.get(key);
if (!deps) return;
deps.forEach((effect) => {
if (effect?.options?.scheduler) {
effect.options.scheduler(effect);
} else {
effect?.();
}
});
};
reactive
实现reactive
所需函数我们都已实现,接下来使用proxy
进行劫持即可,需要注意的是,对于多层嵌套的对象,我们需要进行递归处理:
ts
import { track, trigger } from "./effect";
const reactive = <T extends object>(target: T): T => {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
if (typeof res === "object" && res !== null) {
return reactive(res);
}
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
trigger(target, key, value);
return res;
},
});
};
export default reactive;
接下来让我们测试一下:
ts
//main.ts
import { effect } from "./reactive/effect";
import reactive from "./reactive/reactive";
const person = reactive({ name: "TheyShy", age: 18 });
effect(() => {
document.querySelector("#app")!.innerHTML = person.name + person.age;
});
document.querySelector("#btn")!.addEventListener("click", () => {
person.age++;
});
html
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app"></div>
<hr />
<button id="btn">年龄+1</button>
<script type="module" src="./main.ts"></script>
</body>
</html>
效果如下,已经实现了响应式更新:
computed
computed
实现的核心思路就是设置一个缓存值,依赖值不发生变化时走缓存,否则就重新获取值,我们可以设置一个变量dirty
去控制是否走缓存,通过scheduler
配置项在每次依赖项更新时重置dirty
ts
import { effect } from "./effect";
export const computed = <T>(fn: () => T) => {
let dirty = true;
let _cacheValue: T;
const _value = effect(fn, {
lazy: false,
scheduler: () => {
dirty = true;
},
});
class ComputedRefImpl {
get value() {
//缓存值不变就走缓存 否则重新获取值
if (dirty) {
_cacheValue = _value();
dirty = false;
}
return _cacheValue;
}
}
return new ComputedRefImpl();
};
测试一下:
ts
//main.ts
import { computed } from "./reactive/computed";
import { effect } from "./reactive/effect";
import reactive from "./reactive/reactive";
const person = reactive({
name: "TheyShy",
age: 18,
});
const nameAndAge = computed(() => {
return person.name + person.age;
});
effect(() => {
person.age; //需要触发一下依赖收集track
document.querySelector("#app")!.innerHTML = nameAndAge.value;
});
document.querySelector("#btn")!.addEventListener("click", () => {
person.age++;
});
效果与之前一致:
watch
首先我们需要定义配置项:
ts
interface Options {
immediate?: boolean; //创建时立即触发回调
flush?: "sync" | "post"; //控制组件更新前/后触发回调
}
然后我们需要一个工具函数,帮助处理直接传入对象的形式:
ts
const traverse = (target: any, seen = new Set()) => {
if (typeof target !== "object" || target === null || seen.has(target)) {
return;
}
seen.add(target);
//如果是嵌套对象则递归调用处理
for (const key in target) {
traverse(target[key], seen);
}
return target;
};
实现watch
函数主要就是newValue
和oldValue
的处理,我们先调用一次effect
保存旧值,再通过job
函数完成新旧值的替换,需要注意的是,如果immediate
为true时,直接调用了job
函数,故旧值为undefined
针对flush:post
的情况,因为dom更新是异步的,我们只需将job
添加到微任务队列即可
ts
import { effect } from "./effect";
const watch = (source: any, callback: Function, options?: Options) => {
let getter: Function;
if (typeof source === "function") {
getter = source;
} else {
getter = () => traverse(source);
}
let newValue, oldValue;
//dom更新是异步的,故将回调放入微任务队列中
const flushPostCallbacks = () => {
Promise.resolve().then(job);
};
const job = () => {
newValue = effectFn();
callback(newValue, oldValue);
oldValue = newValue;
};
const effectFn = effect(getter, {
lazy: true,
scheduler: options && options.flush === "post" ? flushPostCallbacks : job,
});
if (options && options.immediate) {
options?.flush === "post" ? flushPostCallbacks() : job();
} else {
oldValue = effectFn(); //默认值
}
};
export default watch;
测试一下:
ts
console.log("a");
watch(
() => person.age,
(newValue: any, oldValue: any) => {
console.log(newValue, oldValue);
},
{
immediate: true,
flush: "post",
}
);
console.log("b");
结果符合预期,immediate
和flush
均正常
其它
至于其它响应式相关函数如:
- ref
- toRef
- toRefs
- toRaw
- shallowReactive
等API的实现比较简单,这里就不再详细列出了,具体代码可以已上传github,需要的小伙伴可以自行参考~