vue3读书笔记之深入响应式系统
vue3发布已经很长一段时间了,最近学习vue原理,记录下来省的忘了,感谢祖师爷尤大赏饭
proxy
首先来看看聊烂了的proxy的使用:
var obj = {
a: 1
}
var newObj = new Proxy(obj, {
get(target, key) {
console.log(target[key])
return target[key]
},
set(target, key, val){
target[key] = val
return true
}
})
newObj.a = 2
newObj.b = 3
console.log(newObj.a) // 2
console.log(newObj.b) // 3
可以看到,在初始化的obj中并不存在b属性,但proxy也可以拦截,这是object.defineProperty不具备的,这也是vue3中不再需要$set的原因
响应式系统实现
在响应式系统实现之前,先来说说副作用函数effect,如果有写过react hook,那么对这个effect一定不会陌生,vue3中的effect与其类似,都是内部函数可以对外产生影响的函数。看如下代码:
var obj = {text: 'hello world'}
function effect(){
document.body.innerHtml = obj.text
}
暂时把obj看做一个响应式对象,那么我们改变obj.text,则会引起视图的改变,那么effect就是一个副作用函数。那么我们如何将obj包装成响应式呢?
- 当副作用函数effect执行,会发生obj.text的读取操作
- 修改obj.text的值,会发生obj.text的设置操作
那如果我们能拦截读取和设置操作,就变得可行了,当发生读取操作时,我们将整个副作用函数放进一个“桶”里,当发生设置操作,就将副作用从桶里取出并执行即可。
var obj = {text: 'hello world'}
var bucket = new Set()
var data = new Proxy(obj, {
get(target, key) {
bucket.add(effect)
return target[key]
},
set(target, key, val) {
target[key] = val
bucket.forEach(fn => fn())
return true
}
})
function effect() {
document.body.innerHTML = data.text
// console.log(data.text)
}
effect()
setTimeout(() => {
data.text = '响应式系统'
}, 3000);
运行如上代码,可以看到三秒后页面中的内容改变了,但目前还有很多问题,逐步修改。
effect函数去具名化
上边代码指定副作用函数叫做effect,但实际使用可能是别的函数,甚至是匿名函数,所以我们要去掉这种具名化命名方式。修改effect函数如下:
// 用一个全局变量存储被注册的effect
var activeEffect
// effect变成一个注册副作用函数的函数
function effect(fn) {
activeEffect = fn
fn()
}
var obj = {text: 'hello world'}
var bucket = new Set()
var data = new Proxy(obj, {
get(target, key) {
// 此处修改
activeEffect && bucket.add(activeEffect)
return target[key]
},
set(target, key, val) {
target[key] = val
bucket.forEach(fn => fn())
return true
}
})
effect(function effectFn(){
document.body.innerHTML = data.text
})
setTimeout(() => {
data.text = '响应式系统'
}, 3000);
目前我们的副作用函数已经可以不依赖固定的具名函数了,但因为是使用proxy,如果此时设置属性:data.newText = '我是新增的属性',还是会触发set操作,进而再执行一次effect,但其实这次的effect是没有必要的。究其原因,是因为我们没有把属性和effect对应起来。所以我们要重新设计一下bucket。
设计bucket
分析一下,我们有用的几个变量分别是:target,key,以及使用effect注册的函数effectFn,他们的几种关系如下:
- 一个属性对应一个副作用函数:
- 两个副作用函数同时读取一个属性:
effect(function effectFn1(){
data.text
})
effect(function effectFn2(){
data.text
})
- 多个属性对应一个副作用函数:
effect(function effectFn() {
return data.num1 + data.num2
})
- 不同副作用函数对应不同对象的不同属性:
effect(function effectFn1(){
data1.text
})
effect(function effectFn2(){
data2.name
})
接下来,我们重新设计桶结构。首先,我们的bucket需要使用weakMap,桶的key就是我们的对象target,桶的value是一个map,用来存放target[key],这个map的value是一个Set结构,用来存放这个key对应的effect函数,因为Set结构天然去重。那为什么我们的桶要用weakMap结构呢?这就涉及到weakMap的优点了:
与map不同的是,weakMap的key只能是object,由于weakMap是弱引用,所以当我们的target对象没有引用的时候,也就是用户侧不需要它的时候,垃圾回收器就可以自动清除它,而不需要手动清除。但如果使用Map代替WeakMap,即是target没有引用,垃圾回收器也不会进行回收,最终会导致内存溢出。
尝试实现这个桶结构如下:
const data = new Proxy(obj, {
get(target, key) {
if(!activeEffect) return
// 得到target的map结构
let depsMap = bucket.get(target)
// 如果这个map不存在,就向桶中加入当前target
!depsMap && bucket.set(target, (depsMap = new Map()))
// 获取到map中的当前key的依赖集合
let deps = depsMap.get(key)
// 如果不存在就新创建一个Set集合,用来装effect函数
!deps && depsMap.set(key, (deps = new Set()))
// 将当前注册的effect加入
deps.add(activeEffect)
return target[key]
},
set(target, key, val) {
target[key] = val
// 获取到当前对象的map
let depsMap = bucket.get(target)
if(!depsMap) return
// 根据key属性值获取到map中的所有当前属性值关联的effect函数
let effects = depsMap.get(key)
// 遍历所有effect执行之
effects && effects.forEach(effect => effect());
return true
}
})
现在这里已经可以实现只对当前effect函数内依赖变量改变的时候执行副作用函数了,我们在把get和set中的部分抽离出来,分别用track和trigger封装一下:
const data = new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, val) {
target[key] = val
trigger(target, key)
return true
}
})
function track(target, key) {
if(!activeEffect) return
let depsMap = bucket.get(target)
!depsMap && bucket.set(target, (depsMap = new Map()))
let deps = depsMap.get(key)
!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(effect => effect());
}
分支切换与cleanup
首先明确一下分支切换的概念,思考如下代码:
const obj = {ok: true, text: 'hello world'}
const data = new Proxy(obj, {/****/})
effect(function effectFn() {
document.body.innerHTML = data.ok ? data.text : 'not'
})
经过分析,我们可以建立键值及effect依赖关系如下:
分析一下:当data.ok变为false后,document.body.innerHtml的值变为‘not’,如果此时修改key2(data.text),由于这个data.text会触发Proxy中的set操作,所以会将key2所依赖的副作用都遍历执行一遍,由于effectF依然在depsMap中,那么effectFn就会执行,但我们此时并不需要它去执行,因为页面中由于data.ok的值为false,我们并不关心data.text的值,所以我们需要想办法把这次的副作用给干掉。
解决的思路就是:每次副作用函数执行时,我们先把它从所有与之关联的依赖集合中删除,然后在执行完毕副作用函数的过程中会重新依赖,那么此时我们当前的属性如果不再需要这个effect,就不会将他收集起来,以此达到我们去掉多余副作用的目的。因此我们需要重新设计effect函数。如下面代码所示:在effect内部定义一个新的effectFn函数,为其添加effectFn.deps属性,改属性是一个数组,用来存储所有包含当前副作用函数的依赖集合。
let activeEffect
function effect(fn) {
const effectFn = () => {
// 当effctFn执行时,将本effectFn设置为当前激活的activeEffect
activeEffect = fn
fn()
}
// activeEffect.deps用来存储所有与该副作用函数相关的依赖集合
effectFn.deps = []
effectFn()
}
那effectFn.deps数组中的依赖集合是如何收集的呢?其实是在track函数中:
function track(target, key) {
if(!activeEffect) return
let depsMap = bucket.get(target)
!depsMap && bucket.set(target, (depsMap = new Map()))
let deps = depsMap.get(key)
!deps && depsMap.set(key, (deps = new Set()))
deps.add(activeEffect)
// 将依赖添加到activeEffect.deps数组中
activeEffect.deps.push(deps)
}
如此我们便完成了对依赖集合的收集工作,每一个副作用都有一个其依赖的集合。有了这个联系后,我们就可以在每次副作用函数执行时,根据effectFn.deps获取所有相关联的依赖集合,进而将副作用函数从依赖集合中移除:
let activeEffect
function effect(fn) {
const effecFn = () => {
// 调用cleanup函数完成清除
cleanup(effecFn)
activeEffect = effecFn
// 调用副作用函数并重新收集依赖
fn()
}
effecFn.deps = []
effecFn()
}
function cleanup(fn) {
for(let i = 0; i < fn.deps.length; i++){
const deps = effecFn.deps[i]
// 将传入的effecFn从依赖集合中移除
deps.delete(fn)
}
// 重置effectFn.deps数组
effecFn.deps.length = 0
}
如此我们便可以避免副作用遗留问题了,但其实运行代码,会发现无限循环执行,问题产生在trigger函数中:
function trigger(target, key) {
let depsMap = bucket.get(target)
if(!depsMap) return
let effects = depsMap.get(key)
effects && effects.forEach(effect => effect()); // 问题在这儿
}
在trigger函数内部,我们遍历effects集合,他是一个Set集合,里面存储着副作用函数,当副作用函数执行时,会调用cleanup函数清除,就是从effects集合中将当前执行的副作用函数剔除,但是副作用函数的执行会导致其重新被收集到集合中,此时对effects集合的遍历仍在执行
上述行为相当于如下代码:
const set = new Set([1])
set.forEach(() => {
set.delete(1)
set.add(1)
console.log('运行中')
})
解决方法可以创建另一个Set集合遍历它:
const set = new Set([1])
const newSet = new Set(set)
newSet.forEach(() => {
set.delete(1)
set.add(1)
console.log('运行中')
})
回到trigger函数:
function trigger(target, key) {
let depsMap = bucket.get(target)
if(!depsMap) return
let effects = depsMap.get(key)
const effectToRun = new Set(effects)
effectToRun && effectToRun.forEach(effect => effect());
}
至此分支切换可以完整实现。
嵌套的effect与effect栈
effect是可以发生嵌套的,vue模板的渲染就是在effect中执行的,所以我们的effect需要支持嵌套。思考如下代码:
var obj = {
foo: '1',
bar: '2'
}
const data = new Proxy(obj, {/****/})
let temp1,temp2
effect(function effectFn1() {
console.log('我是fn1')
effecFn(function effectFn2() {
console.log('我是fn2')
temp2 = data.bar
})
temp1 = data.foo
})
执行如上代码并修改data.foo,会发现控制台输出:
// 我是fn1
// 我是fn2
// 我是fn2
分析:首先执行effectFn1,执行effectFn1会间接执行effectFn2,所以前两次输出是没有问题的,有问题的是第三个输出,我们修改data.foo,本来应该触发effectFn1的执行,但现在缺触发了effectFn2的执行,effectFn1反而没有执行,这显然不符合逻辑。那造成这个问题的原因是什么呢?我们看前边的effect实现,每一次都需要activeEffect = effecFn,那么在上边嵌套执行effectFn2的时候,activeEffect也就变成了effectFn2,当我们需要在改变data.foo后执行effectFn1的时候,activeEffect保存的确是effectFn2函数,所以此时执行了一个错误的副作用函数,导致输出不是我们期望的结果。
为了解决这个问题,我们需要一个副作用函数栈effectStack,在副作用函数执行的时候将该副作用函数压入栈中,当副作用函数执行完毕后将此副作用函数弹出函数栈,并且activeEffect永远指向栈顶元素。 代码如下:
let activeEffect
let effectStack = [] // 新增
function effect(fn) {
const effecFn = () => {
cleanup(effecFn)
// 调用effect函数注册副作用函数时,将 effecFn 赋值给 activeEffect
activeEffect = effecFn
// 副作用函数入栈
effectStack.push(effecFn) // 新增
fn()
// 执行完毕弹栈
effectStack.pop() // 新增
// 重新将栈顶元素赋值给 activeEffect
activeEffect = effectStack[effectStack.length-1] // 新增
}
effecFn.deps = []
effecFn()
}
如此一来,当执行effectFn1时,effectFn1入栈,接下来effectFn2入栈,effectFn2执行完毕并出栈,activeEffect的值将重新交给effectFn1。
避免无限递归循环
思考如下代码:
var obj = {
num: 1
}
const data = new Proxy(obj, {/****/})
effect(() => data.num++)
如上代码,最后一行相当于是data.num = data.num + 1,在一个副作用函数中读取和设置,先触发track函数,将该副作用函数保存到桶中,然后在将data.num加一并赋值给data.num,此时触发trigger操作,即把桶中的副作用函数取出执行该副作用函数。但此时的副作用函数正在执行中没有执行完毕,又要开始下一次执行,这样就会无限递归,造成栈溢出。
要解决这个问题,我们先分析一下:这种现象产生在tracK和trigger都在同一个副作用函数的情况下,要执行的副作用函数都是当前的activeEffect。那么我们在set操作中加一个拦截,也就是当发现trigger触发的副作用函数与当前的activeEffect相同,则不触发trigger的执行。代码如下:
function trigger(target, key) {
let depsMap = bucket.get(target)
if(!depsMap) return
let effects = depsMap.get(key)
const effectToRun = new Set()
effects.forEach(effect => {
if(effect !== activeEffect) effectToRun.add(effect)
})
effectToRun && effectToRun.forEach(effect => effect());
}
调度执行
可调度性是响应式系统非常重要的特性。所谓的可调度,也就是可以随着用户的意愿,在trigger触发副作用函数执行时,可以自定义副作用函数执行的时机、顺序、次数及执行方式。 首先看如下代码:
var obj = {
num: 1
}
const data = new Proxy(obj, {/****/})
effect(() => console.log(data.num))
data.num++
console.log('结束了')
很容易得出运行结果如下:
// 1
// 2
// 结束了
那如果有个需求,想让其输出结果变为:1、结束了、2,要怎么办呢?这个时候就需要响应是系统支持调度了。
我们可以为effect函数设计一个选项参数options,允许用户指定调度器:
effect(
() => console.log(data.num),
// options
{
scheduler(fn) {
// ...
}
}
)
如上代码所示: 用户调用effect注册副作用函数时,可以传入一个对象,用以指定scheduler函数,同时在函数内部我们要把options挂载到对应的副作用函数上。
function effect(fn, options = {}) {
const effecFn = () => {
cleanup(effecFn)
activeEffect = effecFn
effectStack.push(effecFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
}
// 将options挂载到副作用函数上
effecFn.options = options // 新增
effecFn.deps = []
effecFn()
}
有了调度函数,我们在trigger函数中触发副作用函数重新执行时,就可以直接调用用户传递的调度器函数,把控制权交给用户
修改trigger函数如下:
function trigger(target, key) {
let depsMap = bucket.get(target)
if(!depsMap) return
let effects = depsMap.get(key)
const effectToRun = new Set()
effects.forEach(effect => {
if(effect !== activeEffect) effectToRun.add(effect)
})
effectToRun && effectToRun.forEach(effect => {
// 如果存在option.scheduler,则将副作用函数作为参数传递
if(effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect()
}
});
}
有了这个调度器,我们就可以实现上边的需求了,代码如下:
var obj = {
num: 1
}
const data = new Proxy(obj, {/****/})
effect(
() => console.log(data.num),
{
// 注意这个fn即为第一个参数,即副作用函数
scheduler(fn) {
setTimeout(() => {
fn()
}, 0)
}
}
)
effect(() => console.log(data.num))
data.num++
console.log('结束了')
以上是实现了改变副作用函数的执行顺序,通过调度器还可以做到控制它的执行次数,这一点也很重要。
思考如下代码:
var obj = {
num: 1
}
const data = new Proxy(obj, {/****/})
effect(() => console.log(data.num))
data.num++
data.num++
代码依次输出:1,2,3。我们的第一次输出1是有必要的,当执行到第一个自加操作时,由于整段代码同属一个任务队列,后续还有data.num的set操作,就是说这次自加操作只是个过渡状态,那么此次的副作用执行也就不是必要的了,我们只想在此次任务队列中的任务都结束后统一更新data.num就好了。
基于调度器,我们可以实现上述功能,代码如下:
var obj = {
num: 1
}
const data = new Proxy(obj, {/****/})
// 注册一个任务集合,并利用其去重能力
const jobQueue = new Set()
const p = Promise.resolve()
// 一个标志,代表是否正在刷新队列
let isFlushing = false
function flushJob() {
// 如果当前正在刷新队列,代表此次之前就有当前值的设置操作等待执行,
// 所以本次及后续的设置操作的副作用函数都不需要执行了
if(isFlushing) return
// 第一次执行就将标志锁定
isFlushing = true
// 当此次任务队列中任务都执行完毕,才统一执行放在微任务队列中的副作用函数
p.then(() => {
jobQueue && jobQueue.forEach(effect => effect())
}).finally(() => isFlushing = false)
}
effect(
() => console.log(data.num),
{
scheduler(fn) {
// 每次trigger都将effectFn放入jobQueue中
jobQueue.add(fn)
flushJob()
}
}
)
data.num++
data.num++
分析:上述代码中,我们首先定义了jobQueue任务集合,利用其自动去重能力,使得我们每次自加操作向jobQueue中添加副作用函数最终只得到一个副作用函数。当连续改变data.num的值触发trigger操作时,我们不断同步连续地执行effect.options.scheduler函数,但由于flushJob函数通过isFlushing机制,我们只能在一次周期内执行一次flushJob函数。当同步任务执行完毕后,js引擎开始执行微任务队列中的jobQueue循环遍历调用操作,并在微任务执行完毕后,将isFlushing重新置为false。
这个功能很类似vue中连续多次修改响应式数据但只会触发一次更新,实际上vue内部实现了一个更完善的调度器
computed与lazy
深入computed之前,先来看看懒执行的effect,即lazy的effect。举个例子:现在我们实现的effect函数会立即执行传递给它的副作用函数。但有些场景下,我们希望需要他的时候才执行,而不是立即执行,也就是计算属性。这时我们可以通过在options选项中添加lazy来实现。如下代码:
effect(() => console.log(data.num), {lazy: true})
那么我们可以修改之前的effect函数来实现,当options.lazy的值为true时,不立即执行这个副作用函数:
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
}
// 将options挂载到副作用函数上
effecFn.options = options
effecFn.deps = []
if(!options.lazy){ // 新增
effectFn()
}
return effectFn // 新增
}
如此,我们可以实现通过传lazy选项控制副作用函数懒执行。在effect函数执行后,我们可以拿到对应的副作用函数,此时可以在手动调用之,如下:
const myComputed = effect(function effectFn(){console.log(data.num)}, {lazy:true})
myComputed()
但其实这样的意义不大,联想我们computed使用场景,如果我们把传递给effect的函数看做一个getter,这个getter可以返回任何值,如:
cosnt effectFn = effect(
() => data.num1 + data.num2,
{lazy: true}
)
这样我们在手动执行副作用函数时,就能拿到其返回值:
cosnt effectFn = effect(
() => data.num1+data.num2,
{lazy: true}
)
// value是getter的返回值
const value = effectFn()
为了实现这个目标,修改effect如下:
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
const res = fn() // 新增
effectStack.pop()
activeEffect = effectStack[effectStack.length-1]
return res // 新增
}
// 将options挂载到副作用函数上
effecFn.options = options
effecFn.deps = []
if(!options.lazy){
effectFn()
}
return effectFn
}
好了,现在该实现我们的computed函数了:
function computed(getter) {
// 获取到懒执行的effectFn
const effectFn = effect(getter, {lazy: true})
const obj = {
// 只在真正读取computed变量的时候才执行副作用函数
get value() {
return effectFn()
}
}
return obj
}
const computedValue = computed(() => data.num1 + data.num2)
console.log(computedValue)
console.log(computedValue)
console.log(computedValue)
在computed内部返回一个obj,它拥有一个访问器属性,只有读取value的值时才返回副作用函数的执行结果。
但当前的computed还有缺陷,还没有做到缓存值的功能。也就是说我们访问多次computedValue,会导致多次执行副作用函数。而我们希望在data.num1和data.num2没有变化的时候不执行副作用函数,即对其添加缓存机制。
优化后的代码如下:
function computed(getter) {
// 缓存上一次的值
let value
// 是否是脏的标志,如果为脏,则需要重新计算
let dirty = true
const effectFn = effect(getter, {
lazy: true,
// 添加调度器,只有当fn中依赖的值发生变化的时候才会触发scheduler的执行
scheduler() {
dirty = true
}
})
const obj = {
// 只在真正读取computed变量的时候才执行副作用函数
get value() {
if(dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
我们为effectFn添加scheduler,他会在getter函数中依赖的响应式数据改变时执行。这样我们将dirty置为true,下一次访问computedValue时,就会重新调用effectFn计算新值。
watch的实现
watch就是观测一个响应式数据,当数据改变时,执行其第二个参数回调。事实上,watch还是利用了effect及options.scheduler选项。
我们知道,在一个副作用函数中访问响应式数据,会在响应式数据属性和副作用函数之间构建联系。当修改响应式数据后,就会触发副作用函数的更新。但如果传入了options.scheduler就会跳过这个副作用函数,执行options.scheduler。那么我们可以来实现一个简单的watch如下:
function watch(data, cb) {
effect(
// 触发读取操作,与属性建立联系
() => console.log(data.num),
{
scheduler() {
cb()
}
}
)
}
var obj = {
num: 1
}
const data = new Proxy(obj, {/****/})
watch(data, () => console.log('数据改变了'))
data.num++
执行上边代码,可以发现触发watch函数了。但我们的硬编码只能观测到data.num改变。为了让其有通用性,需要封装一个通用的读取操作:
function watch(source, cb) {
effect(
// 触发读取操作,与属性建立联系
() => tranverse(source),
{
scheduler() {
cb()
}
}
)
}
function tranverse(value, seen = new Set()) {
// 如果要读取的数据是原始数据类型或已经读过,则什么都不做
if(typeof value !== 'object' || typeof value === null || seen.has(value)) return
seen.add(value)
// 暂时不考虑数组等结构,假设value就是对象,使用for...in...读取对象的每一个值,递归调用tranverse处理
for(let key in value) {
tranverse(value[key], seen)
}
return value
}
那么watch的第一个参数除了响应式数据,还可以传入一个getter。在getter内部可以指定依赖哪些响应式数据,当这些数据变化时,才会触发回调函数执行。如下边代码:
watch(
() => data.num,
() => console.log('num变了')
)
修改代码如下:
function watch(source, cb) {
let getter
if(typeof source === 'function') {
getter = source
} else {
getter = () => tranverse(source)
}
effect(
// 执行getter
() => getter(),
{
scheduler() {
cb()
}
}
)
}
目前已经实现了大部分watch的能力,但还有一个比较重要的没有实现,那就是在回调中我们可以拿到旧的value和新的value。如:
watch(
() => data.num,
(newVal, oldVal) => console.log(newVal, oldVal)
)
data.num++
那如何获取新老值呢?还是lazy!
function watch(source, cb) {
let getter
if(typeof source === 'function') {
getter = source
} else {
getter = () => tranverse(source)
}
let oldVal, newVal
const effectFn = effect(
// 执行getter
() => getter(),
{
lazy: true,
scheduler() {
// 在scheduler中重新执行的副作用函数,得到的是新值
newVal = effectFn()
cb(newVal, oldVal)
// 将当前新值更新为下一次的旧值
oldVal = newVal
}
}
)
// 手动调用副作用函数,拿到的是旧值
oldVal = effectFn()
}
这段代码最核心的是lazy选项创建了一个懒执行的effect,注意最下边的,我们手动调用effectFn得到的返回值是旧值,即第一次执行得到的值。当变化发生触发scheduler调度函数执行时,会重新调用effectFn函数得到新值,将他们作为参数传给回调函数cb就可以了。还有要将当前新值赋给旧值,这样下一次旧值才是正确的。
立即执行的watch
默认情况下,回调函数只有在后续响应式数据变化才会执行。但我们可以通过immediate选项指定回调是否立即执行。仔细思考,回调函数立即执行与后续执行本质上并无差别。我们可以把scheduler封装成一个通用函数,分别在初始化和变化时执行它。如下:
function watch(source, cb, options = {}) {
let getter
if(typeof source === 'function') {
getter = source
} else {
getter = () => tranverse(source)
}
let oldVal, newVal
const job = () => {
newVal = effectFn()
cb(newVal, oldVal)
oldVal = newVal
}
const effectFn = effect(
// 执行getter
() => getter(),
{
lazy: true,
scheduler: job
}
)
if(options.immediate) {
// 立即执行job,触发回调函数执行
job()
}else{
// 手动调用副作用函数,拿到的是旧值
oldVal = effectFn()
}
}
至此,响应式系统告一段落,感谢尤大,感谢霍春阳的《vue.js设计与实现》。