Vue.js 设计与实现
第一章

响应式数据
PROXY
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // 拦截写入属性操作
if (typeof val == 'number') {
alert(prop)
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // 添加成功
numbers.push(2); // 添加成功
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError(proxy 的 'set' 返回 false)
alert("This line is never reached (error in the line above)");这一段代码中,prop会打印出0,1(index)和length,非常神奇
声明式的声明UI

我司的低代码平台也是使用对象式的声明一切的,包括属性、样式、事件、层级、
这远没有 JavaScript对象灵活。而使用 JavaScript对象来描述 UI的方式,其实就是所谓的虚拟 DOM。现在大家应该觉得虚拟 DOM其实也没有那么神秘了吧。正是因为虚拟 DOM的这种灵活性,Vue.js 3除了支持使用模板描述 UI外,还支持使用虚拟 DOM描述 UI。其实我们在 Vue.js组件中手写的渲染函数就是使用虚拟 DOM来描述 UI的,如以下代码所示:
jsimport { h } from 'vue' export default { render() { return h('h1',{ onClick: handler})//虚拟 DOM }}有的读者可能会说,这里是 h函数调用呀,也不是 JavaScript对象啊。其实 h函数的返回值就是一个对象,其作用是让我们编写虚拟DOM变得更加轻松。如果把上面 h函数调用的代码改成 JavaScript对象,就需要写更多内容:
jsexport default { render() { return { tag: 'h1', props: { onClick: handler } } } }
标记一下,有时间去学一下虚拟DOM
渲染器

创建元素:把 vnode.tag作为标签名称来创建 DOM元素。
为元素添加属性和事件:遍历 vnode.props对象,如果 key以on字符开头,说明它是一个事件,把字符 on截取掉后再调用toLowerCase函数将事件名称小写化,最终得到合法的事件名称,例如 onClick会变成 click,最后调用addEventListener绑定事件处理函数。
处理 children:如果 children是一个数组,就递归地调用renderer继续渲染,注意,此时我们要把刚刚创建的元素作为挂载点(父节点);如果 children是字符串,则使用createTextNode函数创建一个文本节点,并将其添加到新创建的元素内。
编译器
vue<template> <div @click="handler">click me</div> </template> <script> export default { data() { /* ... */ }, methods: { handler: () => {/* ... */}}} </script>其中
<template>标签里的内容就是模板内容,编译器会把模板内容编译成渲染函数并添加到<script>标签块的组件对象上,所以最终在浏览器里运行的代码就是:jsexport default {data() {/* ... */}, methods: { handler: () => {/* ... */} },render() { return h('div', { onClick: handler }, 'click me')}}
表示某一项数据是动态的
jsrender() { return { tag: 'div', props: {id: 'foo',class: cls}, patchFlags: 1 //假设数字 1代表 class是动态的 } }
副作用函数和响应式数据
const obj = { text: 'hello world' }
function effect() {
document.body.innerText = obj.text
}
obj.text = 'hello vue3'在这个例子中effect是副作用函数,它更改了html的文本内容,由于其他函数可能会读取或者调用html的文本,所以它是会产生影响的,所以是副作用函数。
另外,可以用另一种方式理解,document.body.innerText长得就很像全局变量,修改了全局变量的函数是副作用函数。
相应式数据,我们修改了obj.text的内容,希望这个修改能够作用到全局,就希望副作用函数effect再执行一次。(实际上是通知所有用到obj.text的函数或语句再执行一次)
简单分析,可以知道effect执行会触发obj.text的读取,知道修改obj.text会触发obj.text的设置。
那么只要有这么一个桶(代理人),监控obj.text的设置和读取就可以了。
//存储副作用函数的桶
const bucket = new Set()
//原始数据
const data = { text: 'hello world' }
//对原始数据的代理
const obj = new Proxy(data, {
//拦截读取操作
get(target, key) {
//将副作用函数 effect添加到存储副作用函数的桶中
bucket.add(effect)
//返回属性值
return target[key]
},
//拦截设置操作
set(target, key, newVal) {
//设置属性值
target[key] = newVal
//把副作用函数从桶里取出并执行
bucket.forEach(fn => fn())
//返回 true代表设置操作成功
return true
}
})
响应式的实现
let activeEffect
let document = {
body: {}
}
const data = { text: 'hello world' }
const bucket = new Map()
const obj = new Proxy(data, {
get(target, key) {
// 没有副作用 直接返回
if (!activeEffect) {
return target[key]
}
// 处理bucket,他应该是一个三级的树形结构形如:
/**
* bucket: {
* obj1: {
* text1: 'some',
* text2: 'some'
* },
* obj2: {
* text1: 'some',
* text2: 'some'
* }
* }
*/
// target 预期是obj1
let objMap = bucket.get(target)
// 如果没有objMap 说明是第一次读取这个target 那么为其新增一个空白的
if (!objMap) {
objMap = new Map()
bucket.set(target, objMap)
}
// dep 预期是text1
let dep = objMap.get(key)
if (!dep) {
dep = new Set()
objMap.set(key, dep)
}
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
dep.add(activeEffect)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
// 设置的时候 通知对应的修改函数
// 预期拿到obj1
let objMap = bucket.get(target)
// 说明target不代理这个
if (!objMap) {
return true
}
// 预期拿到text1
let effects = objMap.get(key)
if (!effects) {
return true
}
// 拿到text1,text1是一个set对象,里面装载了若干的effect函数,通知这些函数执行
effects.forEach(fn => fn())
}
})
function effect(fn) {
activeEffect = fn
fn()
}
effect(
() => {
console.log('effect run')
document.body.innerText = obj.text
}
)
setTimeout(() => {
obj.notExist = 'hello vue3'
}, 1000)化简并且将逻辑封装到函数
let activeEffect
let document = {
body: {}
}
const data = { text: 'hello world' }
const bucket = new WeakMap()
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return target[key]
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
function effect(fn) {
activeEffect = fn
fn()
}
effect(
() => {
console.log('effect run')
document.body.innerText = obj.text
}
)
setTimeout(() => {
// obj.notExist = 'hello vue3'
obj.text = 'hello vue3'
}, 1000)分支管理和clean up
在这个用例中,包含分支管理,错误执行了不需要执行的副作用函数
let activeEffect
let document = {
body: {}
}
const data = { ok: true, text: 'hello world' }
const bucket = new Map()
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return target[key]
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
function effect(fn) {
activeEffect = fn
fn()
}
effect(
() => {
document.body.innerText = obj.ok ? obj.text : 'Not!'
console.log('effect run', document.body.innerText)
}
)
setTimeout(() => {
// obj.notExist = 'hello vue3'
obj.ok = false
obj.text = 'hello vue3'
}, 1500)
// 实际输出
// effect run hello world
// effect run Not!
// effect run Not!
// 期望输出
// effect run hello world
// effect run Not!期望在obj.ok被设置为false后,不论怎么改变obj.text的值,都不会触发副作用函数。这需要每次副作用函数执行时,把它从所有与之关联的依赖集合中删除。
比如说



原来的流程为读obj.ok,为obj.ok绑effectFn,读obj.text,为obj.ok绑effectFn,之后写obj.ok和obj.text。明明effectFn读取obj.text的值依赖obj.ok的值,但是之后的每一次写,无视了obj.ok的值,多余的执行了effectFn。
那么我们希望实现一个效果,每次执行effectFn的时候,根据走过的读取,重新绑定相应的响应。比如说。
effect(
function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'Not!'
console.log('effect run', document.body.innerText)
}
)因为obj.ok为false,那么执行的effectFn实际上为这段内容
function effectFn() {
document.body.innerText = obj.ok ? 'NOT EXCUTE' : 'Not!'
console.log('effect run', document.body.innerText)
}然而,因为遗留问题。我们实际上执行了这些代码。
setTimeout(() => {
// obj.notExist = 'hello vue3'
obj.ok = false
obj.text = 'hello vue3'
obj.text = 'hello vue3'
obj.text = 'hello vue3'
obj.text = 'hello vue3'
obj.text = 'hello vue3'
obj.text = 'hello vue3'
obj.text = 'hello vue3'
}, 1500)obj.text仍然绑定了响应式的数据通知,导致明明没有这个数据的应用,仍然触发了effectFn。
所以我们要为这个解绑,也就是说,需要每次副作用函数执行时,把它从所有与之关联的依赖集合中删除。
// 副作用函数
function effect(fn) {
// 当effectFn执行时,将其设置为当前激活的副作用函数
activeEffect = fn
fn()
}
// 包装成简单的函数
function effect(fn) {
const effectFnObj = {}
effectFnObj.func = () => {
// 当effectFnObj执行时,将其设置为当前激活的副作用函数
activeEffect = effectFnObj
fn()
}
// activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
effectFnObj.deps = []
effectFnObj.func()
}<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const data = { ok: true, text: 'hello world' }
const bucket = new WeakMap()
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set(effects)
effectsToRun.forEach(item => item.func())
}
function effect(fn) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
activeEffect = effectFn
console.log(activeEffect)
fn()
}
}
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
effectFn.func()
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(
() => {
document.body.innerText = obj.ok ? obj.text : 'Not!'
console.log('effect run', document.body.innerText)
}
)
setTimeout(() => {
// obj.notExist = 'hello vue3'
obj.ok = false
// obj.ok = true
obj.text = 'hello vue3'
}, 1500)
</script>
</html>这个代码存在一些嵌套的问题,非常难理解。
嵌套执行的effect
对于这个用例,我们修改了obj.foo,那么期望触发effectFn1和effectFn2(间接),然而结果不对。
effect(function effectFn1 () {
console.log('effectFn1 执行')
effect(function effectFn2() {
console.log('effectFn2 执行')
temp2 = obj.bar
})
temp1 = obj.foo
}
)
setTimeout(() => {
obj.foo = false
// obj.bar = false
}, 1500)
// 期望的输出
// effectFn1 执行
// effectFn2 执行
// effectFn1 执行
// effectFn2 执行
// 实际输出
// effectFn1 执行
// effectFn2 执行
// effectFn2 执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: true, bar: true }
const bucket = new WeakMap()
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
let temp1, temp2
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set(effects)
effectsToRun.forEach(item => item.func())
}
function effect(fn) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
effectFn.func()
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(function effectFn1 () {
console.log('effectFn1 执行')
effect(function effectFn2() {
console.log('effectFn2 执行')
temp2 = obj.bar
})
temp1 = obj.foo
}
)
setTimeout(() => {
obj.foo = false
// obj.bar = false
}, 1500)
</script>
</html>避免无限递归
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: 1 }
const bucket = new WeakMap()
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
let temp1, temp2
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(item => item.func())
}
function effect(fn) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
effectFn.func()
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect( () => obj.foo++
)
setTimeout(() => {
// obj.foo = false
// // obj.bar = false
}, 1500)
</script>
</html>调度执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: 1 }
const bucket = new WeakMap()
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
let temp1, temp2
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
effectFn.func()
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(
() => console.log(obj.foo),
{
// 传入一个额外的对象作为参数,options
scheduler(fn) {
// 将副作用函数放到宏任务队列中执行
setTimeout(fn)
}
}
)
obj.foo++
console.log('结束了')
setTimeout(() => {
// obj.foo = false
// // obj.bar = false
}, 1500)
</script>
</html>
// 原来的输出
// 1
// 2
// 结束了
// 现在的输出
// 1
// 结束了
// 2这样可以解决一个需求,就是先让用户执行的代码执行,再让响应式的数据更新。
不包含过渡状态的响应式更新
effect(
() => console.log(obj.foo)
)
obj.foo++
obj.foo++
// 这样的代码,会输出
// 1
// 2
// 3但是显然,我们在用户的角度上,应该不关心2,2只是一个过渡状态,我们期望输出1,3。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: 1 }
const bucket = new WeakMap()
// 定义一个任务队列
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(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
let temp1, temp2
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
effectFn.func()
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(
() => console.log(obj.foo),
{
scheduler(fn) {
// 每次调度时,将副作用函数添加到jobQueue队列
jobQueue.add(fn)
// 调用flushJob刷新队列
flushJob()
}
}
)
obj.foo++
obj.foo++
</script>
</html>观察上面的代码,首先,我们定义了一个任务队列 jobQueue,它是一个 Set数据结构,目的是利用 Set数据结构的自动去重能力。接着我们看调度器 scheduler的实现,在每次调度执行时,先将当前副作用函数添加到 jobQueue队列中,再调用 flushJob函数刷新队列。然后我们把目光转向 flushJob函数,该函数通过isFlushing标志判断是否需要执行,只有当其为 false时才需要执行,而一旦 flushJob函数开始执行,isFlushing标志就会设置为true,意思是无论调用多少次 flushJob函数,在一个周期内都只会执行一次。需要注意的是,在 flushJob内通过 p.then将一个函数添加到微任务队列,在微任务队列内完成对 jobQueue的遍历执行。
整段代码的效果是,连续对 obj.foo执行两次自增操作,会同步且连续地执行两次 scheduler调度函数,这意味着同一个副作用函数会被 jobQueue.add(fn)语句添加两次,但由于 Set数据结构的去重能力,最终 jobQueue中只会有一项,即当前副作用函数。类似地,flushJob也会同步且连续地执行两次,但由于 isFlushing标志的存在,实际上 flushJob函数在一个事件循环内只会执行一次,即在微任务队列内执行一次。当微任务队列开始执行时,就会遍历jobQueue并执行里面存储的副作用函数。由于此时 jobQueue队列内只有一个副作用函数,所以只会执行一次,并且当它执行时,字段obj.foo的值已经是 3了,这样我们就实现了期望的输出。

这五个和这两个是分别同步执行的,每个分别同步执行的语块,我们不希望出现过渡态。
留坑
function flushJob() {
// 如果任务正在刷新,则什么都不做
if (isFlushing) {
return
}
// 设置为true,代表正在刷新
isFlushing = true
// 在微任务队列刷新jobQueue队列
jobQueue.forEach(job => job())
// 结束后重置isFlushing
isFlushing = false
}
obj.foo++
obj.foo++
obj.foo++
obj.foo++
obj.foo++
setTimeout(() => {
obj.foo++
obj.foo++
}, 1500)打印1,2,3,4,5,6。稍等7,8
function flushJob() {
// 如果任务正在刷新,则什么都不做
if (isFlushing) {
return
}
// 设置为true,代表正在刷新
isFlushing = true
// 在微任务队列刷新jobQueue队列
jobQueue.forEach(job => job())
// 结束后重置isFlushing
setTimeout(() => isFlushing = false)
}
obj.foo++
obj.foo++
obj.foo++
obj.foo++
obj.foo++
setTimeout(() => {
obj.foo++
obj.foo++
}, 1500)打印1,2。稍等7
计算属性computed
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: 41, bar: 22 }
const bucket = new WeakMap()
// 定义一个任务队列
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(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
const sumRes = computed(() => obj.foo + obj.bar)
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 在这里拿到fn的返回值
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
// 将res作为effectFn的返回值
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
// 只有非lazy的时候,才执行
if (!options.lazy) {
effectFn.func()
}
// 将副作用函数作为返回值返回
return effectFn.func
}
function computed(getter) {
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {lazy: true})
const obj = {
// 当读取value时,才执行effectFn
get value() {
return effectFn()
}
}
return obj
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
console.log(sumRes.value)
</script>
</html>完整功能的computed
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: 1, bar: 2 }
const bucket = new WeakMap()
// 定义一个任务队列
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(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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 sumRes = computed(() => obj.foo + obj.bar)
effect(() => document.body.innerHTML = sumRes.value)
obj.foo++
</script>
</html>比较完善的watch
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: 1, bar: 2 }
const bucket = new WeakMap()
// 定义一个任务队列
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(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
let temp1, temp2
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb) {
// 定义getter
let getter
// 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(souce)
}
// 定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
scheduler() {
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 当数据变化时,调用回调函数cb
// 将旧值和新值作为回调函数的参数
cb(newValue, oldValue)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
}
)
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
// 递归读取值
function traverse(value, seen = new Set()) {
// 如果要读取的是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === 'null' || seen.has(value)) {
return
}
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设value就是一个对象,使用 for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const key in value) {
traverse(value[key], seen)
}
// 暂时的,这个返回value无用
return value
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
watch(() => obj.foo, (newValue, oldValue) => console.log(`obj.foo的值变了,newValue ${newValue}, oldValue ${oldValue}`))
obj.bar++
obj.foo++
obj.foo++
</script>
</html>完整实现的watch
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: 1, bar: 2 }
const bucket = new WeakMap()
// 定义一个任务队列
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(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
}
})
let temp1, temp2
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
let effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
// cleanup用来存储用户注册的过期回调
let cleanup
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn
}
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 当数据变化时,调用回调函数cb
// 将旧值和新值作为回调函数的参数
// 将onInvalidate作为回调函数的第三个参数,以便用户使用
cb(newValue, oldValue, onInvalidate)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
// 使用job函数作为调度器函数
scheduler: () => {
// 在调度函数中判断flush是否为post,如果是,将其放到微任务队列中执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
// 当immediate 为true的时候立即执行job,从而触发回调函数
job()
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
}
// 递归读取值
function traverse(value, seen = new Set()) {
// 如果要读取的是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === 'null' || seen.has(value)) {
return
}
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设value就是一个对象,使用 for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const key in value) {
traverse(value[key], seen)
}
// 暂时的,这个返回value无用
return value
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
watch(
obj,
async (newValue, oldValue, onInvalidate) => {
// 定义一个标志,代表当前副作用函数是否过期,默认为false,代表没有过期
let expired = false
// 调用onInvalidate函数注册一个过期回调
onInvalidate(() => {
// 当过期时,将expired设置为true
expired = true
})
// 发送网络请求
const res = await fetch('/path/to/request')
// 只有当该副作用函数的执行没有过期时,才会执行后续操作
if (!expired) {
finnalData = res
}
},
{
// // 回调函数会在watch创建时立即执行一次
// immediate: true
// 回调函数会在watch创建时立即执行一次
flush: 'pre'
}
)
obj.foo++
setTimeout(() => {
obj.foo++
}, 200);
</script>
</html>总结
在本章中,我们首先介绍了副作用函数和响应式数据的概念,以及它们之间的关系。一个响应式数据最基本的实现依赖于对“读取”和“设置”操作的拦截,从而在副作用函数与响应式数据之间建立联系。当“读取”操作发生时,我们将当前执行的副作用函数存储到“桶”中;当“设置”操作发生时,再将副作用函数从“桶”里取出并执行。这就是响应系统的根本实现原理。
接着,我们实现了一个相对完善的响应系统。使用 WeakMap配合Map构建了新的“桶”结构,从而能够在响应式数据与副作用函数之间建立更加精确的联系。同时,我们也介绍了 WeakMap与 Map这两个数据结构之间的区别。WeakMap是弱引用的,它不影响垃圾回收器的工作。当用户代码对一个对象没有引用关系时,WeakMap不会阻止垃圾回收器回收该对象。
我们还讨论了分支切换导致的冗余副作用的问题,这个问题会导致副作用函数进行不必要的更新。为了解决这个问题,我们需要在每次副作用函数重新执行之前,清除上一次建立的响应联系,而当副作用函数重新执行后,会再次建立新的响应联系,新的响应联系中不存在冗余副作用问题,从而解决了问题。但在此过程中,我们还遇到了遍历 Set数据结构导致无限循环的新问题,该问题产生的原因可以从ECMA规范中得知,即“在调用 forEach遍历 Set集合时,如果一个值已经被访问过了,但这个值被删除并重新添加到集合,如果此时forEach遍历没有结束,那么这个值会重新被访问。”解决方案是建立一个新的 Set数据结构用来遍历。
然后,我们讨论了关于嵌套的副作用函数的问题。在实际场景中,嵌套的副作用函数发生在组件嵌套的场景中,即父子组件关系。这时为了避免在响应式数据与副作用函数之间建立的响应联系发生错乱,我们需要使用副作用函数栈来存储不同的副作用函数。当一个副作用函数执行完毕后,将其从栈中弹出。当读取响应式数据的时候,被读取的响应式数据只会与当前栈顶的副作用函数建立响应联系,从而解决问题。而后,我们遇到了副作用函数无限递归地调用自身,导致栈溢出的问题。该问题的根本原因在于,对响应式数据的读取和设置操作发生在同一个副作用函数内。解决办法很简单,如果 trigger触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。
随后,我们讨论了响应系统的可调度性。所谓可调度,指的是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。为了实现调度能力,我们为 effect函数增加了第二个选项参数,可以通过 scheduler选项指定调用器,这样用户可以通过调度器自行完成任务的调度。我们还讲解了如何通过调度器实现任务去重,即通过一个微任务队列对任务进行缓存,从而实现去重。
而后,我们讲解了计算属性,即 computed。计算属性实际上是一个懒执行的副作用函数,我们通过 lazy选项使得副作用函数可以懒执行。被标记为懒执行的副作用函数可以通过手动方式让其执行。利用这个特点,我们设计了计算属性,当读取计算属性的值时,只需要手动执行副作用函数即可。当计算属性依赖的响应式数据发生变化时,会通过 scheduler将 dirty标记设置为 true,代表“脏”。这样,下次读取计算属性的值时,我们会重新计算真正的值。
之后,我们讨论了 watch的实现原理。它本质上利用了副作用函数重新执行时的可调度性。一个 watch本身会创建一个 effect,当这个 effect依赖的响应式数据发生变化时,会执行该 effect的调度器函数,即 scheduler。这里的 scheduler可以理解为“回调”,所以我们只需要在 scheduler中执行用户通过 watch函数注册的回调函数即可。此外,我们还讲解了立即执行回调的 watch,通过添加新的 immediate选项来实现,还讨论了如何控制回调函数的执行时机,通过 flush选项来指定回调函数具体的执行时机,本质上是利用了调用器和异步的微任务队列。
最后,我们讨论了过期的副作用函数,它会导致竞态问题。为了解决这个问题,Vue.js为 watch的回调函数设计了第三个参数,即onInvalidate。它是一个函数,用来注册过期回调。每当 watch的回调函数执行之前,会优先执行用户通过 onInvalidate注册的过期回调。这样,用户就有机会在过期回调中将上一次的副作用标记为“过期”,从而解决竞态问题。
非原始值的相应方案
以上,我们使用Proxy,其能代理对象响应式,然而,我们仍需要处理非原始值的相应。
既然 Vue.js 3的响应式数据是基于 Proxy实现的,那么我们就有必要了解 Proxy以及与之相关联的 Reflect。什么是 Proxy呢?简单地说,使用 Proxy可以创建一个代理对象。它能够实现对其他对象的代理,这里的关键词是其他对象,也就是说,Proxy只能代理对象,无法代理非对象值,例如字符串、布尔值等。那么,代理指的是什么呢?所谓代理,指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。这句话的关键词比较多,我们逐一解释。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
let activeEffect
const effectStack = []
const data = { foo: 1 }
const bucket = new WeakMap()
// 定义一个任务队列
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(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
const ITERATE_KEY = Symbol()
const obj = new Proxy(data, {
// 拦截读取操作,接受第三个参数receiver
get(target, key, receiver) {
track(target, key)
// 使用reflect.get返回读取到的属性值
return Reflect.get(target, key, receiver)
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
// 将副作用函数与ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
set(target, key, newValue, receiver) {
// 拦截设置操作
// 如果属性不存在,则说明是添加新属性,否则时设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newValue, receiver)
// 将type作为第三个参数传递给trigger函数
// target[key] = newValue
trigger(target, key, type)
},
deleteProperty(target, key) {
// 检查被操作的属性是否时对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, 'DELETE')
}
return res
}
})
let temp1, temp2
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key, type) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
// 取得与key相关联的副作用函数
let effects = depsMap.get(key)
// 取得与ITERATE_KEY相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
// 将与key相关联的副作用函数添加到effectsToRun
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 只有当操作类型为ADD 或者 DELETE时,才触发与ITERATE_KEY相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE') {
// 将与ITERATE_KEY相关联的副作用函数也添加到effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
// cleanup用来存储用户注册的过期回调
let cleanup
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn
}
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 当数据变化时,调用回调函数cb
// 将旧值和新值作为回调函数的参数
// 将onInvalidate作为回调函数的第三个参数,以便用户使用
cb(newValue, oldValue, onInvalidate)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
// 使用job函数作为调度器函数
scheduler: () => {
// 在调度函数中判断flush是否为post,如果是,将其放到微任务队列中执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
// 当immediate 为true的时候立即执行job,从而触发回调函数
job()
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
}
// 递归读取值
function traverse(value, seen = new Set()) {
// 如果要读取的是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === 'null' || seen.has(value)) {
return
}
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设value就是一个对象,使用 for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const key in value) {
traverse(value[key], seen)
}
// 暂时的,这个返回value无用
return value
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(() => {
for(const key in obj) {
console.log(key)
}
})
obj.foo = 1
</script>
</html>这里有一处问题,仅打印一次key,这是我们所期望的,因为obj.foo的值相当于重复设置相同的值。然而,如果改为console.log(key, obj[key]),就会触发两次,这是我们不期望的。
如上面的代码所示,p.foo的初始值为 1,当为 p.foo设置新的值时,如果值没有发生变化,则不需要触发响应。为了满足需求,我们需要修改 set拦截函数的代码,在调用 trigger函数触发响应之前,需要检查值是否真的发生了变化
代码忘记cv了,在这先存个档
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
// 定义区
let activeEffect
const effectStack = []
// const data = { foo: 1 }
const obj = {}
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
// 使用parent作为child的原型
Object.setPrototypeOf(child, parent)
const bucket = new WeakMap()
// 定义一个任务队列
const jobQueue = new Set()
// 使用Promise.resolve() 创建一个promise实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 用一个标志代表是否正在刷新队列
let isFlushing = false
const ITERATE_KEY = Symbol()
function flushJob() {
// 如果任务正在刷新,则什么都不做
if (isFlushing) return
// 设置为true,代表正在刷新
isFlushing = true
// 在微任务队列刷新jobQueue队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
function reactive(obj) {
return new Proxy(obj, {
// 拦截读取操作,接受第三个参数receiver
get(target, key, receiver) {
track(target, key)
// 使用reflect.get返回读取到的属性值
return Reflect.get(target, key, receiver)
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
// 将副作用函数与ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
set(target, key, newValue, receiver) {
// 拦截设置操作
// 先获取旧值
const oldValue = target[key]
// 如果属性不存在,则说明是添加新属性,否则时设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newValue, receiver)
// 比较新值和旧值,只要当不全等的时候才触发相应
if (oldValue !== newValue) {
// 将type作为第三个参数传递给trigger函数
// target[key] = newValue // 使用了Reflect.set设置,不必使用此句
trigger(target, key, type)
}
return res
},
deleteProperty(target, key) {
// 检查被操作的属性是否时对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, 'DELETE')
}
return res
}
})
}
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key, type) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
// 取得与key相关联的副作用函数
let effects = depsMap.get(key)
// 取得与ITERATE_KEY相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
// 将与key相关联的副作用函数添加到effectsToRun
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 只有当操作类型为ADD 或者 DELETE时,才触发与ITERATE_KEY相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE') {
// 将与ITERATE_KEY相关联的副作用函数也添加到effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
// cleanup用来存储用户注册的过期回调
let cleanup
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn
}
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 当数据变化时,调用回调函数cb
// 将旧值和新值作为回调函数的参数
// 将onInvalidate作为回调函数的第三个参数,以便用户使用
cb(newValue, oldValue, onInvalidate)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
// 使用job函数作为调度器函数
scheduler: () => {
// 在调度函数中判断flush是否为post,如果是,将其放到微任务队列中执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
// 当immediate 为true的时候立即执行job,从而触发回调函数
job()
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
}
// 递归读取值
function traverse(value, seen = new Set()) {
// 如果要读取的是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === 'null' || seen.has(value)) {
return
}
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设value就是一个对象,使用 for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const key in value) {
traverse(value[key], seen)
}
// 暂时的,这个返回value无用
return value
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(() => {
console.log(child.bar)
})
child.bar = 2
</script>
</html>观察如上代码,我们定义了空对象 obj和对象 proto,分别为二者创建了对应的响应式数据 child和 parent,并且使用Object.setPrototypeOf方法将 parent设置为 child的原型。接着,在副作用函数内访问 child.bar的值。从代码中可以看出, child本身并没有 bar属性,因此当访问 child.bar时,值是从原型上继承而来的。但无论如何,既然 child是响应式数据,那么它与副作用函数之间就会建立联系,因此当我们执行 child.bar= 2时,期望副作用函数会重新执行。但如果你尝试运行上面的代码,会发现副作用函数不仅执行了,还执行了两次,这会造成不必要的更新。
为了搞清楚问题的原因,我们需要逐步分析整个过程。当在副作用函数中读取 child.bar的值时,会触发 child代理对象的 get拦截函数。我们知道,在拦截函数内是使用 Reflect.get(target, key, receiver)来得到最终结果的,对应到上例,这句话相当于:
Reflect.get(obj, 'bar', receiver)这其实是实现了通过 obj.bar来访问属性值的默认行为。也就是说,引擎内部是通过调用 obj对象所部署的[[Get]]内部方法来得到最终结果的,因此我们有必要查看规范 10.1.8.1节来了解[[Get]]内部方法的执行流程,如图 5-5所示。

