引言
响应式这一个概念应该不难理解,就是js在对某一个对象或者某一个值进行操作时,我们希望通过实现一个响应式系统达到触发某些事件,也就是对操作的相应。
响应式数据与副作用函数
副作用函数指的是会产生副作用的函数,如下面的代码所示:
function effect() {
document.body.innerText = 'hello vue3'
}
当 effect 函数执行时,它会设置 body 的文本内容,但除了effect 函数之外的任何函数都可以读取或设置 body 的文本内容。也就是说,effect 函数的执行会直接或间接影响其他函数的执行,这时我们说 effect 函数产生了副作用。
其实副作用函数并不少见,我们前面在讨论webpack的 tree shaking
话题里面涉及到的是否进行数据流处理对 tree shaking
的效果是不可忽略的。这边不再赘述。
副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用,如下面的代码所示:
// 全局变量
let val = 1
function effect() {
val = 2 // 修改全局变量,产生副作用
}
理解了什么是副作用函数,再来说说什么是响应式数据。假设在一个副作用函数中读取了某个对象的属性:
const obj = { text: 'hello world' }
function effect() {
// effect 函数的执行会读取 obj.text
document.body.innerText = obj.text
}
副作用函数 effect 会设置 body 元素的 innerText
属性,其值为 obj.text
,当 obj.text
的值发生变化时,我们希望副作用函数 effect 会重新执行。
那这样我们的思路就变成了:通过一些手段,在读取 obj.text
值的时候可以将effect函数储存进一个bucket里面,而在设置obj.text
值的时候可以在bucket里面把effect拿出来执行。
响应式数据的基本实现
如何才能让 obj 变成响应式数据呢?通过观察我们能发现:
- 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作;
- 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作。
如何拦截一个对象属性的读取和设置操作。
在 ES2015 之前,只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象 Proxy 来实现,这也是 Vue.js 3 所采用的方式。
function createReactiveObject(target, proxyMap, baseHandlers) {
// 核心就是 proxy
// 目的是可以侦听到用户 get 或者 set 的动作
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
// 把创建好的 proxy 给存起来,
proxyMap.set(target, proxy);
return proxy;
}
这样我们就基于 proxy 创建了一个用于存储副作用函数,并且我们使用了一个proxyMap,这是一个可以把各类型的proxy储存起来的容器。我们分别设置了 get 和 set 拦截函数,用于拦截读取和设置操作。
设计一个完善的响应系统
从上面的示例不难看出,一个响应系统的工作流程如下:
当读取操作发生时,将副作用函数收集到“桶”中;
当设置操作发生时,从“桶”中取出副作用函数并执行。
下面通过一个简单的响应式系统实现来讲解原理:
我们知道proxy对象是可以传入一个具有getter和setter的对象进行get或set操作时处理的函数,因此我们可以创建一个baseHandlers
,进行getter和setter的管理。
//baseHandlers
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
//Reflect.get方法允许你从一个对象中取属性值。就如同属性访问器语法,但却是通过函数调用来实现
const res = Reflect.get(target, key, receiver);
if (!isReadonly) {
// 在触发 get 的时候进行依赖收集
track(target, "get", key);
}
return res;
};
}
function createSetter() {
return function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
// 在触发 set 的时候进行触发依赖
trigger(target, "set", key);
return result;
};
}
我们还需要在副作用函数与被操作的目标字段之间建立明确的联系。
例如当读取属性时,无论读取的是哪一个属性,其实都一样,都会把副作用函数收集到“桶”里;当设置属性时,无论设置的是哪一个属性,也都会把“桶”里的副作用函数取出并执行。副作用函数与被操作的字段之间没有明确的联系。解决方法很简单,只需要在副作用函数与被操作的字段之间建立联系即可,这就需要我们重新设计“桶”的数据结构,而不能简单地使用一个Set 类型的数据作为“桶”了。
我们通过WeakMap
来实现刚才上面我们说的储存effect的bucket。不了解WeakMap类的特性的可以看下WeakMap 对象是一组键/值对的集合
WeakMap 的键是原始对象 target,WeakMap 的值是一个Map 实例,而 Map 的键是原始对象 target 的 key,Map 的值是一个由副作用函数组成的 Set。
//Map存放不同类型的代理
export const reactiveMap = new WeakMap();
export const readonlyMap = new WeakMap();
export const shallowReadonlyMap = new WeakMap();
定义好了上面的几种bucket,我们开始实现响应式系统最核心的部分,也就是proxy的实现:
function createReactiveObject(target, proxyMap, baseHandlers) {
// 核心就是 proxy
// 目的是可以侦听到用户 get 或者 set 的动作
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
// 把创建好的 proxy 给存起来,
proxyMap.set(target, proxy);
return proxy;
}
对于前面的trick和trigger:
export function track(target, type, key) {
if (!isTracking()) {
return;
}
console.log(`触发 track -> target: ${target} type:${type} key:${key}`);
// 1. 先基于 target 找到对应的 dep
// 如果是第一次的话,那么就需要初始化
let depsMap = targetMap.get(target);
if (!depsMap) {
// 初始化 depsMap 的逻辑
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = createDep();
depsMap.set(key, dep);
}
trackEffects(dep);
}
export function trigger(target, type, key) {
// 1. 先收集所有的 dep 放到 deps 里面,
// 后面会统一处理
let deps: Array<any> = [];
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 暂时只实现了 GET 类型
// get 类型只需要取出来就可以
const dep = depsMap.get(key);
// 最后收集到 deps 内
deps.push(dep);
const effects: Array<any> = [];
deps.forEach((dep) => {
// 这里解构 dep 得到的是 dep 内部存储的 effect
effects.push(...dep);
});
// 这里的目的是只有一个 dep ,这个dep 里面包含所有的 effect
// 这里的目前应该是为了 triggerEffects 这个函数的复用
triggerEffects(createDep(effects));
}
評論