vue.js 设计与实现
根据HCY vue.js 设计与实现做的笔记。
命令式和声明式
编程范式可以分为声明式(Declarative)和命令式(Imperative)两种风格。
// 声明式 更关注过程
$('#app') // 获取 div
.text('hello world') // 设置文本内容
.on('click', () => { alert('ok') }) // 绑定点击事件
// 命令式 关注结果
<div @click="() => alert('ok')">hello world</div>
vue路由中有编程式路由导航与声明式路由导航
响应式数据与副作用函数
副作用函数 指的是会产生副作用的函数
function effect() {
document.body.innerText = 'hello vue3'
}
// effect 函数改变了 innerText 内容,当有其他元素调用该 innerText 是则会受到影响,因此 effect() 是一个副作用函数
// 全局变量
let val = 1
function effect() {
val = 2 // 修改全局变量,产生副作用
}
响应式数据 当副作用函数影响到了某个数据,当该数据改变时,我们希望副作用函数影响的数据一起改变。
const obj = { text: 'hello world' }
function effect() {
// effect 函数的执行会读取 obj.text
document.body.innerText = obj.text
}
obj.text = 'hello vue3' // 修改 obj.text 的值,同时希望副作用函数会重新执行
响应式数据基本实现
实现思路:将副作用函数存储于“桶”中,当发生数据发生改变时,在将副作用函数从“桶”中取出,再次调用副作用函数。
在 ES2015 之前,只能通过 Object.defineProperty 函 数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使 用代理对象 Proxy 来实现,这也是 Vue.js 3 所采用的方式。
let bucket = new Set()
// 原始数据
const data = {
text: 'hello word'
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target,key) {
// 将副作用函数添加到存储桶
bucket.add(effect)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 提取出副作用函数
bucket.forEach(fn => fn())
// 返回 true 代表操作成功
return true
}
})
// 副作用函数
function effect() {
document.body.innerText = obj.text
}
// 调用函数
effect()
// 修改响应式数据
setTimeout(()=> {
obj.text = 'hello vue3'
},3000)
上述方法虽然能实现数据的响应式,但是我们是直接调用 effect 方法。
解决副作用函数硬编码
一个响应系统工作流程如下:
- 当读取操作发生时,将副作用函数收集到桶中;
- 当设置操作发生时,从桶中取出副作用函数并执行;
上述响应式系统我们将 effect 函数写死,传入固定的副作用函数,我们需要重新改变副作用函数写法,一旦 函数不叫 effect 那么这个响应式系统就失效。
let bucket = new Set()
// 原始数据
const data = {
text: 'hello word'
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// effect 用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn
// 执行副作用函数
fn()
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target,key) {
// 将副作用函数添加到存储桶
bucket.add(activeEffect)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 提取出副作用函数
bucket.forEach(fn => fn())
// 返回 true 代表操作成功
return true
}
})
// 匿名函数注册副作用函数
effect(() => {
document.body.innerText = obj.text
})
// 修改响应式数据
setTimeout(()=> {
obj.text = 'hello vue3'
},3000)
这样就完美解决了副作用函数硬编码问题。
但是出现另一个问题。
const data = {
text: 'hello word'
}
// 匿名函数注册副作用函数
effect(() => {
console.log('effect running')
document.body.innerText = obj.text
})
// 修改响应式数据
setTimeout(()=> {
obj.noExist = 'hello vue3'
},3000)
我们本希望的响应式数据时,当我们代理了整个 data,一旦对象中已有的属性值发生改变,我们希望副作用函数能重新执行,并改变所调用的数据;但是当我们改变对象中不存在的一个属性值,例如 noExist
,我们所希望的副作用函数不会执行,因为副作用函数并没有调用到该数据。从程序结果看,修改一个没有调用的属性值,副作用函数也被执行了。原因是Proxy监测到了对象属性的变化,因此我们需要改写存储桶。
重新设计桶结构
导致上述问题的原因是 没有在副作用函数与被 操作的目标字段之间建立明确的联系。当读取属性时,无论读到哪个属性都会调用副作用函数。解决方法很简单,只需要在副作用函数与被操作的字段之间建立联系即 可,这就需要我们重新设计“桶”的数据结构,而不能简单地使用一个 Set 类型的数据作为“桶”了。
effect(function effectFn() {
document.body.innerText = obj.text
})
这段代码中存在三个明确的角色。
- 被操作(读取)的代理对象 obj。
- 被操作(读取)的字段名 text。
- effect 函数注册的副作用函数 effectFn
如果用 target 代表一个代理对象所代理的原始对象,key 代表被操作的字段名,effectFn 表示的是被注册的副作用函数。
三者之间的关系是:
target
└── key
└── effectFn
当如果有两个副作用函数同时读取同一个对象的属性值:
effect(function effectFn1() {
obj.text
})
effect(function effectFn2() {
obj.text
})
三者之间的关系是:
target
└── text
└── effectFn1
└── effectFn2
当一个函数调用同个对象的两个不同属性值
effect(function effectFn() {
obj.text1
obj.text2
})
三者之间的关系是:
target
└── text1
└── effectFn
└── text2
└── effectFn
当不同的副作用函数调用不同的对象的属性值
effect(function effectFn1() {
obj1.text1
})
effect(function effectFn2() {
obj2.text2
})
三者之间的关系是:
obj1
└── text1
└── effectFn1
obj2
└── text2
└── effectFn2
重新设置桶结构
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
text: 'hello word'
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// effect 用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn
// 执行副作用函数
fn()
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target,key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
//
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
effects && effects.forEach(fn => fn())
// 返回 true 代表操作成功
return true
}
})
// 匿名函数注册副作用函数
effect(() => {
document.body.innerText = obj.text
})
// 修改响应式数据
setTimeout(()=> {
obj.text = 'hello vue3'
},3000)
从这段代码中看出构建数据结构的方式,利用到了 WeakMap Map Set
WeakMap 由 target --> Map 构成
Map 由 key --> Set 构成
WeakMap 的键是原始对象 target(也就是 obj),WeakMap 的值是一个 Map 实例;而 Map 的键是原始对象 target 的 key(也就是 text), Map 的值是一个由副作用函数组成的 Set 。
WeakMap 和 Map
const map = new Map()
const weakMap = new WeakMap();
{
(function () {
const foo = {foo: 1}
const bar = {bar: 2}
map.set(foo, 1)
weakMap.set(bar, 2)
})()
}
当该函数表达式执行完毕后,对于对象 foo 来说,它仍然作为 map 的 key 被引用着,因此垃圾回收器 (grabage collector)不会把它从内存中移除,我们仍然可以通过 map.keys 打印出对象 foo。然而对于对象 bar 来说,由于 WeakMap 的 key 是弱引用,它不影响垃圾回收器的工作,所以一旦表达式执行 完毕,垃圾回收器就会把对象 bar 从内存中移除,并且我们无法获取 weakmap 的 key 值,也就无法通过 weakmap 取得对象 bar。
WeakMap 对 key 是弱引用,不影响垃圾回收器的工 作。据这个特性可知,一旦 key 被垃圾回收器回收,那么对应的键和值就访问不到了。所以 WeakMap 经常用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息。
优化代码
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
text: 'hello word'
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// effect 用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn
// 执行副作用函数
fn()
}
// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
}
// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
//
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
effects && effects.forEach(fn => fn())
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表操作成功
return true
}
})
// 匿名函数注册副作用函数
effect(() => {
document.body.innerText = obj.text
})
// 修改响应式数据
setTimeout(()=> {
obj.text = 'hello vue3'
},3000)
如以上代码所示,分别把逻辑封装到 track 和 trigger 函数 内,这能为我们带来极大的灵活性。
*分支切换与 cleanup
// 原始数据
const data = {
text: 'hello word',
ok: true
}
// 匿名函数注册副作用函数
effect(() => {
console.log('effect running')
document.body.innerText = obj.ok ? obj.text : 'not'
})
当副作用函数中出现一个三元表达式,即 obj.text 的改变需要经过 obj.ok 的逻辑判断;理想状态,当我们执行完副作用函数后,手动切换 obj.ok = false,不应该执行副作用函数。但实际情况是无论 obj.ok 的值如何改变都会触发副作用函数,即使 obj.text 的值不会在改变。
data
└── ok
└── effectFn
└── text
└── effectFn
当修改 obj.ok = false 的理想状态
也就是说,当我们把 字段 obj.ok 的值修改为 false,并触发副作用函数重新执行之后,遗留的副作用函数会导致不必要的更新。解决思路是每次副作用函数执行前,将其从相关联的依赖集合中移除。要将一个副作用函数从所有与之关联的依赖集合中移除,就需要 明确知道哪些依赖集合中包含它。
如何清除掉副作用函数的无效关联关系?
- 每次副作用函数执行前,可以先把它从所有与之关联的依赖集合中删除,然后清空依赖集合的收集,
- 当副作用函数执行,所有会重新建立关联。(副作用函数中,会重新执行响应式数据的get操作,从而进行收集依赖)
栈溢出
- 当设置响应式对象的值时,触发
trigger
函数,遍历依赖集合, - 遍历的过程中,每个回合,被包裹的副作用函数执行,
cleanup
,把副作用函数从依赖集合中删除- 触发副作用函数
- 副作用函数执行触发响应式数据的
get
操作,重新收集依赖函数
- 继续遍历
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
text: 'hello word',
ok: false
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// effect 用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当 effectFn 执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn
// 执行副作用函数
fn()
}
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
const effectsToRun = new Set(effects)
effectsToRun.forEach(effectFn => effectFn())
// effects && effects.forEach(effectFn => effectFn()) // 删除
}
// 清空副作用函数的依赖集
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集中删除
deps.delete(effectFn)
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表操作成功
return true
}
})
// 匿名函数注册副作用函数
effect(() => {
console.log('running')
document.body.innerText = obj.ok ? obj.text : 'not'
})
// 修改响应式数据
setTimeout(()=> {
obj.text = 'hello vue3'
},3000)
嵌套的 effect 与 effect 栈
effect (注册副作用函数)是可以嵌套的。如:
effect(function effectFn1() {
console.log('effectFn1')
effect(function effectFn2() {
console.log('effectFn2')
})
})
// 当执行 effectFn1 时,effectFn2 也会被影响并且被执行
// 实际场景
// Foo 组件
const Foo = {
render() {
return /*.....*/
}
}
// 在一个 effect 中执行 Foo 的渲染函数
effect(() => {
Foo.render()
})
// 当组件发生嵌套时
const Bar = {
render() {
return /*.....*/
}
}
const Foo = {
render() {
return <Bar />
}
}
// 此时 effect 函数也会发生嵌套
effect(() => {
Foo.render()
effect(() => {
Bar.render()
})
})
effectFn1 内部嵌套了 effectFn2,很明 显,effectFn1 的执行会导致 effectFn2 的执行。需要注意的是, 我们在 effectFn2 中读取了字段 obj.bar,在 effectFn1 中读取 了字段 obj.foo,并且 effectFn2 的执行先于对字段 obj.foo 的 读取操作。在理想情况下,我们希望副作用函数与对象属性之间的联 系如下:
data
└── foo
└── effectFn1
└── bar
└── effectFn2
在这种情况下,我们希望当修改 obj.foo 时会触发 effectFn1 执行。由于 effectFn2 嵌套在 effectFn1 里,所以会间接触发 effectFn2 执行,而当修改 obj.bar 时,只会触发 effectFn2 执行。
我们用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。
由于当前的 activeEffect 是一个全局遍历,当 effect 函数嵌套时,他始终是指向内层的副作用函数,外层副作用函数则失效。因此解决办法是建立一个副作用函数栈,将当前之前的副作用函数压入栈中,执行完后弹出栈,并始终让 activeEffect 执行栈顶的副作用函数。
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
foo: true,
bar: true
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []
// effect 用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在执行副作用函数之前,将副作用函数压入栈中
effectStack.push(effectFn)
// 执行副作用函数
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
const effectsToRun = new Set(effects)
effectsToRun.forEach(effectFn => effectFn())
// effects && effects.forEach(effectFn => effectFn()) // 删除
}
// 清空副作用函数的依赖集
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集中删除
deps.delete(effectFn)
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表操作成功
return true
}
})
let temp1, temp2
// 匿名函数注册副作用函数
effect(function effectFn1() {
console.log('effectFn1')
effect(function effectFn2() {
console.log('effectFn2')
temp2 = obj.bar
})
temp1 = obj.foo
})
避免无限递归循环
const data = { foo: 1 }
const obj = new Proxy(data, { /*...*/ })
effect(() => obj.foo++)
effect 注册副作用函数是内部有一个自增操作,该操作会引起栈溢出:
Uncaught RangeError: Maximum call stack size exceeded
引发这个错误的原因:
effect(() => {
obj.foo = obj.foo + 1
})
在这个语句中,既会读取 obj.foo 的值,又会设置obj.foo 的值。
首先读取 obj.foo 的值,这会触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执行。这样会导致无限递归地调用自己,于是就产生了栈溢出。
读取和设置 操作是在同一个副作用函数内进行的。此时无论是 track 时收集的副作用函数,还是 trigger 时要触发执行的副作用函数,都是 activeEffect。基于此,我们可以在 trigger 动作发生时增加守卫条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
foo: 1
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []
// effect 用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在执行副作用函数之前,将副作用函数压入栈中
effectStack.push(effectFn)
// 执行副作用函数
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
const effectsToRun = new Set(effects)
effectsToRun.forEach(effectFn => {
if (activeEffect !== effectFn) {
effectFn()
}
})
// effects && effects.forEach(effectFn => effectFn()) // 删除
}
// 清空副作用函数的依赖集
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集中删除
deps.delete(effectFn)
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表操作成功
return true
}
})
// 匿名函数注册副作用函数
effect(() => {
obj.foo ++
})
调度执行
可调度性是响应系统非常重要的特性。首先我们需要明确什么可调度性。所谓可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。
例如:
const data = { foo: 1 }
const obj = new Proxy(data, { /*...*/ })
effect(() => {
console.log(obj.foo)
})
obj.foo++
console.log('结束了')
// 执行的顺序为 1 2 '结束了'
// 如果我们要将执行的顺序改变为 1 '结束了' 2
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
foo: 1
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []
// effect 用于注册副作用函数
function effect(fn, options) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在执行副作用函数之前,将副作用函数压入栈中
effectStack.push(effectFn)
// 执行副作用函数
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// 将 options 挂载到 副作用函数
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
const effectsToRun = new Set(effects)
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果调度函数存在则执行调度函数,参数为副作用函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
// effects && effects.forEach(effectFn => effectFn()) // 删除
}
// 清空副作用函数的依赖集
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集中删除
deps.delete(effectFn)
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表操作成功
return true
}
})
// 匿名函数注册副作用函数
effect(() => {
console.log(obj.foo)
}, {
// options
scheduler(fn) {
setTimeout(fn)
}
})
obj.foo++
console.log(123)
除了控制函数的执行时机,控制函数的执行次数也是尤为重要,例如:
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })
effect(() => {
console.log(obj.foo)
})
obj.foo++
obj.foo++
// 执行结果 1 2 3
// 我们的需求是跳过中间的过度阶段,期待的打印结果 1 3
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
foo: 1
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []
// effect 用于注册副作用函数
function effect(fn, options) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在执行副作用函数之前,将副作用函数压入栈中
effectStack.push(effectFn)
// 执行副作用函数
fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将 options 挂载到 副作用函数
effectFn.options = options
}
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 执行副作用函数
effectFn()
}
// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
const effectsToRun = new Set(effects)
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果调度函数存在则执行调度函数,参数为副作用函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
// effects && effects.forEach(effectFn => effectFn()) // 删除
}
// 清空副作用函数的依赖集
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集中删除
deps.delete(effectFn)
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表操作成功
return true
}
})
// 定义一个队列任务
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 一个标志代表正在刷新队列
let isFlushing = false
function flushJob() {
// 如果队列正在刷新,什么都不做
if (isFlushing) return
// 第一次执行,将标志设置为 true,后面则会跳过刷新阶段
isFlushing = true
// 在微任务队列中刷新 jobQueue 队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
// 结束后将刷新标志设为 false
isFlushing = false
})
}
// 匿名函数注册副作用函数
effect(() => {
console.log(obj.foo)
}, {
// options
scheduler(fn) {
// 每次调度时,将副作用函数添加到 jobQueue 队列中
jobQueue.add(fn)
// 调用 flushJob 刷新队列
flushJob()
}
})
obj.foo++
console.log(123)
计算属性与 lazy
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
foo: 1,
bar: 2
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []
// effect 用于注册副作用函数
function effect(fn, options) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在执行副作用函数之前,将副作用函数压入栈中
effectStack.push(effectFn)
// 将 fn 的执行结果存储于 res
const res = fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将 res 作为 effectFn 的返回值
return res
}
// 将 options 挂载到 副作用函数
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 如果用户没有设置 lazy 则立即执行副作用函数
if (!options.lazy) {
// 执行副作用函数
effectFn()
}
return effectFn
}
// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
const effectsToRun = new Set(effects)
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
effectFn()
})
// effects && effects.forEach(effectFn => effectFn()) // 删除
}
// 清空副作用函数的依赖集
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集中删除
deps.delete(effectFn)
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表操作成功
return true
}
})
// 定义一个计算属性函数
function computed(getter) {
//把 getter 作为副作用函数,创建一个 lazy 的 effect
const effectFn = effect(getter, {
lazy: true
})
return {
// 当读取到 value 时,才执行 effectFn
get value() {
return effectFn()
}
}
}
const sumRes = computed(() => obj.bar + obj.foo)
console.log(sumRes)
// 存储副作用函数的桶
let bucket = new WeakMap()
// 原始数据
const data = {
foo: 1,
bar: 2
}
// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []
// effect 用于注册副作用函数
function effect(fn, options) {
const effectFn = () => {
// 调用 cleanup 函数完成清除工作
cleanup(effectFn)
// 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
activeEffect = effectFn
// 在执行副作用函数之前,将副作用函数压入栈中
effectStack.push(effectFn)
// 将 fn 的执行结果存储于 res
const res = fn()
// 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将 res 作为 effectFn 的返回值
return res
}
// 将 options 挂载到副作用函数
effectFn.options = options
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 如果用户没有设置 lazy 则立即执行副作用函数
if (!options.lazy) {
// 执行副作用函数
effectFn()
}
return effectFn
}
// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
// 判断桶中是否有副作用函数 否则 return
if (!activeEffect) return target[key]
// 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
let depsMap = bucket.get(target)
// 如果不存在,则建立一个 Map 与 target 关联
if (!depsMap) {
depsMap = new Map()
bucket.set(target, depsMap)
}
// 在根据 key 从 depsMap 中取出 deps,他是一个 Set
let deps = depsMap.get(key)
// 如果不存在则建立一个 Set 并与 key 关联
if (!deps) {
deps = new Set
depsMap.set(key, deps)
}
// 将副作用函数添加到存储桶
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
// 将其添加到 activeEffect.deps 数组中
activeEffect.deps.push(deps)
}
// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
// 根据 target 从桶中取出一个 depsMap
const depsMap = bucket.get(target)
if (!depsMap) return true
// 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
const effects = depsMap.get(key)
// 提取出副作用函数
const effectsToRun = new Set(effects)
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
// 如果调度函数存在则执行调度函数,参数为副作用函数
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
// effects && effects.forEach(effectFn => effectFn()) // 删除
}
// 清空副作用函数的依赖集
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
// deps 是依赖集
const deps = effectFn.deps[i]
// 将 effectFn 从依赖集中删除
deps.delete(effectFn)
// 最后需要重置 effectFn.deps 数组
effectFn.deps.length = 0
}
}
// 对原始数据代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数 activeEffect 添加到存储副作用函数的桶中
track(target, key)
// 返回属性值
return target[key]
},
// 拦截设置操作
set(target, key, newValue, receiver) {
// 设置属性值
target[key] = newValue
// 把副作用函数从桶里取出并执行
trigger(target, key)
// 返回 true 代表操作成功
return true
}
})
// 定义一个队列任务
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 一个标志代表正在刷新队列
let isFlushing = false
function flushJob() {
// 如果队列正在刷新,什么都不做
if (isFlushing) return
// 第一次执行,将标志设置为 true,后面则会跳过刷新阶段
isFlushing = true
// 在微任务队列中刷新 jobQueue 队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
// 结束后将刷新标志设为 false
isFlushing = false
})
}
// 定义一个计算属性函数
function computed(getter) {
// value 用来缓存上一次的值
let value
// dirty 用来标记是否需要重新执行,true 则意味着脏,需要重新执行
let dirty = true
//把 getter 作为副作用函数,创建一个 lazy 的 effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将 dirty 重置为 true
scheduler() {
dirty = true
// 当计算属性依赖的响应式数据变化时,手动调用 trigger 函数触发响应
trigger(obj, 'value')
}
})
return {
// 当读取到 value 时,才执行 effectFn
get value() {
// 只有“脏”时才计算值,并将得到的值缓存到 value 中
if (dirty) {
value = effectFn()
// 将 dirty 设置为 false,下一次访问直接使用缓存到 value 中的值
dirty = false
}
// 当读取 value 时,手动调用 track 函数进行追踪
track(obj, 'value')
return value
}
}
}
const sumRes = computed(() => obj.bar + obj.foo)