图 5-5 [[Get]]内部方法的执行流程
图 5-5中的第 3步所描述的内容如下。
3.如果 desc是 undefined,那么
a.让 parent的值为? O.[[GetPrototypeOf]]()。
b.如果 parent是 null,则返回 undefined。
c.返回? parent.[[Get]](P, Receiver)。
在第 3步中,我们能够了解到非常关键的信息,即如果对象自身不存在该属性,那么会获取对象的原型,并调用原型的[[Get]]方法得到最终结果。对应到上例中,当读取 child.bar属性值时,由于child代理的对象 obj自身没有 bar属性,因此会获取对象 obj的原型,也就是 parent对象,所以最终得到的实际上是 parent.bar的值。但是大家不要忘了,parent本身也是响应式数据,因此在副作用函数中访问 parent.bar的值时,会导致副作用函数被收集,从而也建立响应联系。所以我们能够得出一个结论,即 child.bar和parent.bar都与副作用函数建立了响应联系。
但这仍然解释不了为什么当设置 child.bar的值时,会连续触发两次副作用函数执行,所以接下来我们需要看看当设置操作发生时的具体执行流程。我们知道,当执行 child.bar= 2时,会调用child代理对象的 set拦截函数。同样,在 set拦截函数内,我们使用 Reflect.set(target, key, newVal, receiver)来完成默认的设置行为,即引擎会调用 obj对象部署的[[Set]]内部方法,根据规范的 10.1.9.2节可知[[Set]]内部方法的执行流程,如图 5-6所示。

