2万字,深入响应式原理

249 阅读12分钟

基础结构

首先创建最基础的几个文件:

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

image.png

image.png

js中对一个对象的所有属性的操作,最终都是通过上面这些内置的函数来实现的,读属性通过其中的Get方法,而这个方法接受两个参数,一个是属性名,还有一个是Receiver,就是传进去的this

但是我们普通a.b这种访问对象属性的时候,是无法传入这个this的,但是通过Reflect就可以,可以直接调用上面内部的方法,上面每一个内置的方法,Reflect都有一个对于的方法。 image.png

那么我们只要做一些修改:

 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函数的

image.png

而在Reflect中,是has函数对应到这个方法。

image.png

我们加上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) {
}

image.png

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()

image.png

新增一下:

 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的值,但是很遗憾,这个过程是无法监听的,只能我们在代码里面手动处理这种情况。

image.png

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的收集了。

依赖收集

上面说了这么多,终于把基础的针对对象,针对数组的读写操作进行了监听和合适的处理。有了上面的架子,下面才能开始做依赖收集,看下面的结构,我们要把哪些对象的哪些属性进行了哪些读取操作,这些操作关联到哪些函数,记录清楚。

image.png

上面这些结构,目前只有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

没问题

参考

渡一教育袁老师的十元大师课