1 模块化
目前, 主流的浏览器如Chrome, FireFox都已经可以支持模块化导入导出了.
特别说明
这里演示一下模块化的基本使用
由于工程化需要配置的环境对于大部分小伙伴而言, 增加了太多暂时不必要的学习成本和负担.
因此, 我在重置版录视频时, 砍掉了这部分. 希望优先保证大家对主线的理解和更自然更好理解的调试
创建一个文件reactive.mjs作为响应式模块
导出
●reactive函数: 实现普通对象的代理
●effect函数. 实现注册副作用函数
示例
// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数
// 修改 [state -> Map[name: Set(fn, fn), age: Set(fn, fn)], state1 -> Map]
const bucket = new WeakMap()// 定义一个全局变量, 保存当前正在执行的副作用函数
let activeEffect = nullfunction isObject(value) {return typeof value === 'object' && value !== null
}// 收集依赖
function track(target, key) {// 只有activeEffect有值时(保存的副作用函数), 才添加到桶中if (!activeEffect) returnlet depMap = bucket.get(target)if (!depMap) {depMap = new Map()bucket.set(target, depMap)}let depSet = depMap.get(key)if (!depSet) {depSet = new Set()depMap.set(key, depSet)}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]: 代理对象*/
export function reactive(data) {if (!isObject(data)) returnreturn 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]: 需要注册的 副作用函数*/
export function effect(fn) {if (typeof fn !== 'function') return// 记录正在执行的副作用函数activeEffect = fn// 调用副作用函数fn()// 重置全局变量activeEffect = null
}
使用ES Module的方式
●使用export导出
●使用import导入
在测试文件中, 通过import导入, 但是需要在script元素上加上type属性, 值为module
<!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 type="module">// 使用import导入import { reactive, effect } from './reactive.mjs'</script></body>
</html>
2 单例模式
🤔思考
1对于同一个源对象每次调用reactive返回的代理对象应该是一样的
2对于一个已经代理过的对象再次代理应该返回的也应该是一样的
1) 实现单例
为了实现单例, 我们需要建立源对象->代理对象的映射关系
●如果存在映射, 说明已经代理过了, 直接返回
●如果不存在映射, 说明没有代理过, 创建一个新的代理对象返回
定义源对象->代理对象的映射表(使用WeakMap)
// 定义一个副作用函数桶, 存放所有的副作用函数. 每个元素都是一个副作用函数
// 修改 [state -> Map[name: Set(fn, fn), age: Set(fn, fn)], state1 -> Map]
const bucket = new WeakMap()// 建立一个映射表 target -> proxy
const reactiveMap = new WeakMap() // 新增// 定义一个全局变量, 保存当前正在执行的副作用函数
let activeEffect = nullfunction isObject(value) {return typeof value === 'object' && value !== null
}// 收集依赖
function track(target, key) {// 只有activeEffect有值时(保存的副作用函数), 才添加到桶中if (!activeEffect) returnlet depMap = bucket.get(target)if (!depMap) {depMap = new Map()bucket.set(target, depMap)}let depSet = depMap.get(key)if (!depSet) {depSet = new Set()depMap.set(key, depSet)}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) {if (!isObject(data)) return// 如果映射表中存在了对应关系if (reactiveMap.has(data)) {// 返回data对应的代理对象return reactiveMap.get(data)}const proxy = 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},})// 建立data(源对象)和proxy(代理对象)的映射关系reactiveMap.set(data, proxy)return proxy
}/*** 注册副作用函数* @param [function]: 需要注册的 副作用函数*/
function effect(fn) {if (typeof fn !== 'function') return// 记录正在执行的副作用函数activeEffect = fn// 调用副作用函数fn()// 重置全局变量activeEffect = null
}
测试用例
<!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><script src="./reactive.js"></script></head><body><script>const source = { name: 'hello' }const p = reactive(source)const p1 = reactive(source)console.log(p === p1) // true</script></body>
</html>
2) 实现重复代理
可以定义一个特殊的标识__v_isReactive
●如果存在该标识, 说明已经代理过, 直接返回
●如果不存在该标识, 说明没有被代理, 创建新的代理对象
示例
// 定义源对象->代理对象映射表
const reactiveMap = new WeakMap()// 定义一个副作用桶bucket
const bucket = new WeakMap()
// 定义一个全局变量, 作于保存 `当前副作用函数`
let activeEffect = null// 收集依赖
function track(target, key) {// 根据不同的target, 获取对应的Maplet depMap = bucket.get(target)if (!depMap) {depMap = new Map()bucket.set(target, depMap)}let depSet = depMap.get(key)if (!depSet) {depSet = new Set()depMap.set(key, depSet)}depSet.add(activeEffect)
}// 触发执行
function trigger(target, key) {let depMap = bucket.get(target)if (!depMap) returnlet depSet = depMap.get(key)if (depSet) {// 如果对应的集合存在, 遍历集合中的每个函数depSet.forEach((fn) => fn())}
}/*** 定义响应式* @param [object] : 普通对象* @return [Proxy] : 代理对象*/
export function reactive(data) {// 如果传入的data不是一个普通对象, 不处理if (typeof data !== 'object' || data == null) returnif (reactiveMap.has(data)) {// 返回data对应的代理对象return reactiveMap.get(data)}// 如果存在标识, 说明data被代理过了if (data['__v_isReactive']) {return data}const proxy = new Proxy(data, {get(target, key) {if (key == '__v_isReactive') return true // 新增// 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},})reactiveMap.set(data, proxy)return proxy
}/*** 注册副作用函数* @params [function]: 要注册的 副作用函数*/
export function effect(fn) {if (typeof fn !== 'function') return// 将当前注册的副作用函数 保存 到全局变量中activeEffect = fn// 执行当前副作用函数, 收集依赖fn()// 重置全局变量activeEffect = null
}
测试用例
<!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><script src="./reactive.js"></script></head><body><script>const source = { name: 'hello' }const p = reactive(source)const p1 = reactive(p)console.log(p === p1) // true</script></body>
</html>
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><script src="./reactive.js"></script></head><body><div id="app"></div><script>const state = reactive({ flag: true, name: 'xiaoming', age: 20 })effect(() => {app.innerHTML = state.flag ? state.name : state.age})setTimeout(() => {// 我们期望:// - 当flag为true时, 收集name// - 当flag为false时, 收集agestate.flag = false}, 1000)</script></body>
</html>
为了实现上述功能. 当分支切换时, 我们需要做两件事
1清理之前的依赖
2重新收集新的依赖
1) 包装副作用函数
由于我们需要在副作用函数上扩展一些属性, 重新收集新的依赖
可以考虑封装一个类ReactiveEffect来包装副作用函数, 方便后期扩展属性
class RectiveEffect {constructor(fn) {this.fn = fn}run() {activeEffect = thisthis.fn()activeEffect = null}
}
在全局变量activeEffect上保存的是reactiveEffect的实例
2) 建立对应关系
如果我们想清理依赖关系, 就需要知道当前副作用函数被哪些属性依赖.
可以考虑扩展一个deps属性来记录
class RectiveEffect {constructor(fn) {this.fn = fnthis.deps = [] // 新增}run() {activeEffect = thisthis.fn()activeEffect = null}
}
在track收集属性依赖的副作用函数的同时, 记录当前副作用函数依赖的属性
// 收集依赖
function track(target, key) {// 根据不同的target, 获取对应的Maplet 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) // 将当前副作用函数添加到对应的集合activeEffect.deps.push(depSet)
}
🤔思考
Q: 为什么将集合放入数组, 而不是将key值放入数组里?
A: 如果将key放入数组里, 后面还是需要通过key找到集合, 再清除集合中所有的元素
3) 清理旧依赖
新增一个清理函数cleanup
function cleanup(target) {let deps = target.depsif (deps) {deps.forEach((depSet) => {// 清空SetdepSet.delete(target)})deps.length = 0}
}
在重新收集依赖前, 先清理旧的依赖关系
class RectiveEffect {constructor(fn) {this.fn = fnthis.deps = []}run() {activeEffect = thiscleanup(this) // 新增this.fn()activeEffect = null}
}
此时, 会出现Set假死的问题
上述问题可以简化为
const set = new Set([1])
set.forEach(item => {set.delete(1)set.add(1)console.log('死循环...')
})
在trigger时, 构造一个新的Set来遍历
// 触发执行
function trigger(target, key) {let depMap = bucket.get(target)if (!depMap) returnlet depSet = depMap.get(key)if (depSet) {let effects = [...depSet]effects.forEach((effect) => effect.run())}
}
4 避免死循环
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><script src="./reactive.js"></script></head><body><div id="app"></div><script>const state = reactive({ name: 'xiaoming', age: 20 })effect(() => {// 在副作用函数中, 设置依赖的值, 会出现死循环state.age = state.age + 1})</script></body>
</html>
1先获取state.age的值, 收集依赖, 建立了age-> 副作用函数的关系
2再更新state.age的值, 触发更新, 调用trigger
3在trigger中调用age对应的副作用函数, 再次执行
2) 如何避免
我们重写trigger函数, 多加一个判断: 只有当遍历执行的函数跟当前副作用函数不同时, 才会调用run去执行
function trigger(target, key) {let depMap = bucket.get(target)if (!depMap) returnlet depSet = depMap.get(key)if (depSet) {let effects = [...depSet]// 如果对应的集合存在, 遍历集合中的每个函数effects.forEach((effect) => {if (effect !== activeEffect) {effect.run()}})}
}