经过前面的努力, 我们初步了解最基本的响应式原理, 并且实现了一个Demo接下来, 我们要一步步完善响应式系统~
Party1
什么是响应式
实现一对多
所谓一对多: 一个属性对应多个副作用函数🤔思考如果一个属性存在多个与之对应的副作用函数理论上当属性改变时,属性关联的每一个副作用函数都应该重新执行
1) 手动方式示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">xiaopang</div>
<script>
function isObject(value) {
return typeof value === 'object' && value !== null
}
/**
* 创建响应式数据
* @param [object]: 普通对象
* @return [Proxy]: 代理对象
*/
function reactive(data) {
if (!isObject(data)) return
return new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value
effect() // 调用副作用函数, 调用effect函数会重新获取新的数据
effect1()
return true
},
})
}
// 定义一个响应式数据 : 触发者
const state = reactive({ name: 'xiaopang' })
// 定义一个副作用函数 : 响应者
function effect() {
console.log('effect被执行了...')
app.innerHTML = state.name
}
function effect1() {
console.log('effect1被执行了...')
state.name
}
// 当state.name改变时, 重新执行对应副作用函数effect
setTimeout(() => {
state.name = 'xxp'
}, 1000)
</script>
</body>
</html>
很显然, 我们可以通过手动的方式, 依次执行每一个关联的副作用函数.
2) 自动方式
我们可以创建一个存储所有副作用函数的空间, 这个空间叫做
副作用桶
首先想到的是数组, 数组的每个元素都是一个副作用函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">xiaopang</div>
<script>
// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数
const bucket = [] // 新增
function isObject(value) {
return typeof value === 'object' && value !== null
}
/**
* 创建响应式数据
* @param [object]: 普通对象
* @return [Proxy]: 代理对象
*/
function reactive(data) {
if (!isObject(data)) return
return new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value
// 从副作用函数桶中依次取出每一个元素(副作用函数)执行
bucket.forEach((fn) => fn()) // 修改
return true
},
})
}
// 定义一个响应式数据 : 触发者
const state = reactive({ name: 'xiaopang' })
// 定义一个副作用函数 : 响应者
function effect() {
console.log('effect被执行了...')
app.innerHTML = state.name
}
bucket.push(effect)
bucket.push(effect)
function effect1() {
console.log('effect1被执行了...')
state.name
}
bucket.push(effect1)
// 当state.name改变时, 重新执行对应副作用函数effect
setTimeout(() => {
state.name = 'xxp'
}, 1000)
</script>
</body>
</html>
但是数组元素是也可重复的, 为了效率, 我们还要去重, 而Set集合是默认去重的因此, 考虑用Set实现, Set集合中存放的每个元素也是副作用函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="app">xiaopang</div>
<script>
// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数
const bucket = new Set() // 修改
function isObject(value) {
return typeof value === 'object' && value !== null
}
/**
* 创建响应式数据
* @param [object]: 普通对象
* @return [Proxy]: 代理对象
*/
function reactive(data) {
if (!isObject(data)) return
return new Proxy(data, {
get(target, key) {
return target[key]
},
set(target, key, value) {
target[key] = value
// 从副作用函数桶中依次取出每一个元素(副作用函数)执行
bucket.forEach((fn) => fn())
return true
},
})
}
// 定义一个响应式数据 : 触发者
const state = reactive({ name: 'xiaopang' })
// 定义一个副作用函数 : 响应者
function effect() {
console.log('effect被执行了...')
app.innerHTML = state.name
}
bucket.add(effect)
function effect1() {
console.log('effect1被执行了...')
state.name
}
bucket.add(effect1)
const effect2 = () => {
console.log(state.name)
}
bucket.add(effect2)
// 当state.name改变时, 重新执行对应副作用函数effect
setTimeout(() => {
state.name = 'xxp'
}, 1000)
</script>
</body>
</html>
Party2
实现依赖收集
1) 为什么要依赖收集前面, 我们并没有区分不同属性对应的副作用函数, 而是全部放入到副作用桶里示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 定义一个副作用桶bucket
const bucket = new Set() // 修改
/**
* 定义响应式
* @param [object] : 普通对象
* @return [Proxy] : 代理对象
*/
function reactive(data) {
// 如果传入的data不是一个普通对象, 不处理
if (typeof data !== 'object' || data == null) return
return new Proxy(data, {
get(target, key) {
// console.log(`自定义访问${key}`)
return target[key]
},
set(target, key, value) {
// console.log(`自定义设置${key}=${value}`)
target[key] = value // 先更新值
bucket.forEach((fn) => fn())
return true
},
})
}
const pState = reactive({ name: 'hello', age: 20 })
function effectName() {
console.log('effectName...', pState.name)
}
bucket.add(effectName)
function effectAge() {
console.log('effectAge...', pState.age)
}
bucket.add(effectAge)
setTimeout(() => {
pState.name = 'brojie'
}, 1000)
</script>
</body>
</html>
接下来我们思考这样的问题🤔思考如果一个副作用函数effectName只引用了name另一个副作用函数effectAge只引用了age理论上,更新name只需要重新执行effectName而不需要重新执行effectAge换句话说, 依赖收集就是建立属性与副作用函数的对应关系
2) 实现思路
-
将当前
副作用函数
保存到一个全局变量 -
当执行
副作用函数
时, 会触发代理对象的自定义get
操作 -
在
get
操作时, 将全局变量中保存的函数添加到副作用桶
3) 具体实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 定义一个副作用桶bucket
const bucket = new Set() // 修改
// 定义一个全局变量, 作于保存 `当前副作用函数`
let activeEffect = null
/**
* 定义响应式
* @param [object] : 普通对象
* @return [Proxy] : 代理对象
*/
function reactive(data) {
// 如果传入的data不是一个普通对象, 不处理
if (typeof data !== 'object' || data == null) return
return new Proxy(data, {
get(target, key) {
// console.log(`自定义访问${key}`)
if (activeEffect != null) {
bucket.add(activeEffect)
}
return target[key]
},
set(target, key, value) {
// console.log(`自定义设置${key}=${value}`)
target[key] = value // 先更新值
bucket.forEach((fn) => fn())
return true
},
})
}
const pState = reactive({ name: 'hello', age: 20 })
// 定义副作用函数
function effectName() {
console.log('effectName...', pState.name)
}
// 将副作用函数保存到全局变量中
activeEffect = effectName
// 执行副作用函数
effectName()
// 重置全局变量
activeEffect = null
setTimeout(() => {
pState.name = 'brojie'
}, 1000)
</script>
</body>
</html>
4) 优化接下来, 我们优化一下, 封装一个注册函数, 方便注册副作用函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 定义一个副作用桶bucket
const bucket = new Set() // 修改
// 定义一个全局变量, 作于保存 `当前副作用函数`
let activeEffect = null
/**
* 定义响应式
* @param [object] : 普通对象
* @return [Proxy] : 代理对象
*/
function reactive(data) {
// 如果传入的data不是一个普通对象, 不处理
if (typeof data !== 'object' || data == null) return
return new Proxy(data, {
get(target, key) {
// console.log(`自定义访问${key}`)
if (activeEffect != null) {
bucket.add(activeEffect)
}
return target[key]
},
set(target, key, value) {
// console.log(`自定义设置${key}=${value}`)
target[key] = value // 先更新值
bucket.forEach((fn) => fn())
return true
},
})
}
/**
* 注册副作用函数
* @params [function]: 要注册的 副作用函数
*/
function registEffect(fn) {
if (typeof fn !== 'function') return
// 将当前注册的副作用函数 保存 到全局变量中
activeEffect = fn
// 执行当前副作用函数, 收集依赖
fn()
// 重置全局变量
activeEffect = null
}
const pState = reactive({ name: 'hello', age: 20 })
registEffect(function effectName() {
console.log('effectName...', pState.name)
})
setTimeout(() => {
pState.name = 'brojie'
}, 1000)
</script>
</body>
</html>
提示源码里注册副作用函数的函数名就叫effect这里我们重在理解函数的功能, 不用去纠结名字
5) 改进桶结构
看起来现在可以自动收集依赖. 但是依然解决不了不同的属性对应不同的副作用函数集合这个问题因此, 我们需要改进桶结构将桶改造成一个Map映射表, 不同的属性对应不同的Set集合
示例
// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数
const bucket = new Map() // 修改 [name: Set(fn, fn), age: Set(fn, fn)]
// 定义一个全局变量, 保存当前正在执行的副作用函数
let activeEffect = null
function isObject(value) {
return typeof value === 'object' && value !== null
}
// 收集依赖
function track(target, key) {
if (!activeEffect) return
let depSet = bucket.get(key)
if (!depSet) {
depSet = new Set()
bucket.set(key, depSet)
}
// 只有activeEffect有值时(保存的副作用函数), 才添加到桶中
depSet.add(activeEffect)
}
function trigger(target, key) {
// 从副作用函数桶中依次取出每一个元素(副作用函数)执行
let depSet = bucket.get(key)
if (depSet) {
depSet.forEach((fn) => fn())
}
}
/**
* 创建响应式数据
* @param [object]: 普通对象
* @return [Proxy]: 代理对象
*/
function reactive(data) {
if (!isObject(data)) return
return new Proxy(data, {
get(target, key) {
// 在get操作时, 收集依赖
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
// 在set操作时, 触发副作用重新执行
trigger(target, key)
return true
},
})
}
/**
* 注册副作用函数
* @param [function]: 需要注册的 副作用函数
*/
function effect(fn) {
if (typeof fn !== 'function') return
// 记录正在执行的副作用函数
activeEffect = fn
// 调用副作用函数
fn()
// 重置全局变量
activeEffect = null
}
至此, 我们通过改进桶结构,可以区分同一个代理对象的不同的属性
6) 进一步改进桶结构
思考如果不同的源对象存在同名属性, 就会出现问题比如 pState代理的源对象上存在name属性 pState1代理的源对象上也存在name属性这样, 在bucket桶里, 就不能区分不同的代理对象问题示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 定义一个副作用桶bucket
const bucket = new Map() // 修改
// 定义一个全局变量, 作于保存 `当前副作用函数`
let activeEffect = null
// 收集依赖
function track(target, key) {
// 根据不同的key, 获取对应的集合
let depSet = bucket.get(key)
if (!depSet) {
// 如果不存在, 创建一个新的集合, 并添加到桶中
depSet = new Set()
bucket.set(key, depSet) // 建立 key -> Set的对应关系
}
depSet.add(activeEffect) // 将当前副作用函数添加到对应的集合
}
function trigger(target, key) {
let depSet = bucket.get(key)
if (depSet) {
// 如果对应的集合存在, 遍历集合中的每个函数
depSet.forEach((fn) => fn())
}
}
/**
* 定义响应式
* @param [object] : 普通对象
* @return [Proxy] : 代理对象
*/
function reactive(data) {
// 如果传入的data不是一个普通对象, 不处理
if (typeof data !== 'object' || data == null) return
return new Proxy(data, {
get(target, key) {
// console.log(`自定义访问${key}`)
if (activeEffect != null) {
// 收集依赖
track(target, key)
}
return target[key]
},
set(target, key, value) {
// console.log(`自定义设置${key}=${value}`)
target[key] = value // 先更新值
// 触发更新
trigger(target, key)
return true
},
})
}
/**
* 注册副作用函数
* @params [function]: 要注册的 副作用函数
*/
function registEffect(fn) {
if (typeof fn !== 'function') return
// 将当前注册的副作用函数 保存 到全局变量中
activeEffect = fn
// 执行当前副作用函数, 收集依赖
fn()
// 重置全局变量
activeEffect = null
}
const pState = reactive({ name: 'hello', age: 20 })
const pState1 = reactive({ name: 'p1' })
registEffect(function effectFn() {
console.log('effectFn...', pState.name)
})
registEffect(function effectFn1() {
console.log('effectFn1...', pState1.name)
})
console.log(bucket)
setTimeout(() => {
pState.name = 'brojie'
}, 1000)
</script>
</body>
</html>
解决方案
示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// 定义一个副作用桶bucket
const bucket = new WeakMap() // 修改
// 定义一个全局变量, 作于保存 `当前副作用函数`
let activeEffect = null
// 收集依赖
function track(target, key) {
// 根据不同的target, 获取对应的Map
let depMap = bucket.get(target)
if (!depMap) {
depMap = new Map()
bucket.set(target, depMap) // 建立target -> Map的对应关系
}
// 根据不同的key, 获取对应的集合
let depSet = depMap.get(key)
if (!depSet) {
// 如果不存在, 创建一个新的集合
depSet = new Set()
depMap.set(key, depSet) // 建立 key -> Set的对应关系
}
depSet.add(activeEffect) // 将当前副作用函数添加到对应的集合
}
function trigger(target, key) {
let depMap = bucket.get(target)
if (!depMap) return
let depSet = depMap.get(key)
if (depSet) {
// 如果对应的集合存在, 遍历集合中的每个函数
depSet.forEach((fn) => fn())
}
}
/**
* 定义响应式
* @param [object] : 普通对象
* @return [Proxy] : 代理对象
*/
function reactive(data) {
// 如果传入的data不是一个普通对象, 不处理
if (typeof data !== 'object' || data == null) return
return new Proxy(data, {
get(target, key) {
// console.log(`自定义访问${key}`)
if (activeEffect != null) {
// 收集依赖
track(target, key)
}
return target[key]
},
set(target, key, value) {
// console.log(`自定义设置${key}=${value}`)
target[key] = value // 先更新值
// 触发更新
trigger(target, key)
return true
},
})
}
/**
* 注册副作用函数
* @params [function]: 要注册的 副作用函数
*/
function registEffect(fn) {
if (typeof fn !== 'function') return
// 将当前注册的副作用函数 保存 到全局变量中
activeEffect = fn
// 执行当前副作用函数, 收集依赖
fn()
// 重置全局变量
activeEffect = null
}
const pState = reactive({ name: 'hello', age: 20 })
registEffect(function effectName() {
console.log('effectName...', pState.name)
})
registEffect(function effectAge() {
console.log('effectAge...', pState.age)
})
console.log(bucket)
setTimeout(() => {
pState.name = 'brojie'
}, 1000)
</script>
</body>
</html>
至此,我们可以区分不同的代理对象下不同属性对应的副作用函数集合真正实现了完善的桶结构~✌如果学到这里, 恭喜你, 已经实现了一个可用的响应式系统! ^_^😃
💡小结1在get时收集依赖:收集不同代理对象不同属性所依赖的副作用函数2在set时触发依赖:取出当前属性所依赖的所有副作用函数, 重新执行