副作用函数指的是会产生副作用的函数。例如:effect函数会直接或间接影响其他函数的执行,这时我们便说effect函数产生了副作用。
function effect(){
document.body.innerText = 'hello vue3'
}
再例如:
//全局变量
let val = 2
function effect() {
val = 2 //修改全局变量,产生副作用
}
上代码:
const obj = { text: 'hello world' }
function effect() {
// effect 函数的执行会读取 obj.text
document.body.innerText = obj.text
}
当前修改obj.text的值的时候,除了本身的发生变化之外,不会有任何其他反应。
若修改obj.text的值的时候,effect函数副作用函数自动重新执行,如果能实现这个目标,那么obj对象就是响应式数据。很显然,目前还不能实现,接下来我们将数据变成响应式数据。
1.当effect副作用函数执行时,触发obj.text的读取操作。
2.当修改obj.text的值的时候,出发obj.text的设置操作。
问题的关键:我们如何才能拦截一个对象属性的读取和设置操作。在ES2015之前只能通过Object.defineProperty函数实现,这也是Vue.js 2所采用的方式。在ES2015+中,我们可以使用Proxy代理对象来实现,这也是Vue.js 3所采用的方式。
采用Proxy来实现:
/**
* 实现一个响应式
* @param { Object } bucket
* @param { Object } data
* @param { Function } effect
* @param { Object } obj
*/
// 存储副作用函数的桶
const bucket = new Set()
// 副作用函数
function effect() {
console.log(obj.text)
}
//原始数据
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(fun => fun())
//返回 true 代表设置操作成功
return true
}
})
effect()
setTimeout(() => {
console.log('一秒后触发设置')
obj.text = 'hello vue3'
},1000)
目前还存在许多缺陷,我们需要去掉通过名字来获取副作用函数的硬编码机制 。
/**
* 注册副作用函数机制
* @param {any} activeEffect
* @param {Function} effect
* @param {Object} obj1
* @param {Object} data 用的是上面的
*/
//用全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = fn
// 执行副作用函数
fn()
}
const obj1 = new Proxy(data, {
//拦截读取操作
get(target, key) {
//--将副作用函数加入到桶里
//--bucket.add(effect)
//++将activeEffect中存储的副作用函数收集到“桶”中
if (activeEffect) {
bucket.add(activeEffect)
}
//返回属性值
return target[key]
},
//拦截设置操作
set(target, key, newVal) {
//设置属性值
target[key] = newVal
//把副作用函数从桶里取出来并执行
bucket.forEach(fun => fun())
//返回 true 代表设置操作成功
return true
}
})
effect(
() => {
console.log('run', obj1.text)
}
)
setTimeout(() => {
console.log('一秒后触发设置')
obj1.notExist = 'hello vue3'
},1000)
没有在副作用函数与被操作的目标字段之间建立明确的联系。无论读取的是哪一个属性,都会把副作用函数收集到“桶”里。
首先分析一下注册副作用函数触发都存在哪些角色:
①obj1对象
②text字段
③使用副作用函数注册的函数
我们来为这三个角色建立一个树形关系。target表示obj1——代理对象所代理的原始对象;用key来表示text字段——被操作的字段名;effectFn表示被注册的副作用函数。这个联系建立起来后,就可以解决前文提到的问题了。
因为WeakMap对key是弱引用,只有被引用的有价值的信息可以访问,没有被引用的信息就会被垃圾回收器回收。如果是Map,即使信息没有引用,垃圾回收器也不会去回收它,那么就会有很大机率导致内存溢出。
代码如下:
const bucketMap = new WeakMap()
const obj2 = new Proxy(data, {
get(target, key) {
// 没有activeEffect直接返回
if(!activeEffect) return target[key]
// 取出WeakMap桶里的值 target ===> key
let depsMap = bucketMap.get(target)
// 如果不存在depsMap,那就新建Map与target建立联系
if(!depsMap) {
bucketMap.set(target, (depsMap = new Map()))
}
// key ===> effectFn
let deps = depsMap.get(key)
if(!deps) {
depsMap.set(key, deps = new Set())
}
// 注册副作用函数
deps.add(activeEffect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
// 取target
const depsMap = bucketMap.get(target)
if(!depsMap) return
// 根据key取副作用函数
const effects = depsMap.get(key)
// 执行副作用函数
effect && effect.forEach(fn => fn())
return true
}
})
我们可以将activeEffect注册副作用函数机制单独封装到一个函数track中,表达追踪的含义。将触发副作用函数单独封装到trigger函数中。代码更改如下:
/**
* 建立联系
* @param { Object } bucketMap
* @param { Object } obj2
* @param { Function } track 追踪
* @param { Function } trigger 触发
*/
// WeakMap桶
const bucketMap = new WeakMap()
function track(target, key) {
// 没有activeEffect直接返回
if (!activeEffect) return target[key]
// 取出WeakMap桶里的值 target ===> key
let depsMap = bucketMap.get(target)
// 如果不存在depsMap,那就新建Map与target建立联系
if (!depsMap) {
bucketMap.set(target, (depsMap = new Map()))
}
// key ===> effectFn
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, deps = new Set())
}
// 注册副作用函数
deps.add(activeEffect)
}
function trigger(target, key) {
// 取target
const depsMap = bucketMap.get(target)
if (!depsMap) return
// 根据key取副作用函数
const effects = depsMap.get(key)
// 执行副作用函数
effect && effect.forEach(fn => fn())
}
const obj2 = new Proxy(data, {
get(target, key) {
// 注册副作用函数
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
// 触发副作用函数
trigger(target, key)
return true
}
})
我们用以下代码说明分支切换。如下:
const data = { ok: true, text: 'hello world'}
const obj = new Proxy(/*....*/)
effect(function effectFn{
document.body.innerText = obj.ok ? obj.text : 'not'
})
在effectFn函数内部的三元表达式,根据ok字段值的不同会执行不同的代码分支。ok的值发生变化时,代码执行的分支会根治变化,这就是所谓的分支切换。
分支切换可能会产生一流的副作用函数。根据上面的代码案例来说,effectFn与响应式数据建立的关系如下:
副作用函数与响应式数据之间的联系当修改ok字段值改为false的时候,text的不会被读取,所以指挥触发ok字段的读取,而不会触发text读取,所以理想状态下effectFn不应该被字段text所对应的依赖集合收集。
显然我们目前还不能做到这一点。
理想状态遗留的副作用函数会导致不必要的更新。解决问题的思路就是:每次副作用函数执行时,我们可以先把它从所有与之关联的依赖几何中删除。当副作用函数执行完毕后,会重新建立联系,新的联系里不会包含遗留的副作用函数。
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn
fn()
}
// deps用来存储所有与这副作用函数相关联的依赖集合
effectFn.deps = []
effectFn()
}
function track(target, key) {
if (!activeEffect) return target[key]
let depsMap = bucketMap.get(target)
if (!depsMap) {
bucketMap.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, deps = new Set())
}
deps.add(activeEffect)
// ======= 主要就是增加关联数组中 ===========
activeEffect.deps.push(deps)
}
对依赖集合收集
有了这个联系后,我们就可以在每次副作用函数执行时,根据deps获取所有相关联的依赖集合,进而将副作用函数从依赖集合中移除。
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
fn()
}
// deps用来存储所有与这副作用函数相关联的依赖集合
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
//遍历effectFn的deps数组
for(let i = 0; i < effectFn.deps.length; i++) {
let deps = effectFn.deps[i]
deps.delete(effectFn)
}
// 最后需要重置effectFn.deps数组
effectFn.deps.length = 0
}
现在我们来执行一下这完整代码,看会有啥效果:
const data1 = {
ok: true,
text: 'hello world'
}
const obj2 = new Proxy(data1, {
get(target, key) {
// 注册副作用函数
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
// 触发副作用函数
trigger(target, key)
return true
}
})
function trigger(target, key) {
// 取target
const depsMap = bucketMap.get(target)
if (!depsMap) return
// 根据key取副作用函数
const effects = depsMap.get(key)
// 执行副作用函数
effects && effects.forEach(fn => fn())
}
/**
* 重新设计effectFn
* @param { Function } effect
* @param { Function } cleanup
* @param { Function } track
*/
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
fn()
}
// deps用来存储所有与这副作用函数相关联的依赖集合
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
//遍历effectFn的deps数组
for(let i = 0; i < effectFn.deps.length; i++) {
let deps = effectFn.deps[i]
deps.delete(effectFn)
}
// 最后需要重置effectFn.deps数组
effectFn.deps.length = 0
}
function track(target, key) {
// 没有activeEffect直接返回
if (!activeEffect) return target[key]
// 取出WeakMap桶里的值 target ===> key
let depsMap = bucketMap.get(target)
// 如果不存在depsMap,那就新建Map与target建立联系
if (!depsMap) {
bucketMap.set(target, (depsMap = new Map()))
}
// key ===> effectFn
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, deps = new Set())
}
// 注册副作用函数
deps.add(activeEffect)
// ======= 主要就是增加关联数组中 ===========
activeEffect.deps.push(deps)
}
// 测试
effect(
() => {
let n = obj2.ok ? obj2.text : 'not'
console.log('run', n)
}
)
setTimeout(() => {
console.log('一秒后触发设置')
obj2.ok = false
},1000)
可以看到目前会无限不断去执行, 问题出现在哪里呀?问题便出现在trigger函数下面这句中。
effects && effects.forEach(fn => fn())
Why?有啥问题?来看下面代码:
const set = new Set([1])
set.forEach(item => {
set.delete(1)
set.add(1)
console.log('遍历中!!!')
})
由于不断执行,我截不下全图。不断执行的原因:语言规范中说过,在调用forEach遍历Set集合时,一个值被访问过了,但被删除后又被重新添加到集合,如果此时forEach遍历没有结束,那么该值会重新被访问。所以,上面代码会不断去执行 。
同理,trigger函数里面的effects也是一样,当副作用函数执行的时候,cleanup会进行清除,但是副作用函数的执行会导致其被重新收集到集合中,而此时遍历仍然在进行,所以我们实现的响应式才会不断的去执行。
我们可以构造另一个Set集合并遍历它。我们去修改一下trigger触发函数:
function trigger(target, key) {
// 取target
const depsMap = bucketMap.get(target)
if (!depsMap) return
// 根据key取副作用函数
const effects = depsMap.get(key)
// 执行副作用函数
const effectToRun = new Set(effects) //新增
effectToRun && effectToRun.forEach(fn => fn()) //新增
// effects && effects.forEach(fn => fn()) //剔除
}
如上图所示,无限循环问题得以解决。
Vue响应式系统(二)https://blog.csdn.net/qq_55806761/article/details/135612738