前言
在ES6中,"reactive"
通常指的是创建具有响应式特性的对象,使得这些对象的属性可以自动地响应数据变化而更新视图。Vue.js的响应式系统就是基于这个概念构建的。在ES6中,我们可以通过使用Proxy
对象来手动创建具有类似响应式特性的对象。
下面我们将逐步创建一个简单的reactive
函数,来手动实现一个类似Vue.js中响应式对象的功能(只实现了set及get功能):
1、 reactive.js:
js
import { mutableHandlers } from "./baseHandlers.js";
export const reactiveMap = new WeakMap(); // 创建一个WeakMap用来存储已经代理过的对象,WeakMap对内存的回收更加友好
export function reactive(target) {
// 将target变成响应式对象
return createReactiveObject(target, reactiveMap, mutableHandlers); // 创建响应式对象
}
export function createReactiveObject(target, proxyMap, proxyHandlers) {
// 创建响应式的函数,参数分别是目标对象,存储代理对象的map,代理对象的处理函数
// 判断target是不是引用类型
if (typeof target !== "object" || target === null) {
// 不是引用类型直接返回
return target;
}
// 该对象是否已经被代理过(已经是响应式对象)
const existingProxy = proxyMap.get(target); // 从reactiveMap中获取target
if (existingProxy) {
return existingProxy; // 如果已经代理过了,直接返回
}
// 执行代理操作(将target变成响应式对象)
const proxy = new Proxy(target, proxyHandlers); //第二个参数:当target被读取值,设置值,判断值等等操作时,会触发的函数
// 往 reactiveMap中增加 proxy,把已经代理过的对象存储起来
proxyMap.set(target, proxy);
return proxy; // 返回代理对象
}
在上述代码中,主要是实现reactive
函数的主要功能之一,将引用类型转换为响应式对象,下面让我逐步解释这段代码的功能:
- 创建
reactiveMap
:这行代码创建了一个WeakMap
对象,用于存储已经代理过的对象。WeakMap
中的键是弱引用的,这意味着如果键对象没有其他引用,因此对内存的回收更加友好,当被代理的对象被垃圾回收时,相应的条目也会被自动移除。 reactive
函数 :这个函数接受一个普通对象作为参数,并将其转换为具有响应式特性的对象。它调用了createReactiveObject
函数来创建响应式对象,并传入了目标对象、reactiveMap
和mutableHandlers
作为参数,参数分别是目标对象,存储代理对象的map,代理对象的处理函数。createReactiveObject
函数 :这个函数用于创建响应式对象。- 首先它判断传入的目标对象是否是引用类型(即对象或数组),如果不是,则直接返回目标对象本身。
- 然后它检查目标对象是否已经被代理过,如果是,则直接返回之前创建的代理对象。如果目标对象尚未被代理过,则使用
Proxy
对象创建一个新的代理对象,并将其存储到reactiveMap
中。 - 最后返回创建的代理对象。
2、baseHandlers.js
js
import { track, trigger } from "./effect.js";
const get = createGetter(); // 创建一个get函数
const set = createSetter(); // 创建一个set函数
function createGetter() {
return function get(target, key, receiver) {
// console.log('target被读取值');
const res = Reflect.get(target, key, receiver); // 获取源对象中的键值
// 这个属性究竟还有哪些地方用到了,(副作用函数的收集,computed,watch...)
track(target, key); // 依赖收集
return res;
}
}
function createSetter() {
return function set(target, key, value, receiver) {
// console.log('target被设置值', key, value);
const res = Reflect.set(target, key, value, receiver); // 设置源对象中的键值 === target[key] = value
trigger(target, key); // 触发副作用函数
// 需要记录下来此时是哪一个key的值变更了,再去通知其他依赖该值的函数生效,更新浏览器的视图(响应式)
// 触发被修改的属性身上的副函数 依赖收集(被修改的key在哪些地方被使用了)发布订阅
return res;
}
}
export const mutableHandlers = {
// get: function (target, key, receiver) { // target 被代理的源对象,key是源对象中的键,receiver是代理后对象
// console.log('target被读取值');
// const res = Reflect.get(target, key, receiver); // 获取源对象中的键值
// return res;
// },
get,
set,
// set: function (target, key, value, receiver) {
// console.log('target被设置值', key, value);
// const res = Reflect.set(target, key, value, receiver); // 设置源对象中的键值 === target[key] = value
// return res;
// // 更新浏览器的视图(响应式)
// },
}
以上代码实现了对代理对象的操作处理,包括了对属性的读取和设置。被代理对象中的任意属性的值发生修改,都应该将用到了这个属性的各个函数重新执行一遍,那么在执行之前就需要先为每一个属性都做好副作用函数的收集,也称为依赖收集.这也是实现 Vue.js 响应式系统的关键之一,它确保了在属性变化时能够正确地触发浏览器视图更新。下面让我来逐步解释这段代码的功能:
-
createGetter
函数:- 这个函数返回一个用于处理属性读取的函数。在这个函数内部,首先调用了
Reflect.get()
方法获取目标对象中指定键的值,并将结果存储在变量res
中。 - 接着调用了外部导入的
track
函数,用于收集当前属性的依赖关系。这意味着该属性被访问时,需要跟踪它的依赖,以便在属性变化时触发相应的副作用函数。 - 最后返回属性的值
res
。
- 这个函数返回一个用于处理属性读取的函数。在这个函数内部,首先调用了
-
createSetter
函数:- 这个函数返回一个用于处理属性设置的函数。在这个函数内部,首先调用了
Reflect.set()
方法将指定键的值设置为指定的值,并将结果存储在变量res
中。 - 接着调用了外部导入的
trigger
函数,用于触发与当前属性相关联的副作用函数。这意味着当属性被修改时,需要通知所有依赖该属性的函数执行相应的副作用操作。 - 最后返回设置操作的结果
res
。
- 这个函数返回一个用于处理属性设置的函数。在这个函数内部,首先调用了
-
mutableHandlers
对象:- 这个对象包含了对可变对象的操作处理函数,其中包括了
get
和set
操作的具体实现。在这里,get
和set
的处理函数分别由createGetter
和createSetter
函数返回。 mutableHandlers
对象被导出,可以被其他模块引用和使用。
- 这个对象包含了对可变对象的操作处理函数,其中包括了
3、effect.js
js
const targetMap = new WeakMap();
let activeEffect = null; //得是一个副作用函数
export function effect(fn,options={}) { //watch,computed的核心逻辑
const effectFn = () => {
try {
activeEffect = effectFn
return fn()
} finally {
activeEffect = null
}
}
if(!options.lazy){
effectFn()
}
return effectFn
}
// 为某个属性添加effect
export function track(target,key) {
// targetMap = { //存成这样的结构
// target: {
// key: [effect1,effect2,...]
// }
// }
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) { //改属性未添加过effect
dep = new Set()
}
if (!dep.has(activeEffect) && activeEffect) {
// 存入一个effect函数
dep.add(activeEffect)
}
depsMap.set(key, dep)
}
// 触发属性effect
export function trigger(target,key) {
const depsMap = targetMap.get(target)
if(!depsMap){ //当前对象中所有的key都没有副作用函数,从来都没有被使用过
return
}
const deps = depsMap.get(key)
if (!deps) { //这个属性没有依赖
return
}
deps.forEach(effectFn => {
effectFn() //将该属性上的所有副作用函数全部触发
});
}
以上代码实现了一个状态管理的功能,用于实现对对象属性的依赖跟踪和副作用函数的执行,并管理对象属性的依赖关系和执行副作用函数。这样,在属性值发生变化时,就能自动执行相应的副作用函数,从而实现数据的响应式处理。下面我将详细的介绍一下以上代码的功能:
-
effect
函数:- 这个函数用于创建副作用函数。副作用函数是一个函数,它会在响应式数据发生变化时被执行。
effect
函数接受两个参数:fn
是一个函数,代表需要执行的副作用函数;options
是一个包含选项的对象,默认为空对象。其中,lazy
是一个布尔值选项,表示是否延迟执行副作用函数,默认为false
。- 在
effect
函数内部,首先创建了一个名为effectFn
的函数,它是一个封装了传入的副作用函数fn
的函数。在effectFn
函数内部,通过try...finally
结构确保了activeEffect
变量的正确设置和清除。然后根据options.lazy
的值决定是否立即执行effectFn
函数,并返回effectFn
函数。
-
track
函数:- 这个函数用于为某个属性添加副作用函数。当某个属性被访问时,需要调用
track
函数来收集对应的依赖关系,以便在属性值发生变化时执行相应的副作用函数。 track
函数接受两个参数:target
是目标对象,key
是目标对象的属性名。- 在
track
函数内部,首先根据target
从targetMap
中获取对应的依赖映射关系depsMap
,如果不存在,则创建一个新的空映射关系并存储到targetMap
中。然后根据key
从depsMap
中获取对应的依赖集合dep
,如果不存在,则创建一个新的空集合。接着判断当前副作用函数activeEffect
是否已经存在于dep
中,如果不存在且activeEffect
存在,则将其添加到dep
中。最后将更新后的dep
存储回depsMap
中。
- 这个函数用于为某个属性添加副作用函数。当某个属性被访问时,需要调用
-
trigger
函数:- 这个函数用于触发属性的副作用函数。当某个属性的值发生变化时,需要调用
trigger
函数来执行所有依赖于该属性的副作用函数。 trigger
函数接受两个参数:target
是目标对象,key
是目标对象的属性名。- 在
trigger
函数内部,首先根据target
从targetMap
中获取对应的依赖映射关系depsMap
,如果不存在,则说明目标对象中所有的属性都没有副作用函数,直接返回。然后根据key
从depsMap
中获取对应的依赖集合deps
,如果不存在,则说明该属性没有任何依赖,直接返回。接着遍历deps
集合,依次执行其中的副作用函数。
- 这个函数用于触发属性的副作用函数。当某个属性的值发生变化时,需要调用
值得注意的是,对于effect
函数,实际上大部分实现监听效果的函数都会使用到这种类似的函数,就如watch
和 computed
函数。因为它用于创建副作用函数,而 watch
和 computed
又都是基于副作用函数实现的,它们都依赖于副作用函数来实现对数据的观察和计算。
结语
就这样,我们分别通过创建reactive.js
、baseHandlers.js
和effect.js
三个js文件实现了一个简单的reactive
函数。
总结
在ES6中,我们可以通过使用Proxy
对象和一些基本的JavaScript技巧手动创建具有响应式特性的数据处理系统。本文介绍了如何使用Proxy
对象来实现一个简单的reactive
函数,以及如何配合使用WeakMap
、Reflect
和副作用函数来构建一个简易的响应式系统。通过对依赖关系的追踪和副作用函数的执行,我们可以实现数据的自动更新和视图的同步更新,这也是现代JavaScript框架中常见的核心功能之一。
补充:什么是副作用函数
副作用函数指的是在函数执行过程中,除了返回一个值之外,还对函数外部的状态产生了影响,或者执行了与函数本身的目的无关的操作。
在响应式系统中,副作用函数通常用于创建观察者模式或实现数据的自动更新。例如,当一个数据发生变化时,会触发与之相关联的副作用函数,从而执行一些预定的操作,比如更新UI界面或触发其他相关的事件。Vue.js中的watch
和computed
就是基于副作用函数实现的,它们能够监视数据变化并执行相应的操作。
最后,假如您也和我一样,在准备春招。欢迎加微信shunwuyu,这里有几十位一心去大厂的友友可以相互鼓励,分享信息,模拟面试,共读源码,齐刷算法,手撕面经。来吧,友友们!