什么是副作用函数
指会产生副作用的函数。一个函数(这里我们叫effect)的执行会直接或简介的影响其他函数的执行,这时effect函数就产生了副作用。其实副作用是非常容易产生的,比如一个函数修改了全局变量,这也是一个副作用
const data = { text: 'hello world' }
function effect() {
document.body.innerText = data.text
}
data.text = 'lee'
effect函数的执行会设置body的值,但除了它,任何函数都可以读取或设置body的文本内容,也就是说effect执行会直接影响或间接影响其他函数的执行,那么effect就产生了副作用,他就是副作用函数
简易实现
- 当副作用函数effect执行时,触发字段obj.text的读取操作
- 当修改obj.text的值时,会触发字段obj.text的设置操作 如果我们能拦截一个对象的读取和设置操作,就可以实现,代码如下
// 存储副作用的容器
const bucket = new Set()
// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj= new Proxy(data, {
// 拦截读取数据操作
get(target, key) {
// 将副作用函数存储到容器中
bucket.add(effect)
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数取出来并执行
bucket.forEach(fn => fn())
return true
}
})
function effect() {
document.body.innerText = obj.text
}
effect()
document.body.innerText = obj.text
setTimeout(() => {
obj.text = 'hello lee'
}, 1000);
我们定义了一个Set类型的容器(桶),在读取obj.text的值时,我们就把effect函数放到桶里,即bucket.add(effect),然后返回属性值,在设置obj.text的effect函数取出并执行。而我们的拦截操作是通过代理对象Proxy来实现的。这样看来是不是很简单,只通过Proxy进行拦截就可以实现了。
从上述代码中,我们可以看出响应系统的工作流程如下:
- 当读取操作时,将副作用函数收集到容器中
- 当设置操作时,从容器中去除副作用函数并执行
完善的响应系统
存在的问题
- 当副作用函数名称改变,代码即失效
- 为obj添加新的属性时,理论上不执行,但函数确重新执行了 根本原因:使用了Set数据结构作为存储副作用函数的容器,没有在副作用函数与备操作的目标字段之间建立明确的联系。
解决方式
- 提供注册副作用函数的机制 定义全局变量activeEffect,他的作用是存储被注册的副作用函数,接着重新定义了effect函数,它变成了一个用来注册副作用函数的函数,effect函数接受一个参数fn,即要注册的函数
// 全局变量存储被注册的副作用函数
let activeEffect
// effct函数用于注册副作用函数
function effect(fn) {
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = fn
// 执行副作用函数
fn() // 执行副作用函数
}
使用一个匿名的副作用函数作为effect函数的参数,当effect函数执行时,首先会将匿名函数的副作用函数fn赋值给全局变量,接着执行被注册的匿名函数fn,这样厨房obj.text的读取操作,进而厨房Proxy的get拦截
const obj= new Proxy(data, {
// 拦截读取数据操作
get(target, key) {
if(activeEffect) {
// 将副作用函数存储到容器中
bucket.add(effect)
}
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal
// 把副作用函数取出来并执行
bucket.forEach(fn => fn())
return true
}
})
- 建立副作用函数与被操作的字段之间联系 首先,需要使用WeakMap代替Set,然后修改get/set拦截器
const obj = new Proxy(data, {
get(target, p, receiver) {
if (!activeEffect) return target[key]// 没有正在执行的副作用函数 直接返回
let depsMap = bucket.get(target)
if (!depsMap) { // 不存在,则创建一个Map
bucket.set(target, depsMap = new Map())
}
let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数
if (!deps) { // 不存在,则创建一个Set
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect) // 将副作用函数加进去
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
const depsMap = bucket.get(target) // target Map
if (!depsMap) return;
const effects = depsMap.get(key) // effectFn Set
effects && effects.forEach(fn => fn())
}
})
完整代码
const data = { text: 'hello world' }
// 全局变量存储被注册的副作用函数
let activeEffect
// effct函数用于注册副作用函数
function effect(fn) {
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = fn
// 执行副作用函数
fn() // 执行副作用函数
}
// 存储副作用函数的桶
const bucket = new WeakMap();
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
track(target, key);
return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key)
}
})
// 在 get中拦截函数内调用track函数追踪变化
function track(target, key) {
if (!activeEffect) return
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
}
// set 中拦截函数内调用trigger 函数触发变化
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return
const effects = depsMap.get(key);
effects && effects.forEach(fn => fn())
}
effect(() => {
console.log('effect run')
document.body.innerHTML = obj.text
})
setTimeout(() => {
console.log(222)
obj.text = 'hello lee'
}, 1000)