基础结构
首先创建最基础的几个文件:
reactive.js
import { track, trigger } from './effect.js'
export function reactive(target) {
return new Proxy(target, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
trigger(target, key)
return Reflect.set(target, key, value)
}
})
}
effect.js
export function track(target, key) {
console.log(`%c依赖收集:${key}`, 'color: red')
}
export function trigger(target, key) {
console.log(`%c触发更新:${key}`, 'color: blue')
}
index.js
import { reactive } from './reactive.js'
const state = reactive({
a: 1,
b: 2
})
function fn() {
state.a
state.b
}
fn()
state.a = 3
state.a++
index.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>
<script type="module" src="./index.js"></script>
</body>
</html>
看下打印结果
依赖收集:a
依赖收集:b
触发更新:a
依赖收集:a
触发更新:a
state.a++ 对应到先读取再赋值。 以上都是最基础的部分。
下面,开始一点点完善这个响应式结构。
对象的处理
首先是对于reactive.js来说,接受到的target并不总是对象,要处理非对象的情况。
新建utils.js
export function isObject(value) {
return value !== null && typeof value === 'object'
}
增加一个判断是不是对象的方法。
在reactive.js里面要加上判断,不是对象直接返回。
接着,对于同一个对象,如果多次调用reactive,应该返回同一份代理,而不是多份,那么我们要加一个weakMap,保存一下已经代理过的对象。
import { track, trigger } from './effect.js'
import { isObject } from './utils.js'
const targetMap = new WeakMap()
export function reactive(target) {
if (!isObject(target)) {
return target
}
if (targetMap.has(target)) {
return targetMap.get(target)
}
const proxy = new Proxy(target, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
trigger(target, key)
return Reflect.set(target, key, value)
}
})
targetMap.set(target, proxy)
return proxy
}
下面开始深入到对象的代理细节里面。
this指向问题
看一下这个例子:
import { reactive } from './reactive.js'
const obj = {
a: 1,
b: 2,
get c() {
return this.a + this.b
}
}
const state1 = reactive(obj)
function fn() {
state1.c
}
fn() // 依赖收集:c
期望的是收集了c a b,但是现在实际上只收集到了c,没有对a b 进行依赖收集。
问题就出在了get c中的this,打印一下这个this 是{a: 1, b: 2},是原对象,不是代理对象,访问原对象的a b属性是不会触发依赖收集的。
那么如何修改get 函数调用中的this。
用Reflect
Reflect
js中对一个对象的所有属性的操作,最终都是通过上面这些内置的函数来实现的,读属性通过其中的Get方法,而这个方法接受两个参数,一个是属性名,还有一个是Receiver,就是传进去的this。
但是我们普通a.b这种访问对象属性的时候,是无法传入这个this的,但是通过Reflect就可以,可以直接调用上面内部的方法,上面每一个内置的方法,Reflect都有一个对于的方法。
那么我们只要做一些修改:
get(target, key, receiver) {
track(target, key)
return Reflect.get(target, key, receiver)
}
我们再看一下上面的结果:
依赖收集:c
Proxy(Object) {a: 1, b: 2}
依赖收集:a
依赖收集:b
a b也可以进行依赖收集了,而且打印的this也是proxy的代理对象了。
同样的,我们把set也改一下:
set(target, key, value, receiver) {
trigger(target, key)
return Reflect.set(target, key, value, receiver)
}
对象的嵌套
如果属性值是一个对象:
const obj = {
a: {
b: 1
}
}
const state1 = reactive(obj)
state1.a.b // 依赖收集:a
state1.a的时候,返回的是{b:1},这个对象不是代理,所以再访问b属性的时候,不会触发依赖收集,应该要把 { b: 1}一块收集的,所以对传入的对象要做递归遍历,进行深度代理
get(target, key, receiver) {
track(target, key)
const result = Reflect.get(target, key, receiver)
if (isObject(result)) {
return reactive(result)
}
return result
}
结果也是符合预期的:
依赖收集:a
依赖收集:b
in
in操作符也是开发中经常使用的,但是目前我们的get是无法监控到in操作的,下面代码执行是不会打印任何东西的。
const obj = {
a: 1
}
const state1 = reactive(obj)
'a' in state1
看一下ECMA规范里面对in操作符的表述,最终是执行了hasProperty函数的
而在Reflect中,是has函数对应到这个方法。
我们加上has方法:
set(target, key, value, receiver) {
trigger(target, key)
return Reflect.set(target, key, value, receiver)
}
这个时候就能收集到a的依赖了:依赖收集:a
for in
for in也是常用的,我们现在的代码也处理不了这个情况:
const obj = {
a: 1
}
const state1 = reactive(obj)
for (const key in state1) {
}
for in对应的是Reflect.ownKeys(), 增加:
ownKeys(target) {
track(target )
return Reflect.ownKeys(target)
}
那么现在就能对for in进行相应,其实不止是for in
Object.keys(),Object.getOwnPropertyNames(),Object.getOwnPropertySymbols()这些都是走的ownKeys
Object.values(),Object.entries()是先走ownKeys,然后再走get
操作符分类
继续考虑一个问题:
const obj = {
a: 1
}
const state1 = reactive(obj)
'a' in state1
state1.a = 2
上面这种情况当给a重新赋值的时候,应不应该触发更新。 我们代码只关心a 在不在 state1里面,并不关心他的值是什么,所以不应该触发,但是现在的代码是会触发更新的。
那么具体到代码里面应该怎么做,我们应该对依赖的收集进行类型的区分,还要保存起来。
新建文件operation.js
export const TrackOpTypes = {
GET: 'get',
HAS: 'has',
ITERATE: 'iterate'
}
export const TriggerOpTypes = {
SET: 'set',
DELETE: 'delete',
ADD: 'add'
}
然后再依赖收集和派发更新的时候,我们都要带上这个分类:
get(target, key, receiver) {
track(target, key, TrackOpTypes.GET)
const result = Reflect.get(target, key, receiver)
if (isObject(result)) {
return reactive(result)
}
return result
},
has(target, key) {
track(target, key, TrackOpTypes.HAS)
return Reflect.has(target, key)
},
ownKeys(target) {
track(target, undefined, TrackOpTypes.ITERATE)
return Reflect.ownKeys(target)
},
set(target, key, value, receiver) {
trigger(target, key, TriggerOpTypes.SET)
return Reflect.set(target, key, value, receiver)
}
set
补齐set代码中的操作类型判断
set(target, key, value, receiver) {
const type = target.hasOwnProperty(key)
? TriggerOpTypes.SET
: TriggerOpTypes.ADD
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (!result) return
if (!Object.is(oldValue, value) || type === TriggerOpTypes.ADD) {
trigger(target, key, type)
}
return result
}
这里面判断对象是否有一个属性用的是hasOwnProperty,而不是in,主要的区别是对原型链上属性的识别,如果用的in,对原型链上存在的属性的赋值是SET,如果用的是hasOwnProperty,那么就是ADD。
判断值是否发生改变的时候也不要用===, 用Object.is(),会规避掉+0 -0 NaN特殊情况带来的问题
这里做的判断是,只有当新旧值不一样,或者你是新增属性的时候,才会触发派发更新。
delete
delete操作对应的是Reflect.deleteProperty()
新增一下:
deleteProperty(target, key) {
const hadKey = target.hasOwnProperty(key)
const res = Reflect.deleteProperty(target, key)
if (hadKey && res) {
trigger(target, key, TriggerOpTypes.DELETE)
}
return res
}
有了操作类型之后,我们改一下effect.js的打印:
export function track(target, key, type) {
console.log(`%c【${type}】:${key}`, 'color: red')
}
export function trigger(target, key, type) {
console.log(`%c【${type}】:${key}`, 'color: blue')
}
数组的处理
我们先试一下当前的代码对数组的处理:
const obj = [1]
const state1 = reactive(obj)
state1[0]
state1[0] = 9
for (let i = 0; i < state1.length; i++) {}
得到结果:
【get】:0
【set】:0
【get】:length
【get】:length
好像没啥问题。
for-of includes
但是当我们用for of的时候,就报错了:
const obj = [1]
const state1 = reactive(obj)
for (const item of state1) {
}
effect.js:2 Uncaught TypeError: Cannot convert a Symbol value to a string
这里报错的就是effect.js中的track函数中的
export function track(target, key, type) {
console.log(`%c【${type}】:${key}`, 'color: red')
}
中的key,说明这个key是一个Symbol,那我们打印一下看看key到底是什么,是Symbol(Symbol.iterator)。
我们改一下console.log(`%c【${type}】:`, 'color: red', key)。
现在输出的是:
【get】: Symbol(Symbol.iterator)
【get】: length
【get】: 0
【get】: length
先读了迭代器symbol.iterator,然后开始遍历数组,读length和每一项。
再试一下includes:
const obj = [2, 1]
const state1 = reactive(obj)
state1.includes(1)
【get】: includes
【get】: length
【get】: 0
【get】: 1
先读的includes,然后读的length,再依次读每一项
再看看lastIndexOf
const obj = [2, 1, 3,,]
const state1 = reactive(obj)
state1.lastIndexOf(1)
【get】: lastIndexOf
【get】: length
【has】: 3
【has】: 2
【get】: 2
【has】: 1
【get】: 1
这里不一样的是在读取每一项之前,先读了has,也就是in操作符。
我们这里给的是一个稀疏数组,后面两项是没有填充的,所有后面两项只读了has,没有读get。所以这里如果我们lastIndexOf(undefined),会返回的是-1,而不是最后一项的下标。
到这里看上去都没啥问题:
const obj1 = {
a: 1
}
const obj = [obj1]
const state1 = reactive(obj)
console.log(state1.includes(obj1))
当查找一个对象的时候,返回的竟然是false,用lastIndexOf和indexOf也都查不到。
【get】: includes
【get】: length
【get】: 0
false
分析一下代码,是因为在get里面,我们递归处理对象,所以get得到的是proxy对象,而不是原始的对象,再和原始的对象进行比较,肯定是不相同的。
那么我们只要在这几个函数执行的时候,如果includes返回的是false,其他两个返回的是-1,也就是没找到的时候,我们再去拿原始对象调用一下这几个方法。
reactive.js中,我们要重写一下includes,indexOf,lastIndexOf这几个方法:
const arrayInstrumentations = {}
const RAW = Symbol('raw')
;['includes', 'indexOf', 'lastIndexOf'].forEach((key) => {
arrayInstrumentations[key] = function (...args) {
// 这里的this是proxy
const res = Array.prototype[key].apply(this, args)
if (res < 0 || res === false) {
// this[RAW]是原始对象
return Array.prototype[key].apply(this[RAW], args)
}
return res
}
})
...
get(target, key, receiver) {
if (key === RAW) {
return target
}
track(target, key, TrackOpTypes.GET)
if (arrayInstrumentations.hasOwnProperty(key) && Array.isArray(target)) {
return arrayInstrumentations[key]
}
const result = Reflect.get(target, key, receiver)
if (isObject(result)) {
return reactive(result)
}
return result
}
首先是重写了这三个方法,如果没有拿到,就尝试用原始对象去调用一下。
在get里,如果key是Symbol(raw),那么就返回原始对象。(这种方法的缺点就是,必须先把代理数组全部遍历一遍,然后再去原始数组里面遍历)。
这个问题解决了,再看一个:
const obj = [1]
const state1 = reactive(obj)
state1[2] = 1
下标赋值
【add】:2
这里add了属性2,看上去很合理,但是有一个问题,数组的length肯定是发生了改变的,但是这里没有体现出来。
在ECMA规范里面有这块的体现,当index > length的时候,会直接去更新length的值,但是很遗憾,这个过程是无法监听的,只能我们在代码里面手动处理这种情况。
set中:
const oldValue = target[key]
// 获取数组原来的长度
const oldLenth = Array.isArray(target) ? target.length : 0
const result = Reflect.set(target, key, value, receiver)
...
if (!result) return
// 获取数组现在的长度
const newLenth = Array.isArray(target) ? target.length : 0
...
if (!Object.is(oldValue, value) || type === TriggerOpTypes.ADD) {
trigger(target, key, type)
// 手动触发length的set
if (Array.isArray(target) && oldLenth !== newLenth) {
if (key !== 'length') {
trigger(target, 'length', TriggerOpTypes.SET)
}
}
}
如果target是数组,并且长度变了,并且变的不是length,就手动触发length的set。
【add】:2
【set】:length
现在看一下结果,length触发了。
刚刚是把length变大,那么把length变小呢?
const obj = [1, 2, 3]
const state1 = reactive(obj)
state1.length = 1
【set】:length 触发了length,没问题,但是此时算是数组删除了两个元素,没有触发。
if (Array.isArray(target) && oldLenth !== newLenth) {
if (key !== 'length') {
trigger(target, 'length', TriggerOpTypes.SET)
} else {
for (let i = newLenth; i < oldLenth; i++) {
trigger(target, i.toString(), TriggerOpTypes.DELETE)
}
}
}
其实就只要在key是length的时候,从新长度遍历到旧长度,触发一下delete就行。
push
我们来试一下Push方法
const obj = [1]
const state1 = reactive(obj)
state1.push(2)
【get】: push
【get】: length
【add】:1
【set】:length
push方法是为了给数组添加元素,我们其实没有必要去收集他的length的变化。
除了push之外,其实还有其他一些方法也有一样的问题,比如: pop, shift, unshift, splice,我们要给这些方法统一做处理。在这些方法执行的期间,我们要暂停依赖收集。
reactive.js中
import { track, trigger, pauseTracking, resumeTracking } from './effect.js'
['push', 'pop', 'shift', 'unshift', 'splice'].forEach((key) => {
arrayInstrumentations[key] = function (...args) {
pauseTracking()
const res = Array.prototype[key].apply(this, args)
resumeTracking()
return res
}
})
effect.js中
let shouldTrack = true
export function pauseTracking() {
shouldTrack = false
}
export function resumeTracking() {
shouldTrack = true
}
export function track(target, key, type) {
if (!shouldTrack) return
console.log(`%c【${type}】:`, 'color: red', key)
}
看一下现在的打印结果:
【get】: push
【add】:1
【set】:length
这时就不会多一次length的收集了。
依赖收集
上面说了这么多,终于把基础的针对对象,针对数组的读写操作进行了监听和合适的处理。有了上面的架子,下面才能开始做依赖收集,看下面的结构,我们要把哪些对象的哪些属性进行了哪些读取操作,这些操作关联到哪些函数,记录清楚。
上面这些结构,目前只有dep中的函数,也就是正儿八经的依赖,我们还没有拿到,下面来拿一下调用函数。
effect.js中
let activeEffect = undefined
...
export function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
export function track(target, key, type) {
if (!shouldTrack || !activeEffect) return
console.log(activeEffect)
console.log(`%c【${type}】:`, 'color: red', key)
}
使用的时候:
import { reactive } from './reactive.js'
import { effect } from './effect.js'
const obj = {
a: 1
}
const state1 = reactive(obj)
function fn1() {
state1.a
}
effect(fn1)
这里函数的调用交给effect函数,在effect函数里面,把执行函数交给一个全局的变量,调用结束之后,清空这个变量。那么执行函数,触发里面的track函数的时候,就能拿到当前的函数了。
// 打印结果
ƒ fn1() {
state1.a
}
【get】: a
下面开始实现依赖收集的各种map set
const ITERATE_KEY = Symbol('iterate')
export function track(target, key, type) {
if (!shouldTrack || !activeEffect) return
let propMap = targetMap.get(target)
if (!propMap) {
propMap = new Map()
targetMap.set(target, propMap)
}
if (type === ITERATE_KEY) {
key = ITERATE_KEY
}
let typeMap = propMap.get(key)
if (!typeMap) {
typeMap = new Map()
propMap.set(key, typeMap)
}
let depSet = typeMap.get(type)
if (!depSet) {
depSet = new Set()
typeMap.set(type, depSet)
}
if (!depSet.has(activeEffect)) {
depSet.add(activeEffect)
}
console.log(targetMap)
console.log(`%c【${type}】:`, 'color: red', key)
}
这里要额外加一个ITERATE_KEY = Symbol('iterate') 前面说for of的时候说过,执行for of的时候,实际上访问的是Symbol('iterate')属性,这里给他加上
派发更新
派发更新就是,当某个属性发生了某种变化之后,找到对应的dep中的所有函数,进行执行。 effect.js中
export function trigger(target, key, type) {
const effectFns = getEffectFns(target, key, type)
for (const effectFn of effectFns) {
effectFn()
}
console.log(`%c【${type}】:${key}`, 'color: blue')
}
function getEffectFns(target, key, type) {
const propMap = targetMap.get(target)
if (!propMap) return
const keys = [key]
if (type === TriggerOpTypes.ADD || type === TriggerOpTypes.DELETE) {
keys.push(ITERATE_KEY)
}
const effectFns = new Set()
const triggerTypeMap = {
[TriggerOpTypes.SET]: [TrackOpTypes.GET],
[TriggerOpTypes.ADD]: [
TrackOpTypes.GET,
TrackOpTypes.HAS,
TrackOpTypes.ITERATE
],
[TriggerOpTypes.DELETE]: [
TrackOpTypes.GET,
TrackOpTypes.HAS,
TrackOpTypes.ITERATE
]
}
for (const key of keys) {
const typeMap = propMap.get(key)
if (!typeMap) continue
const trackTypes = triggerTypeMap[type]
for (const trackType of trackTypes) {
const dep = typeMap.get(trackType)
if (dep) {
dep.forEach((effectFn) => {
effectFns.add(effectFn)
})
}
}
}
return effectFns
}
这里面用上了我们之前存下来的操作分类,比如对set来说,只有get类型的才需要去派发更新。
还考虑了ADD和Delete的时候,对Iterate的影响。
依赖的重新收集与清除
用下面的例子测试一下
import { reactive } from './reactive.js'
import { effect } from './effect.js'
const obj = {
a: true,
b: 2,
c: 2
}
const state1 = reactive(obj)
function fn1() {
if (state1.a) {
state1.b
} else {
state1.c
}
}
effect(fn1)
state1.a = false
fn
【get】: a
【get】: b
fn
【set】:a
fn打印了两次,说明这个函数执行了两次,说明派发更新是没问题的,但是state1.c的set没有检测到。
第一次依赖收集的时候,只收集了a, b, 没有收集c(因为没有执行state1.c), 当state1.a变为false的时候,派发更新会去触发fn的重新运行,这里更新了state.c,但是此时的activeEffect是null,上面的判断,null的时候,是不会触发依赖收集的。
所以当state1.a发生改变了之后,再次运行fn1的时候,要重新进行依赖收集,改一下effect函数。
export function effect(fn) {
const effectFn = () => {
try {
activeEffect = effectFn
return fn()
} finally {
activeEffect = null
}
}
effectFn()
}
再试一下:
fn
【get】: a
【get】: b
fn
【get】: a
【get】: c
【set】:a
c被收集了。
还是这个例子,加一行代码:
const obj = {
a: true,
b: 2,
c: 3
}
const state1 = reactive(obj)
function fn1() {
console.log('fn')
if (state1.a) {
state1.b
} else {
state1.c
}
}
effect(fn1)
state1.a = false
state1.b = 1
看一下运行结果:
fn
【get】: a
【get】: b
fn
【get】: a
【get】: c
【set】:a
fn
【get】: a
【get】: c
【set】:b
fn 运行了三次,最后一次是由于b的set引起的。但是当a为false的时候,b的set对函数是无关紧要的,不应该派发更新的,b的依赖是第一次收集的时候加进去的,那么当第二次重新收集依赖的时候,应该要把b去掉,也就是把上一次的依赖清空。
effect.js中
effectFn需要一个deps属性保存所有相关的属性,记录一下哪些属性的set保存了当前的函数。 执行fn前,先执行cleanUp函数,把那些set中保存的当前函数删除,然后还要清空自己的deps数组。
export function effect(fn) {
const effectFn = () => {
try {
activeEffect = effectFn
cleanUp(effectFn)
return fn()
} finally {
activeEffect = null
}
}
effectFn.deps = []
effectFn()
}
export function cleanUp(effectFn) {
for (const depSet of effectFn.deps) {
depSet.delete(effectFn)
}
effectFn.deps.length = 0
}
export function track(target, key, type) {
...
if (!depSet.has(activeEffect)) {
depSet.add(activeEffect)
activeEffect.deps.push(depSet)
}
...
}
看一下结果,fn就只执行了两次
fn
【get】: a
【get】: b
fn
【get】: a
【get】: c
【set】:a
【set】:b
再看一种情况:
const obj = {
a: true,
b: 2
}
const state1 = reactive(obj)
function fn1() {
console.log('fn')
effect(() => {
console.log('inner')
state1.a
})
state1.b
}
effect(fn1)
state1.b = 1
理论上fn应该执行两次的,但是只执行了一次。
fn
inner
【get】: a
【set】:b
执行栈
这里是因为嵌套的调用,当进入fn执行的时候,activeEffect有值,然后进入Inner的执行,activeEffect又有值了,但是当inner执行完的时候,activeEffect是Null,这个时候再去执行fn中的state.b的时候,就不会进行依赖收集了,这是执行栈的问题,我们加一个栈就能解决。
const effectStack = []
export function effect(fn) {
const effectFn = () => {
try {
activeEffect = effectFn
effectStack.push(effectFn)
cleanUp(effectFn)
return fn()
} finally {
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
}
effectFn.deps = []
effectFn()
}
...
这就完整了
fn
inner
【get】: a
【get】: b
fn
inner
【get】: a
【get】: b
【set】:b
再看一种:
const obj = {
b: 2
}
const state1 = reactive(obj)
function fn1() {
console.log('fn')
state1.b++
}
effect(fn1)
直接给你干成栈溢出
fn
【get】: b
fn
【get】: b
fn
RangeError: Maximum call stack size exceeded
换成state1.a = state1.a + 1也是一样的,栈溢出,因为改动的时候触发了fn执行,执行又触发了改动...
在派发更新的时候,如果这个依赖就是当前执行的函数,就跳过:
export function trigger(target, key, type) {
const effectFns = getEffectFns(target, key, type)
for (const effectFn of effectFns) {
if (effectFn === activeEffect) {
continue
}
effectFn()
}
console.log(`%c【${type}】:${key}`, 'color: blue')
}
那么到这里呢,响应式的基本功能就差不多都实现了,代码对很多场景都做了适应,大部分情况下都是可用的。
扩展
对实现的响应式做一些功能扩展,让他能适应更灵活的场景。
const effectFn = effect(fn1, {
lazy: true,
scheduler: (eff) => {
console.log('scheduler')
}
})
实现的效果是可以lazy触发,也就是不立即执行,这样就是把effectFn这个函数本身返回出来,还有就是当传入sheduler的时候,派发更新的时候,会把effectFn作为参数传给你,由你决定执行什么样的函数。
export function effect(fn, options = {}) {
// 新增
const { lazy = false } = options
...
effectFn.options = options
if (!lazy) {
effectFn()
}
return effectFn
}
export function trigger(target, key, type) {
const effectFns = getEffectFns(target, key, type)
if (!effectFns) return
for (const effectFn of effectFns) {
if (effectFn === activeEffect) {
continue
}
// 新增
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
}
ref
下面实现一下ref函数。 新建ref.js
export function ref(value) {
return {
get value() {
track(this, 'value', TrackOpTypes.GET)
return value
},
set value(newValue) {
value = newValue
trigger(this, 'value', TriggerOpTypes.SET)
}
}
}
试一下
const state = ref(1)
effect(() => {
console.log('effect', state.value)
})
state.value++
没有问题
【get】: value
effect 1
【get】: value
effect 2
【set】:value
computed
新建computed.js
import { effect, track, trigger } from './effect.js'
import { TrackOpTypes, TriggerOpTypes } from './operation.js'
function normalizeParameter(getterOrOptions) {
let getter, setter
if (typeof getterOrOptions === 'function') {
getter = getterOrOptions
setter = () => {
console.warn('computed value is readonly')
}
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return {
getter,
setter
}
}
export function computed(getterOrOptions) {
const { getter, setter } = normalizeParameter(getterOrOptions)
let value,
dirty = true
const effectFn = effect(getter, {
lazy: true,
scheduler() {
dirty = true
trigger(obj, 'value', TriggerOpTypes.SET)
}
})
const obj = {
get value() {
track(obj, 'value', TrackOpTypes.GET)
if (dirty) {
value = effectFn()
dirty = false
}
return value
},
set value(newValue) {
setter(newValue)
}
}
return obj
}
comouted中最重要的就是使用了dirty变量,只有当getter中进行了派发更新的时候,dirty会变成true,也就是这个数据是脏数据不能用了,下一次你再读value属性的时候,才会去重新执行一下effectFn,否则就直接返回value。
用一下:
const state = reactive({
a: 1,
b: 2
})
const sum = computed(() => {
console.log('computed')
return state.a + state.b
})
effect(() => {
console.log('effect', sum.value)
})
state.a++
结果:
【get】: value
computed
【get】: a
【get】: b
effect 3
【get】: value
computed
【get】: a
【get】: b
effect 4
【set】:value
【set】:a
没问题
参考
渡一教育袁老师的十元大师课