图 5-6中第 2步所描述的内容如下。
2.如果 ownDesc是 undefined,那么
a.让 parent的值为 O.[[GetPrototypeOf]]()。
b.如果 parent不是 null,则
I.返回? parent.[[Set]](P, V, Receiver);
c.否则
I.将 ownDesc设置为{[[Value]]: undefined,[[Writable]]: true,[[Enumerable]]: true,[[Configurable]]: true}。
由第 2步可知,如果设置的属性不存在于对象上,那么会取得其原型,并调用原型的[[Set]]方法,也就是 parent的[[Set]]内部方法。由于 parent是代理对象,所以这就相当于执行了它的 set拦截函数。换句话说,虽然我们操作的是 child.bar,但这也会导致parent代理对象的 set拦截函数被执行。前面我们分析过,当读取child.bar的值时,副作用函数不仅会被 child.bar收集,也会被parent.bar收集。所以当 parent代理对象的 set拦截函数执行时,就会触发副作用函数重新执行,这就是为什么修改 child.bar的值会导致副作用函数重新执行两次。
接下来,我们需要思考解决方案。思路很简单,既然执行两次,那么只要屏蔽其中一次不就可以了吗?我们可以把由 parent.bar触发的那次副作用函数的重新执行屏蔽。怎么屏蔽呢?我们知道,两次更新是由于 set拦截函数被触发了两次导致的,所以只要我们能够在set拦截函数内区分这两次更新就可以了。当我们设置 child.bar的值时,会执行 child代理对象的 set拦截函数:
// child的 set拦截函数
set(target, key, value, receiver) {
// target是原始对象 obj
// receiver是代理对象 child
}此时的 target是原始对象 obj,receiver是代理对象child,我们发现 receiver其实就是 target的代理对象。
但由于 obj上不存在 bar属性,所以会取得 obj的原型parent,并执行 parent代理对象的 set拦截函数:
// parent的 set拦截函数
set(target, key, value, receiver) {
// target是原始对象 proto
// receiver仍然是代理对象 child
}我们发现,当 parent代理对象的 set拦截函数执行时,此时target是原始对象 proto,而 receiver仍然是代理对象 child,
而不再是 target的代理对象。通过这个特点,我们可以看到target和 receiver的区别。由于我们最初设置的是 child.bar的值,所以无论在什么情况下,receiver都是 child,而 target则是变化的。根据这个区别,我们很容易想到解决办法,只需要判断receiver是否是 target的代理对象即可。只有当 receiver是target
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
// 定义区
let activeEffect
const effectStack = []
// const data = { foo: 1 }
const obj = {}
const proto = { bar: 1 }
const child = reactive(obj)
const parent = reactive(proto)
// 使用parent作为child的原型
Object.setPrototypeOf(child, parent)
const bucket = new WeakMap()
// 定义一个任务队列
const jobQueue = new Set()
// 使用Promise.resolve() 创建一个promise实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 用一个标志代表是否正在刷新队列
let isFlushing = false
const ITERATE_KEY = Symbol()
function flushJob() {
// 如果任务正在刷新,则什么都不做
if (isFlushing) return
// 设置为true,代表正在刷新
isFlushing = true
// 在微任务队列刷新jobQueue队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
function reactive(obj) {
return new Proxy(obj, {
// 拦截读取操作,接受第三个参数receiver
get(target, key, receiver) {
// 在这里,判断receiver是不是target的代理对象:代理对象可以通过raw属性访问原始数据(这个raw是我们约定的)
if (key === 'raw') {
return target
}
track(target, key)
// 使用reflect.get返回读取到的属性值
return Reflect.get(target, key, receiver)
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
// 将副作用函数与ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
set(target, key, newValue, receiver) {
// 拦截设置操作
// 先获取旧值
const oldValue = target[key]
// 如果属性不存在,则说明是添加新属性,否则时设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newValue, receiver)
// target === receiver.raw 说明receiver就是target的代理对象
if (target === receiver.raw) {
// 比较新值和旧值,只要当不全等的时候才触发相应
if (oldValue !== newValue) {
// 将type作为第三个参数传递给trigger函数
// target[key] = newValue // 使用了Reflect.set设置,不必使用此句
trigger(target, key, type)
}
}
return res
},
deleteProperty(target, key) {
// 检查被操作的属性是否时对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, 'DELETE')
}
return res
}
})
}
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key, type) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
// 取得与key相关联的副作用函数
let effects = depsMap.get(key)
// 取得与ITERATE_KEY相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
// 将与key相关联的副作用函数添加到effectsToRun
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 只有当操作类型为ADD 或者 DELETE时,才触发与ITERATE_KEY相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE') {
// 将与ITERATE_KEY相关联的副作用函数也添加到effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
// cleanup用来存储用户注册的过期回调
let cleanup
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn
}
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 当数据变化时,调用回调函数cb
// 将旧值和新值作为回调函数的参数
// 将onInvalidate作为回调函数的第三个参数,以便用户使用
cb(newValue, oldValue, onInvalidate)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
// 使用job函数作为调度器函数
scheduler: () => {
// 在调度函数中判断flush是否为post,如果是,将其放到微任务队列中执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
// 当immediate 为true的时候立即执行job,从而触发回调函数
job()
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
}
// 递归读取值
function traverse(value, seen = new Set()) {
// 如果要读取的是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === 'null' || seen.has(value)) {
return
}
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设value就是一个对象,使用 for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const key in value) {
traverse(value[key], seen)
}
// 暂时的,这个返回value无用
return value
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(() => {
console.log(child.bar)
})
console.log(child.raw === obj)
console.log(parent.raw === proto)
child.bar = 2
</script>
</html>浅相应和深相应
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
// 定义区
let activeEffect
const effectStack = []
const obj = reactive(
{
foo: {
bar: 1
}
}
)
const bucket = new WeakMap()
// 定义一个任务队列
const jobQueue = new Set()
// 使用Promise.resolve() 创建一个promise实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 用一个标志代表是否正在刷新队列
let isFlushing = false
const ITERATE_KEY = Symbol()
function flushJob() {
// 如果任务正在刷新,则什么都不做
if (isFlushing) return
// 设置为true,代表正在刷新
isFlushing = true
// 在微任务队列刷新jobQueue队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
function reactive(obj) {
return new Proxy(obj, {
// 拦截读取操作,接受第三个参数receiver
get(target, key, receiver) {
// 在这里,判断receiver是不是target的代理对象:代理对象可以通过raw属性访问原始数据(这个raw是我们约定的)
if (key === 'raw') {
return target
}
track(target, key)
// 使用reflect.get返回读取到的属性值
// 当读取属性值时,直接返回了结果,这不能实现深相应。
// return Reflect.get(target, key, receiver)
// 得到原始值结果
const res = Reflect.get(target, key, receiver)
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
// 将副作用函数与ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
set(target, key, newValue, receiver) {
// 拦截设置操作
// 先获取旧值
const oldValue = target[key]
// 如果属性不存在,则说明是添加新属性,否则时设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newValue, receiver)
// target === receiver.raw 说明receiver就是target的代理对象
if (target === receiver.raw) {
// 比较新值和旧值,只要当不全等的时候才触发相应
if (oldValue !== newValue) {
// 将type作为第三个参数传递给trigger函数
// target[key] = newValue // 使用了Reflect.set设置,不必使用此句
trigger(target, key, type)
}
}
return res
},
deleteProperty(target, key) {
// 检查被操作的属性是否时对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, 'DELETE')
}
return res
}
})
}
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key, type) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
// 取得与key相关联的副作用函数
let effects = depsMap.get(key)
// 取得与ITERATE_KEY相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
// 将与key相关联的副作用函数添加到effectsToRun
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 只有当操作类型为ADD 或者 DELETE时,才触发与ITERATE_KEY相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE') {
// 将与ITERATE_KEY相关联的副作用函数也添加到effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
// cleanup用来存储用户注册的过期回调
let cleanup
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn
}
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 当数据变化时,调用回调函数cb
// 将旧值和新值作为回调函数的参数
// 将onInvalidate作为回调函数的第三个参数,以便用户使用
cb(newValue, oldValue, onInvalidate)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
// 使用job函数作为调度器函数
scheduler: () => {
// 在调度函数中判断flush是否为post,如果是,将其放到微任务队列中执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
// 当immediate 为true的时候立即执行job,从而触发回调函数
job()
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
}
// 递归读取值
function traverse(value, seen = new Set()) {
// 如果要读取的是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === 'null' || seen.has(value)) {
return
}
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设value就是一个对象,使用 for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const key in value) {
traverse(value[key], seen)
}
// 暂时的,这个返回value无用
return value
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(() => console.log(obj.foo.bar))
obj.foo.bar = 2
</script>
</html>这样就够深了。
接着可以配置一个可以选择深浅响应的工厂函数,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
</body>
<script>
// 定义区
let activeEffect
const effectStack = []
const obj = reactive(
{
foo: {
bar: 1
}
}
)
const bucket = new WeakMap()
// 定义一个任务队列
const jobQueue = new Set()
// 使用Promise.resolve() 创建一个promise实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 用一个标志代表是否正在刷新队列
let isFlushing = false
const ITERATE_KEY = Symbol()
function flushJob() {
// 如果任务正在刷新,则什么都不做
if (isFlushing) return
// 设置为true,代表正在刷新
isFlushing = true
// 在微任务队列刷新jobQueue队列
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
// 结束后重置isFlushing
isFlushing = false
})
}
// 封装 createReactive函数,接受参数isShallow,代表是否为浅相应,默认为false,即非浅响应(深相应)
function createReactive(obj, isShallow = false) {
return new Proxy(obj, {
// 拦截读取操作,接受第三个参数receiver
get(target, key, receiver) {
// 在这里,判断receiver是不是target的代理对象:代理对象可以通过raw属性访问原始数据(这个raw是我们约定的)
if (key === 'raw') {
return target
}
track(target, key)
// 使用reflect.get返回读取到的属性值
// 当读取属性值时,直接返回了结果,这不能实现深相应。
// return Reflect.get(target, key, receiver)
// 得到原始值结果
const res = Reflect.get(target, key, receiver)
// 如果配置了浅响应,在这里直接返回原始值
if (isShallow) {
return res
}
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
has(target, key) {
track(target, key)
return Reflect.has(target, key)
},
ownKeys(target) {
// 将副作用函数与ITERATE_KEY 关联
track(target, ITERATE_KEY)
return Reflect.ownKeys(target)
},
set(target, key, newValue, receiver) {
// 拦截设置操作
// 先获取旧值
const oldValue = target[key]
// 如果属性不存在,则说明是添加新属性,否则时设置已有属性
const type = Object.prototype.hasOwnProperty.call(target, key) ? 'SET' : 'ADD'
// 设置属性值
const res = Reflect.set(target, key, newValue, receiver)
// target === receiver.raw 说明receiver就是target的代理对象
if (target === receiver.raw) {
// 比较新值和旧值,只要当不全等的时候才触发相应
if (oldValue !== newValue) {
// 将type作为第三个参数传递给trigger函数
// target[key] = newValue // 使用了Reflect.set设置,不必使用此句
trigger(target, key, type)
}
}
return res
},
deleteProperty(target, key) {
// 检查被操作的属性是否时对象自己的属性
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
// 使用Reflect.deleteProperty完成属性的删除
const res = Reflect.deleteProperty(target, key)
if (res && hadKey) {
// 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新
trigger(target, key, 'DELETE')
}
return res
}
})
}
function reactive(obj) {
return createReactive(obj)
}
function shallowReactive(obj) {
return createReactive(obj, true)
}
function track(target, key) {
// 没有副作用 直接返回
if (!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) bucket.set(target, depsMap = new Map())
// dep 预期是text1
let deps = depsMap.get(key)
if (!deps) depsMap.set(key, deps = new Set())
// 这样,修改某一属性就详细到代理的某一个对象的某个属性
deps.add(activeEffect)
// deps 就是一个与当前副作用函数存在联系的依赖集合
activeEffect.deps.push(deps)
}
function trigger(target, key, type) {
// 设置的时候 通知对应的修改函数
let depsMap = bucket.get(target)
if (!depsMap) return
// 取得与key相关联的副作用函数
let effects = depsMap.get(key)
// 取得与ITERATE_KEY相关联的副作用函数
const iterateEffects = depsMap.get(ITERATE_KEY)
const effectsToRun = new Set()
// 将与key相关联的副作用函数添加到effectsToRun
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
// 只有当操作类型为ADD 或者 DELETE时,才触发与ITERATE_KEY相关联的副作用函数重新执行
if (type === 'ADD' || type === 'DELETE') {
// 将与ITERATE_KEY相关联的副作用函数也添加到effectsToRun
iterateEffects && iterateEffects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
}
effectsToRun.forEach(item => {
// 如果一个副作用函数存在调度器,则调用该调度器,并将副作用函数作为参数传递
if (item.options.scheduler) {
item.options.scheduler(item.func)
} else {
item.func()
}
})
}
function effect(fn, options = {}) {
// 这个effectFn是副作用函数
const effectFn = {
func: () => {
cleanup(effectFn)
// 当调用effect注册副作用函数时,将副作用函数赋值给activeEffect
activeEffect = effectFn
// 在调用副作用函数之前将当前副作用函数压入栈中
effectStack.push(effectFn)
// 将fn的执行结果存储到res中
const res = fn()
// 在调用副作用函数之后,将当前副作用函数弹出栈,并把activeEffect还原成之前的值
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res
}
}
// 将options挂载到effectFn上
effectFn.options = options
// activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
effectFn.deps = []
if (!options.lazy) {
effectFn.func()
}
return effectFn.func
}
function computed(getter) {
// 使用value来缓存上一次计算的值
let value
// dirty标志,用来标识是否需要重新计算值,为true代表脏,需要重新计算
let dirty = true
// 把getter作为副作用函数,创建一个lazy的effect
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,在调度器中将dirty重置为true
scheduler() {
// 当计算属性依赖的响应式数据变化时,手动调用trigger函数触发相应
if (!dirty) {
dirty = true
trigger(obj, 'value')
}
}
})
const obj = {
// 当读取到value才执行effectFn
get value() {
if (dirty) {
value = effectFn()
// 将dirty设置为false,下一次访问直接使用缓存到value的值
dirty = false
}
// 当读取value时,手动调用track函数进行追踪
track(obj, 'value')
return value
}
}
return obj
}
// watch 函数接收两个参数,source是响应式数据,cb是回调函数,当监控到source数据变化时,cb执行
function watch(source, cb, options = {}) {
// 定义getter
let getter
// 如果source是函数,说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
getter = () => traverse(source)
}
// 定义旧值和新值
let oldValue, newValue
// 使用effect注册副作用函数时,开启lazy选项,并把返回值存储到effectFn中以便后续手动调用
// cleanup用来存储用户注册的过期回调
let cleanup
// 定义onInvalidate函数
function onInvalidate(fn) {
cleanup = fn
}
// 提取scheduler调度函数为一个独立的job函数
const job = () => {
// 在scheduler中重新执行副作用函数,得到的是新值
newValue = effectFn()
// 在调用回调函数cb之前,先调用过期回调
if (cleanup) {
cleanup()
}
// 当数据变化时,调用回调函数cb
// 将旧值和新值作为回调函数的参数
// 将onInvalidate作为回调函数的第三个参数,以便用户使用
cb(newValue, oldValue, onInvalidate)
// 更新旧值,不然下一次会得到错误的旧值
oldValue = newValue
}
const effectFn = effect(
// 触发读取操作,从而建立联系
() => getter(),
{
lazy: true,
// 使用job函数作为调度器函数
scheduler: () => {
// 在调度函数中判断flush是否为post,如果是,将其放到微任务队列中执行
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
}
}
)
if (options.immediate) {
// 当immediate 为true的时候立即执行job,从而触发回调函数
job()
} else {
// 手动调用副作用函数,拿到的值就是旧值
oldValue = effectFn()
}
}
// 递归读取值
function traverse(value, seen = new Set()) {
// 如果要读取的是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === 'null' || seen.has(value)) {
return
}
// 将数据添加到seen中,代表遍历地读取过了,避免循环引用的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设value就是一个对象,使用 for...in读取对象的每一个值,并递归地调用traverse进行处理
for (const key in value) {
traverse(value[key], seen)
}
// 暂时的,这个返回value无用
return value
}
function cleanup(effectFn) {
// 遍历 effectFn.deps数组
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
}
// 执行区
effect(() => console.log(obj.foo.bar))
obj.foo.bar = 2
</script>
</html>