Vue源码解读 深入解读Proxy vue3响应式原理
本文中的代码已经同步至GitHub,大家可自行阅读实验
1.1前言(副作用函数)
我们都知道,vue3中的响应式是通过proxy的get和set劫持对象属性变化来完成的。 在get阶段,会收集代码中所有与代理对象有依赖的函数到一个set容器中,see4ee54rt阶段会遍历执行所有的函数,我们称这些函数为副作用函数
而vue就是在副作用函数中完成对视图更新的操作的
(如果还不了解proxy,可以下去自行了解)
伪代码:
const obj = new Proxy(data, {
get(target, key) {
//收集副作用函数到容器中
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
//执行容器中的副作用函数
return true
}
})
副作用函数be like:
function effect() {
document.querySelector('.myContainer').innerHTML = data.text
}
我们可以先把副作用函数抽象的理解成修改视图层的函数,例如你在vue模板中使用了一个变量,那么vue就会把这个代理对象的副作用函数收集到一个桶中,等到这个对象的属性变化的时候,副作用函数就会执行,视图层被修改,始终和我们的最新的数据统一。
示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="myContainer"></div>
<button>click to change div</button>
<script>
const bucket = new Set()
const data = { text: 'hello world' }
const obj = new Proxy(data, {
get(target, key) {
bucket.add(effect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
function effect() {
document.querySelector('.myContainer').innerHTML = data.text
}
effect()
</script>
<script>
document.querySelector('button').onclick = function () {
obj.text += 'h'
}
</script>
</body>
</html>
如上图代码所示,当我们点击按钮时,修改代理对象的值,页面也随之更新
1.2完善我们的响应式系统
从上文中我们可以总结出一个响应式系统的工作流程
- 当读取操作发生时,将副作用函数收集到桶中
- 当设置操作发生时,从桶中取出副作用函数并执行
但是我们可以发现一些问题,上文中我们设计的响应式系统比较简单,我们把副作用函数的名字写死了(effect),如果副作用函数的名字不叫effect,那么代码就不能够正常工作,为了解决这个问题我们需要提供一个用来注册副作用函数的机制
代码如下:
//用全局变量存储被注册的副作用函数
let activeEffect
//effect用来注册副作用函数
function effect(fn) {
//调用时将fn赋值给activeEffect
activeEffect = fn
//执行副作用函数
fn()
}
我们可以这样使用effect函数
effect(
//匿名的副作用函数
() => {
document.querySelector('.myContainer').innerHTML = obj.text
}
)
然后把proxy get里添加的effect改成我们全局副作用函数注册变量
old: buckect.add(effect)
new: buckect.add(activeEffect)
这样我们就解决了副作用函数硬编码的问题,也就是不把副作用函数的名字写死,只要使用effect进行注册,那么proxy就可以拿到副作用函数
1.2.1 增加不存在的属性所出现的问题
如果我们对这个响应式系统再进行测试,例如为响应式数据上设置一个不存在的属性
effect(
//匿名的副作用函数
() => {
console.log('effect run')//会打印两次
document.querySelector('.myContainer').innerHTML = obj.text
}
)
setTimeout(() => {
obj.newPro = 'hello vue3'
}, 1000);
如上代码所示,我们一秒钟后为对象添加一个新属性newpro, 理论上,我们副作用函数中只绑定了text一个属性,所以我们预期是打印内容只执行一次,可是实际结果却执行了两次
为什么会这样呢?因为我们没有在副作用函数与被操作字段之间建立一个明确的联系,导致在proxy 的 get中,只要读取任意属性,都会把副作用函数收集到桶中,set中,无论什么属性变化,也都会把桶中的副作用函数全部拿出来执行。因此为了解决这个问题,我们需要重新设计桶的数据结构,不能简单的使用set数据结构来作为桶了。
那么如何设计新的桶的数据结构呢?
我们先仔细观察下面的代码
effect(
function effectFn {
document.querySelector('.myContainer').innerHTML = obj.text
}
)
在这段代码中存在三个角色:
- 被读取属性的代理对象obj
- 被读取的字段名text
- 使用effect函数注册的副作用函数effectFn
如果使用target来表示一个代理对象的原始对象,用key来表示被读取的字段名,用effectFn来表示被注册的副作用函数,那么我们可以为这三个角色简历如下关系
可以看到这是一个树形结构
如果有两个副作用函数同时读取同一个对象的属性值:
那么关系如下
如果有一个副作用函数同时读取了一个对象的两个属性
那么关系如下:
如果在不同的副作用函数中读取了两个对象的不同属性
那么关系如下:
总之,这就是一个树形结构,有了这个关系,我们就可以解决上文中的问题,依照这个关系,如果我们设置obj2.text2的值,就只会导致effectFn2函数重新执行,并不会导致effectFn1函数重新执行。
修改后的代码如下图所示: 大家仔细看注释就好了,注释写的很清楚
//修改桶的数据结构 为weakmap //如果有对weakmap还不熟悉的小伙伴自己下去了解
const bucket = new WeakMap()
const data = { text: 'hello world' }
const obj = new Proxy(data, {
//对get和set进行修改
get(target, key) {
//如果没有activeEffect 直接return
if (!activeEffect) return target[key]
// 根据target从桶中取得depsMap,它是一个map类型:key----> effects
let depsMap = bucket.get(target)
//如果不存在depsMap,那么新建一个map与target关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
//再根据key 从depsMap中取得deps,它是一个set类型
//里面存储着所有与当前key相关联的副作用函数,effects
let deps = depsMap.get(key)
// 如果key不存在,同样新建立一个set并与key关联
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
//最后将当前激活的副作用函数添加到桶里
deps.add(activeEffect)
//返回属性值
return target[key]
},
set(target, key, newVal) {
//设置属性值
target[key] = newVal
//根据target从桶中取得depsMap,它是key---->effects
const depsMap = bucket.get(target)
if (!depsMap) return
//根据key取得的所有副作用函数effects
const effects = depsMap.get(key)
//执行副作用函数
effects && effects.forEach(fn => fn())
}
})
从这段代码我们可以看出来,我们分别使用了WeakMap,Map,Set 其中:
- WeakMap 由 target --> Map 构成
- Map 由 key --> Set 构成
他们之间的关系如下图所示:
经过我们的完善,现在只有对代理对象中与副作用函数有依赖的属性操作时,副作用函数才会执行。
最后我们对get 和 set 中的代码做一些封装处理
const obj = new Proxy(data, {
//对get和set进行修改
get(target, key) {
//将副作用函数添加到桶中
track(target, key)
//返回属性值
return target[key]
},
set(target, key, newVal) {
//设置属性值
target[key] = newVal
//将副作用函数从桶中拿出来执行
trigger(target, key)
}
})
//将副作用函数添加到桶中
function track(target, key) {
if (!activeEffect) return target[key]
// 根据target从桶中取得depsMap,它是一个map类型:key----> effects
let depsMap = bucket.get(target)
//如果不存在depsMap,那么新建一个map与target关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
//再根据key 从depsMap中取得deps,它是一个set类型
//里面存储着所有与当前key相关联的副作用函数,effects
let deps = depsMap.get(key)
// 如果key不存在,同样新建立一个set并与key关联
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
//最后将当前激活的副作用函数添加到桶里
deps.add(activeEffect)
}
//将副作用函数从桶中拿出来执行
function trigger(target, key) {
//根据target从桶中取得depsMap,它是key---->effects
const depsMap = bucket.get(target)
if (!depsMap) return
//根据key取得的所有副作用函数effects
const effects = depsMap.get(key)
//执行副作用函数
effects && effects.forEach(fn => fn())
}
好啦,现在我们的响应式系统就已经有模有样啦
1.3分支切换与cleanup
1.3.1什么是分支切换
如下代码所示:
effect(function effectFn(){
documnet.querySelector('.myContainer').innerHTML = obj.ok?obj.text:'not'
})
在副作用函数中,根据字段obj.ok字段的不同,会执行不同的代码分支,这就是分支切换。
分支切换有可能产生遗留的副作用函数,拿上面的代码举例,若obj.ok的初始值是true,那么副作用函数首先会和ok和text两个属性建立联系。
当obj.ok变为false时,代表obj.text这一代码分支不会执行,讲道理这个时候副作用函数应该与obj.text断开联系,也就是说obj.ok为false时,obj.text值的变化不应该导致副作用函数的重新执行。
但是我们的程序目前还做不到这一点。
下面尝试来解决这个问题。
解决这个问题的思路很简单,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。
也就是说,每次副作用函数执行,先把该副作用函数从所有包含它的set集合(key--->Map)中删除掉,然后副作用函数的执行会重新收集依赖,这样就不会出现遗留的副作用函数依然留在“桶”中。
要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确的知道哪些依赖集合中包含它,因此我们需要重新设计副作用函数,在副作用函数内部定义新的effectFn函数,并为其添加effectFn.dep属性,该属性是一个数组,用来存储所有包含当前副作用函数的依赖集合。
重新设计后的副作用函数:
function effect(fn) {
const effectFn = () => {
//当effetFn执行时,将其设置为当前激活的副作用函数
activeEffect = effectFn
fn()
}
//deps用来存储所有与该副作用函数相关联的依赖
effectFn.deps = []
//执行副作用函数
effectFn()
}
下一步我们要修改track函数,对effectFn.deps数组中的依赖集合进行收集。
修改后的track函数,其实只多了一行代码:
function track(target, key) {
if (!activeEffect) return target[key]
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect)
//新增
activeEffect.deps.push(deps)
}
下一步实现cleanup函数,它的作用是把副作用函数从依赖集合中删除 实现的思路其实很简单,就是遍历effectFn.dep数组,把副作用函数从依赖集合中删除
function cleanup(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i]
//从依赖集合中删除副作用函数
deps.delete(effectFn)
}
//最后重置dep数组
effectFn.deps.length = 0
}
最后再一次修改effect函数,调用cleanup函数完成清除工作
function effect(fn) {
const effectFn = () => {
//当effetFn执行时,将其设置为当前激活的副作用函数
cleanup(effectFn)
activeEffect = effectFn
fn()
}
//deps用来存储所有与该副作用函数相关联的依赖
effectFn.deps = []
//执行副作用函数
effectFn()
}
但是此时我们会发现目前的代码会导致副作用函数的无限执行
问题出在trigger函数的这行代码上:
effects && effects.forEach(fn => fn())
这行代码遍历effect集合,它是一个set数据结构,里面存储着副作用函数,当副作用函数执行时,会调用cleanup进行清楚,但是副作用函数的执行会导致其重新被收集到集合中。
造成这个现象的原因是:js语言规范中说明,当通过forEach遍历set集合时,如果一个值被访问过但是又被重新添加到集合中,如果此时遍历还没有结束,那么该值会被重新访问。
const set = new Set([1])
set.forEach(item =>{
set.delete(1)
set.add(1)
console.log('遍历中')
})
上面这段代码就会无限次的执行。
解决方法也很简单,我们可以构造另外一个set集合并且遍历它
修改后的trigger函数如下:
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set(effects) //新增
effectsToRun.forEach(effectFn => effectFn()) //新增
// effects && effects.forEach(fn => fn())
}
这样,我们新构建了一个effectsToRun集合并遍历他,代替直接遍历effects集合,从而避免了无限执行。
1.4当副作用函数发生嵌套
1.4.1前言
我们经常在vue中使用render渲染函数
实际上render就是在一个effect中执行的
//Foo组件
const Foo = {
render(){
return ....
}
}
上面的这段代码等同于:
effect(()=>{
Foo.render()
})
那么我们可以想象这样一种场景:当我们的组件发生嵌套时,例如Foo组件渲染了Bar组件
// Bar 组件
const Bar = {
render() { /* ... */ },
}
// Foo 组件渲染了 Bar 组件
const Foo = {
render() {
return <Bar /> // jsx 语法
}
}
那么这个时候就会发生effec嵌套,它相当于:
effect(() => {
Foo.render()
// 嵌套
effect(() => {
Bar.render()
})
})
因此我们的响应式系统要设计成可嵌套的,显然,我们目前的响应式系统并不支持这个功能。
1.4.2继续完善我们的响应式系统
我们用以下的数据测试一下当发生嵌套会发生什么:
//原始数据
const data = { foo: true, bar: true }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
let temp1, temp2
effect(function fn1() {
console.log('fn1 执行')
effect(function fn2() {
console.log('fn2 执行')
temp2 = obj.bar
})
temp1 = obj.foo
})
我们对这段代码的预期是,foo与副作用函数fn1建立联系,bar与副作用函数fn2建立联系,当foo变化时会执行fn1,fn2(为什么会执行fn2?因为fn2在fn1函数的内部,所以fn1执行时会顺带执行fn2),bar变化时会执行fn2
但是实际情况却是foo和bar的变化都只会导致fn2的执行
这是为什么呢?其实就出在我们实现的effect函数与activeEffect上。
let activeEffect
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
观察上面的代码,我们发现activeEffect是我们用来全局注册存储effect函数的变量,它当然只能存储一个值。再结合我们上面的代码来看,当副作用函数发生嵌套时,还没有等foo的get完成函数依赖注册时,activeEffect的值就已经被覆盖成fn2了,因此foo和bar的值改变都只会导致fn2的执行。
为了解决这个问题,我们需要一个副作用函数栈effectStack,在副作用函数执行时,我们将当前副作用函数压入栈中,等到副作用函数执行完后将其从栈中弹出,并让activeEffect始终指向栈顶的副作用函数。这样就能解决上面的问题了。
let activeEffect
//effect栈
const effectStack = []
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
//入栈
effectStack.push(effectFn)
fn()
//出栈
effectStack.pop()
//指向栈顶
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
这样的话,响应式数据就只会收集直接读取其值的副作用函数作为依赖,从而避免发生错乱
可以看到现在程序执行的结果已经符合我们的预期了
1.5避免无限递归循环
目前我们的响应式系统已经很完善了,但是依然有一些小问题:
我们在副作用函数中对代理对象的属性进行自增运算
const data = { foo: 1 }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
let temp1, temp2
effect(function fn1() {
obj.foo++
})
可以发现我们的浏览器报错了:大概意思就是说递归太多导致了栈溢出
为什么会出现这种情况呢?
obj.foo++ 可以等价为 obj.foo = obj.foo + 1
我们可以尝试分析一下代码的流程:首先读取foo的值,这个操作会被proxy的get捕获到,执行track函数,紧接着对obj.foo的赋值又会被proxy的set捕获到,执行trigger函数,而在trigger函数中会执行副作用函数,也就是会调用当前的函数(自己调用自己,递归),问题是当前副作用函数还没有执行完毕,这样会无限的调用自己,于是就导致了栈溢出。
解决这个问题不难,我们可以在trigger函数中增加一个判断条件,如果trigger触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
//新增
effects && effects.forEach(effectFn => {
//如果触发执行的副作用函数与当前执行的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => effectFn())
}
这样,我们就解决了无限递归的问题
1.6调度执行
1.6.1什么是可调度性
可调度性是响应式系统中非常重要的特性,指的是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机,次数,和方式。
假如现在我们有如下代码:
输出结果是:1 2 结束了 如果用户需求有变,用户期望输出结果变成:1 结束了 2,那这个时候我们要怎么做呢?
1.6.2实现可调度性
我们可以为effect函数设计一个选项参数options,允许用户指定调度器:
effect(
function fn1() {
console.log(obj.foo)
},
//options
{
//调度器scheduler是一个函数
scheduler(fn) {
}
}
)
如上所示,用户在调用effect函数注册副作用函数时,可以传递第二个参数options,它是一个对象,其中允许指定scheduler调度函数,同时在effect函数内部,我们需要把options选项挂载到对应的副作用函数上:
function effect(fn,options) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
//新增,将options挂载到effectFn
effectFn.options = options
effectFn.deps = []
effectFn()
}
有了调度函数,我们在trigger函数中触发副作用函数执行时,就可以直接调用用户传递的调度器函数,从而把控制权交给用户
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(effectFn => {
//新增
if (effectFn.options.scheduler) {//如果用户指定了调度器
effectFn.options.scheduler(effectFn)//使用调度器执行副作用函数
} else {
effectFn()
}
})
}
这样我们就实现了一个调度器,把副作用函数执行的控制权交给用户,实现了上面的需求,输出1 结束了 2
const data = { foo: 1 }
const obj = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
effect(
function fn1() {
console.log(obj.foo)
},
//options
{
//调度器scheduler是一个函数
scheduler(fn) {
setTimeout(fn)
}
}
)
obj.foo++
console.log('结束了')
除了控制副作用函数的执行顺序,通过调度器我们还可以做到控制它的执行次数,这一点也尤其重要,我们思考下面的例子:
effect(
function fn1() {
console.log(obj.foo)
}
)
obj.foo++
obj.foo++
在副作用函数中打印obj.foo的值,然后对其进行两次自增操作,在没有指定调度器的情况下,输出结果为:1,2,3
如果我们只关心结果,不关心过程,那么中心打印2就是多余的,因为这只是一个过渡状态,我们期望打印的结果是:1,3
基于调度器我们很容易就可以实现这个功能
// 定义一个任务队列
const jobQueue = new Set()
//使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 一个标志代表是否正在刷新队列
let isFlushing = false
function flushJob() {
//如果队列正在刷新,则什么也不做
if (isFlushing) return
isFlushing = true
p.then(() => {
jobQueue.forEach(job => job())
}).finally(() => {
isFlushing = false
})
}
effect(
function fn1() {
console.log(obj.foo)
},
{
scheduler(fn) {
//每次调度时,把副作用函数添加到jobQueue队列中
jobQueue.add(fn)
//调用flushJob刷新队列
flushJob()
}
}
)
obj.foo++
obj.foo++
这个功能有点类似于vue.js中连续修改多次响应式数据,但是只会触发一次更新,实际上vue.js内部实现了一个更加完善的调度器,思路和上文介绍的相同。
1.7计算属性的原理
大的要来啦!前面我们实现了副作用函数,和选项参数options,track函数,trigger函数等等,利用这些,我们就可以实现vue.js中一个非常重要并且十分有特色的能力,计算属性。
1.7.1懒加载的effect
在深入了解计算属性之前,我们先来了解一下懒加载的effect,即lazy的effect,什么意思呢?举个例子,现在我们所实现的effect函数会立即执行传递给它的副作用函数
effect(
//这个副作用函数会立刻执行
()=>{
console.log(obj.foo)
}
)
但是在有些场景下,我们不希望它立刻执行,而是希望它在需要的时候才执行,如计算属性,这个时候我们就可以通过在options中添加lazy属性来达到目的
effect(
//指定了lazy属性,函数不会立刻执行
()=>{
console.log(obj.foo)
},
//options
{
lazy:true
}
)
lazy选项和我们之前介绍的scheduler调度器一样,通过options选项指定,有了它我们就可以修改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]
}
effectFn.options = options
effectFn.deps = []
//新增
//只有options.lazy不为true的时候才执行
if (!options.lazy) {
effectFn()
}
//把副作用函数作为返回值返回
return effectFn
}
在代码中我们可以看到,我们根据lazy的值来判断是否立刻执行副作用函数,并且把副作用函数作为返回值返回,这就意味着我们调用effect函数的时候,通过返回值就可以拿到对应的副作用函数,这样我们就可以手动执行该副作用函数 了
const effectFn = effect(()=>{
console.log(obj.foo)
},{lazy:true}
)
//手动执行副作用函数
effectFn()
如果仅仅能手动执行副作用函数,那意义其实不大,但是如果我们把传递给effect的函数看作一个getter,那么这个getter函数可以返回任何值
const effectFn = effect(
()=> obj.foo + obj.bar,
{lazy:true}
)
//这样我们手动执行的时候,就可以拿到其返回值
const value = effectFn()
为了达到这个效果,我们要给我们的effect函数做一些修改,把里面的effectFn函数添加返回值
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
//新增
const res = fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
//新增
return res
}
//计算属性
function computed(getter) {
const effectFn = effect(getter, {
lazy: true
})
const obj = {
get value() {
return effectFn()
}
}
return obj
}
现在我们的计算属性就基本完成了,我们测试一下,会发现,getter中的数据发生变化时,手动调用计算属性.value会返回更新的值
const data1 = { foo: 1 }
const obj1 = new Proxy(data1, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
const data2 = { bar: 1 }
const obj2 = new Proxy(data2, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
const result = computed(() => obj1.foo + obj2.bar, { lazy: true })
但是,现在还存在一个问题,我们发现,如果我们为计算属性的value与副作用函数进行绑定时,如果getter中的值变了,并不能导致副作用函数执行(obj1.foo的值变化,或者obj2.bar的值变化)
effect(
() => {
document.querySelector('.myContainer').innerHTML = result.value
}
)
这是因为我们在计算属性中并没有执行track和trigger的代码,导致副作用函数不能和result.value关联起来
将track和trigger函数的代码添加上,我们发现,上述问题就顺利解决了
function computed(getter) {
const effectFn = effect(getter, {
lazy: true,
scheduler() {
console.log(2)
trigger(obj, 'value')
}
})
const obj = {
get value() {
console.log(1)
track(obj, 'value')
return effectFn()
}
}
return obj
}