深入理解Vue 3.0 Reactive

3,271 阅读13分钟

因为Vue 3.0刚推出,本身打算只是简单了解下,但了解Vue 3.0的新特性后,如monorepo代码管理,源码偏向函数性编程,还有Composition Api设计,拍手叫绝,决定认真学习下,刚好前段时间拉勾教育搞特惠,用便宜的价钱买了Vue 3.0源码解析,结合最近在拉勾前端训练营学到手撕Reactive Api,整理一篇Vue 3.0响应式原理笔记。

代理

深入了解Vue 3.0前,必须先了解Javascript的代理 (Proxy),因为Vue 3.0的响应式数据是基于代理实现的。在Vue 2.0时代,响应式数据是基于 Object.defineProperty实现,可以用下图概括它的实现:

vue_reactive

Object.defineProperty 的好处是兼容性好,可以操控对象属性的细节,然而也有相应的问题出现,一是直接修改对象本身,二是在数据劫持上性能劣于Proxy,三是由于是针对属性,所以如果属性上有任何变动,无法处理,四是无法处理数组。

上述的问题在Vue 3.0中通过 Proxy解决。

代理模式

代理不是JS独有的对象,JS的Proxy是基于代理模式所设计,所以了解代理模式,有助我们更好理解代理。 代理模式指的是间接操控对象的设计,用一张图概括说明:

proxy_pattern

我们平时在桌面上的捷径其实就是代理模式的实现,用户不是直接打开应用软件,而是通过桌面的捷径打开。

Proxy

Javascript的Proxy就是基于上述的代理模式设计的,可以通过Proxy间接操控对象或数组。

下面用代码示例简单介绍它的用法:

const target = {
foo: 'bar'
};

const handler = {
get() {
return 'handler override';
}
};
const proxy = new Proxy(target, handler);
console.log (proxy.foo)  // handler override 
console.log (target.foo) // bar

Proxy接收两个参数,第一个是需要代理的对象,第二个是捕获器 (handler),它是一个对象,里面有Proxy指定的捕获方法,如 get, set, delete等,用于操作代理对象时,触发不同的捕获器。注意,与Object.defineProperty不同的是,它是以整个对象为单位,而不是属性。用户可以通过操作 Proxy创建的实例去间接操作对象本身。 上述的示例中,添加get处理器,重载对象的获取。

get接收3个参数: trapTarget, property, receiver。trapTarget是捕获对象,property是属性,receiver是代理对象本身。有了这些参数,就可以重建被捕获方法的原始行为:

const target = {
foo: 'bar'
};

const handler = {
get(trapTarget, property, receiver) {
  console.log (receiver === proxy) // true
return trapTarget[property];
}
};

const proxy = new Proxy(target, handler);
console.log(proxy.foo); // true  bar
console.log(target.foo); // bar

处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和函数签名,而且也具有与被拦截方法相同的行为。因此,使用反射API 也可以 像下面这样定义出空代理对象:

const target = {
  foo: 'bar'
};

const handler = {
  get() {
    return Reflect.get(...arguments);
  }
};

const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar

Proxy的简单介绍到此为止,详细可查阅 MSDN或 Javascript高级程序设计 (第四版)。

Reactive 简单实现

Vue 2.0 的响应式数据创建是 在"黑盒" 进行,即创建Vue实例时,根据传入的参数来创建响应式数据。而在 Vue 3.0 则是可以显式创建响应式数据:

<template>
  <div>
    <p>{{ state.msg }}</p>
    <button @click="random">Random msg</button>
  </div>
</template>
<script>
  import { reactive } from 'vue'
  export default {
    setup() {
      const state = reactive({
        msg: 'msg reactive'
      })

      const random = function() {
        state.msg = Math.random()
      }

      return {
        random,
        state
      }
    }
  }
</script>

上述的例子导入reactive函数,通过reactive显式创建响应式数据。

在阅读reactive源码前,先简单实现一个reactive来理解它。

先看下官方文档reactive是干什麽的:

Returns a reactive copy of the object.

The reactive conversion is "deep"—it affects all nested properties. In the ES2015 Proxy based implementation, the returned proxy is not equal to the original object. It is recommended to work exclusively with the reactive proxy and avoid relying on the original object.

简单来说,就是reactive接收一个对象,返回一个响应式副本,它是一个Proxy实例,里面的属性,包括嵌套对象都是响应式的。

根据上述得知,函数首先需要判定参数是否为对象,返回一个Proxy实例:

// 因为null的typeof也是object,所以要另外增加对它的判定
const isObject = val => val !== null && typeof val === 'object'

function reactive (target) {
  if (!isObject (target)) {
    return target
  }
  ...
  return new Proxy(target, handler)
}

现在可以实现捕获器,需要注意要对嵌套情况的处理:

const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target

function reactive (target) {
  if (!isObject(target)) return target

  const handler = {
    get (target, key, receiver) {
      const result = Reflect.get(target, key, receiver)
      return convert(result)
    },
    set (target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)
      }
      return result
    },
    deleteProperty (target, key) {
      const result = Reflect.deleteProperty(target, key)
      return result
    }
    
    return new Proxy(target, handler)
  }

Vue 2.0对嵌套对象情况,是创建实例时直接递归转变为响应式数据,而在Vue 3.0则是当获取相应属性时处理,判断是否是嵌套对象,是则递归创建响应式数据,从而优化性能。

现在有一个大概的实现,不过最关键的响应式部分还没有实现。Vue 3.0的响应式设计与Vue 2.0 类似,也是使用观察者模式,所以可以参考 Vue 2.0的简单实现,有助于理解Vue 3.0的实现。

Vue 3.0会存有一个全局的TargetMap,用来存放收集依赖,它的键为被依赖的对象,值也是一个Map,键为被依赖的属性,值是属性发生改变时,需要调用的函数。因此我们需要track和trigger函数,前者收集依赖,后者当属性发生改变,调用函数。

let targetMap = new WeakMap()  // 全局变量,存放依赖

function track (target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)  // 获取被依赖对象的Map,没有则创建
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key) // 根据对象属性,获取需调用的函数,没有则创建
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)
}

function trigger (target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}

剩下最关键的effect还没有实现。实现之前,先看一个使用例子:

<!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">
    import { reactive, effect } from './reactivity/index.js'

    const product = reactive({
      name: 'iPhone',
      price: 5000,
      count: 3
    })
    let total = 0 
    effect(() => {
      total = product.price * product.count
    })
    console.log(total)  // 15000 

    product.price = 4000
    console.log(total) // 12000

    product.count = 1
    console.log(total) // 4000

  </script>
</body>
</html>

上述的例子可得知,当调用effect函数时,它会执行一次传入的函数,如果之后传入函数里的值发生变更,会调用之前effect里传入的函数。问题是Vue是怎样知道要调用的函数?答案是当调用effect函数时,Vue已经在获取相应的值时,收集依赖。先看effect的实现:

let activeEffect = null  // 全局指针指向最近传入effect的函数
function effect (callback) {
  activeEffect = callback
  callback() // 访问响应式对象属性,去收集依赖
  activeEffect = null
}

通过上述的例子解释effect的执行过程。我们先命名例子传入的函数为 totalSum,当调用effect时,activeEffect指向totalSum,然后调用totalSum,它会分别获取 product.price 和 product.count,就在此时,触发了代理对象的get捕获器,因此触发了track函数,收集依赖。再看一次track吧:

let targetMap = new WeakMap()  // 全局变量,存放依赖

function track (target, key) {
  if (!activeEffect) return
  let depsMap = targetMap.get(target)  // 获取被依赖对象的Map,没有则创建
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key) // 根据对象属性,获取需调用的函数,没有则创建
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)  // 收集依赖
}

最后把track和trigger放进代理对象proxy里就完成reactive函数了:

const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)

function reactive (target) {
  if (!isObject(target)) return target

  const handler = {
    get (target, key, receiver) {
      // 收集依赖
      track(target, key)
      const result = Reflect.get(target, key, receiver)
      return convert(result)
    },
    set (target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)
        // 触发更新
        trigger(target, key)
      }
      return result
    },
    deleteProperty (target, key) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && result) {
        // 触发更新
        trigger(target, key)
      }
      return result
    }
  }

  return new Proxy(target, handler)
}

用网上找到一个图概括Vue 3.0的响应式原理:

vue3_reactive

源码阅读

有了简单实现的基础后,可以阅读源码了。

Reactive函数的源代码位于源码路径 packages/reactivity/src/reactive.ts。

function reactive (target) {
   // 如果尝试把一个 readonly proxy 变成响应式,直接返回这个 readonly proxy
  if (target && target.__v_isReadonly) {
     return target
  } 

  return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers)
}

// isReadonly指定target是否唯读,baseHandlers是基本数据类型的代理捕获器,collectionHandlers则是集合的
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {

  if (!isObject(target)) {
    // 目标必须是对象或数组类型
    if ((process.env.NODE_ENV !== 'production')) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }

    return target
  }

  if (target.__v_raw && !(isReadonly && target.__v_isReactive)) {
    // target 已经是 Proxy 对象,直接返回
    // 有个例外,如果是 readonly 作用于一个响应式对象,则继续
    return target

  }

  if (hasOwn(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */)) {

    // target 已经有对应的 Proxy 了
    return isReadonly ? target.__v_readonly : target.__v_reactive
  }

  // 只有在白名单里的数据类型才能变成响应式

  if (!canObserve(target)) {
    return target
  }

  // 利用 Proxy 创建响应式
  const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers)

  // 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
  def(target, isReadonly ? "__v_readonly" /* readonly */ : "__v_reactive" /* reactive */, observed)

  return observed
}

以上是reactive基本构建过程,与之前实现差不多,只是源码考虑更多。isReadonly是一个布尔值,用于表示代理对象是否唯读。Proxy比Object.defineProperty好的一点是,它可以处理数组。

canObserve函数针对target作进一步限制:

const canObserve = (value) => {
  return (!value.__v_skip &&
   isObservableType(toRawType(value)) &&
   !Object.isFrozen(value))
}

const isObservableType = /*#__PURE__*/ makeMap('Object,Array,Map,Set,WeakMap,WeakSet')

带有 __v_skip 属性的对象、被冻结的对象,以及不在白名单内的对象是不能变成响应式的。

const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers) 创建代理对象,根据target的构造函数,如果是基本数据类型,则返回baseHandlers,它的值是mutableHandlers。

const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

无论命中哪个处理器函数,它都会做依赖收集和派发通知这两件事其中的一个。

依赖收集:get 函数

看下创造get捕获器的源码:

function createGetter(isReadonly = false) {

  return function get(target, key, receiver) {
    // 根据不同属性返回不同结果
    if (key === "__v_isReactive" /* isReactive */) {
      // 代理 observed.__v_isReactive
      return !isReadonly
    }
    else if (key === "__v_isReadonly" /* isReadonly */) {
      // 代理 observed.__v_isReadonly
      return isReadonly;
    }
    else if (key === "__v_raw" /* raw */) {
      // 代理 observed.__v_raw
      return target
    }

    const targetIsArray = isArray(target) 
    
    // 如果target是数组而且属性包含于arrayInstrumentations
    // arrayInstrumentations 包含对数组一些方法修改的函数
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 求值
    const res = Reflect.get(target, key, receiver)

    // 内置 Symbol key 不需要依赖收集
    if (isSymbol(key) && builtInSymbols.has(key) || key === '__proto__') {
      return res
    }

    // 依赖收集
    !isReadonly && track(target, "get" /* GET */, key)
    
    return isObject(res)
      ? isReadonly
        ?
        readonly(res)
        // 如果 res 是个对象或者数组类型,则递归执行 reactive 函数把 res 变成响应式
        : reactive(res)
      : res
  }
}

看一下arrayInstrumentations,它是对代理数组的一些方法,调用所包含的方法时,收集依赖:

const arrayInstrumentations = {}
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
  arrayInstrumentations[key] = function (...args) {
    // toRaw 可以把响应式对象转成原始数据
    const arr = toRaw(this) // this是数组本身
    for (let i = 0, l = this.length; i < l; i++) {
      // 依赖收集
      track(arr, "get" /* GET */, i + '')
    }

    // 先尝试用参数本身,可能是响应式数据
    const res = arr[key](...args)
    if (res === -1 || res === false) {
      // 如果失败,再尝试把参数转成原始数据
      return arr[key](...args.map(toRaw))
    }
    else {
      return res
    }
  }
})

为什么要修改这几个方法?因为当修改数组数据时,这几个方法获得的值可能不同,所以每次调用它们,都需要重新收集依赖。

看track函数之前,看一下get的最后,最后根据结果作出不同行动,如果是基本数据类型,直接返回值,否则递归变成响应式数据,这是与Vue 2.0不同之处。vue 2.0是创建时直接递归处理,3则是当获取属性时才判断是否处理,延时定义子对象响应式的实现,在性能上会有较大的提升。

最后看get的最核心函数track:

// 是否应该收集依赖
let shouldTrack = true

// 当前激活的 effect
let activeEffect

// 原始数据对象 map
const targetMap = new WeakMap()

function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

  let depsMap = targetMap.get(target)
  
  if (!depsMap) {
    // 每个 target 对应一个 depsMap
    targetMap.set(target, (depsMap = new Map()))
  }

  let dep = depsMap.get(key)
  if (!dep) {

    // 每个 key 对应一个 dep 集合
    depsMap.set(key, (dep = new Set()))
  }

  if (!dep.has(activeEffect)) {
    // 收集当前激活的 effect 作为依赖
    dep.add(activeEffect)
   // 当前激活的 effect 收集 dep 集合作为依赖
    activeEffect.deps.push(dep)
  }
}

基本实现与之前的简单版相同,只是现在激活的effect也要收集dep作为依赖。

派发通知:set 函数

派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。我们来看一下 set 函数的实现,它是执行 createSetter 函数的返回值:

function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key]
    value = toRaw(value)
    const hadKey = hasOwn(target, key)
    // 使用Reflect修改
    const result = Reflect.set(target, key, value, receiver)

    // 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
    if (target === toRaw(receiver)) {
      if (!hadKey) {
      没有属性而增加则触发增加类型
        trigger(target, "add" /* ADD */, key, value)
      }
      else if (hasChanged(value, oldValue)) {
      有属性而修改则触发修改类型
        trigger(target, "set" /* SET */, key, value, oldValue)
      }
    }
    return result
  }
}

set的逻辑很简单,重点是trigger函数,它的作用是派发通知。


// 原始数据对象 map WeakMap的特点是键为引用
const targetMap = new WeakMap()

function trigger(target, type, key, newValue) {
  // 通过 targetMap 拿到 target 对应的依赖集合
  const depsMap = targetMap.get(target)

  if (!depsMap) {
    // 没有依赖,直接返回
    return
  }

  // 创建运行的 effects 集合
  const effects = new Set()

  // 添加 effects 的函数
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        effects.add(effect)
      })
    }
  }

  // SET | ADD | DELETE 操作之一,添加对应的 effects
  if (key !== void 0) {
    add(depsMap.get(key))
  }

  const run = (effect) => {
    // 调度执行
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    }
    else {
      // 直接运行
      effect()
    }
  }
  
  // 遍历执行 effects
  effects.forEach(run)
}

派发通知与之前的实现相似,就是获取相应代理对象属性收集的依赖,然后派发通知。

副作用函数: effect

重点需要关注的是 activeEffect (当前激活副作用函数),这部分的实现比之前的简易版複杂多了,它是整个Vue 3.0响应式的重点。

// 全局 effect 栈
const effectStack = []

// 当前激活的 effect
let activeEffect

function effect(fn, options = EMPTY_OBJ) {
  if (isEffect(fn)) {
    // 如果 fn 已经是一个 effect 函数了,则指向原始函数
    fn = fn.raw
  }

  // 创建一个 wrapper,把传入的函数封装,它是一个响应式的副作用的函数
  const effect = createReactiveEffect(fn, options)

  if (!options.lazy) {
    // lazy 配置,计算属性会用到,非 lazy 则直接执行一次
    effect()
  }

  return effect
}

与之前简单让activeEffect指向最近使用的effect函数不同,源码还封装effect函数,接下来看它是怎样封装:

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect(...args) {
    if (!effect.active) {
      // 非激活状态,则判断如果非调度执行,则直接执行原始函数。
      return options.scheduler ? undefined : fn(...args)
    }

    if (!effectStack.includes(effect)) {
      // 清空 effect 引用的依赖
      cleanup(effect)

      try {
        // 开启全局 shouldTrack,允许依赖收集
        enableTracking()
        
        // 压栈
        effectStack.push(effect)
        activeEffect = effect
        
        // 执行原始函数
        return fn(...args)
      }
      finally {
        // 出栈
        effectStack.pop()

        // 恢复 shouldTrack 开启之前的状态
        resetTracking()

        // 指向栈最后一个 effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }

  // 给effect一个id
  effect.id = uid++

  // 标识是一个 effect 函数
  effect._isEffect = true

  // effect 自身的状态
  effect.active = true

  // 包装的原始函数
  effect.raw = fn

  // effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
  effect.deps = []

  // effect 的相关配置
  effect.options = options

  return effect
}

createReactiveEffect最后返回一个带属性的effect函数。封装effect函数的过程中,做了两件事:

  1. 设定activeEffect的指向
  2. 负责effectStack的出入栈

第1点在之前的简易版了解,为什么要有 effectStack?因为要处理嵌套场景,考虑以下场景:

import { reactive} from 'vue' 
import { effect } from '@vue/reactivity' 

const counter = reactive({ 
num: 0, 
num2: 0 
}) 

function logCount() { 
  effect(logCount2) 
  console.log('num:', counter.num) 
} 

function count() { 
  counter.num++ 
} 

function logCount2() { 
  console.log('num2:', counter.num2) 
} 

effect(logCount) 
count()

我们希望的是当 counter.num发生变化时,触发logCount函数,但如果没有栈,只有activeEffect指针,当调用 effect(logCount),activeEffect指向的是 logCount2,而不是logCount,所以最后结果是:

num2: 0 

num: 0 

num2: 0

而不是:

num2: 0 

num: 0 

num2: 0 

num: 1

因此我们需要一个栈来存下外层的effect函数,以让activeEffect指针之后指向外层effect。不妨重看源码相关部分:

function createReactiveEffect(fn, options) {
  ...
  try {
		...
        // 压栈
        effectStack.push(effect)
        activeEffect = effect
        
        // 执行原始函数
        return fn(...args)
      }
      finally {
        // 执行完结后,出栈
        effectStack.pop()

        // 恢复 shouldTrack 开启之前的状态
        resetTracking()

        // 指向栈最后一个 effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
...
  return effect
}

当effect函数调用logCount时,把logCount压入effectStack栈中,然后在logCount里,又有一个effect函数调用logCount2,把logCount2压入effectStack栈中。logCount2获取counter.num2的值,这时activeEffect指向logCount2,counter.num2收集logCount2 (activeEffect)为依赖,然后effect执行finally区域的代码,把logCount2出栈,activeEffect指向栈的尾部,即logCount,现在logCount继续执行,获取counter.num,counter.num把logCount收集为依赖,因为activeEffect指向logCount。

counter.num发生变化,则会执行logCount。

最后还有一个cleanUp函数没有解释,它会把effect函数的依赖删除:

function cleanup(effect) {

  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }

    deps.length = 0
  }
}

在执行 track 函数的时候,除了收集当前激活的 effect 作为依赖,还通过 activeEffect.deps.push(dep) 把 dep 作为 activeEffect 的依赖,这样在 cleanup 的时候我们就可以找到 effect 对应的 dep 了,然后把 effect 从这些 dep 中删除。

为什么需要 cleanup 呢?因为要考虑组件的渲染函数也是副作用函数,以下面的场景为例:

<template>
  <div v-if="state.showMsg">
    {{ state.msg }}
  </div>
  <div v-else>
    {{ Math.random()}}
  </div>
  <button @click="toggle">Toggle Msg</button>
  <button @click="switchView">Switch View</button>
</template>
<script>
  import { reactive } from 'vue'

  export default {

    setup() {
      const state = reactive({
        msg: 'Hello World',
        showMsg: true
      })

      function toggle() {
        state.msg = state.msg === 'Hello World' ? 'Hello Vue' : 'Hello World'
      }

      function switchView() {
        state.showMsg = !state.showMsg
      }

      return {
        toggle,
        switchView,
        state
      }
    }
  }
</script>

这是黄轶老师在Vue 3.0举的例子,他的解释如下:

结合代码可以知道,这个组件的视图会根据 showMsg 变量的控制显示 msg 或者一个随机数,当我们点击 Switch View 的按钮时,就会修改这个变量值。

假设没有 cleanup,在第一次渲染模板的时候,activeEffect 是组件的副作用渲染函数,因为模板 render 的时候访问了 state.msg,所以会执行依赖收集,把副作用渲染函数作为 state.msg 的依赖,我们把它称作 render effect。然后我们点击 Switch View 按钮,视图切换为显示随机数,此时我们再点击 Toggle Msg 按钮,由于修改了 state.msg 就会派发通知,找到了 render effect 并执行,就又触发了组件的重新渲染。

但这个行为实际上并不符合预期,因为当我们点击 Switch View 按钮,视图切换为显示随机数的时候,也会触发组件的重新渲染,但这个时候视图并没有渲染 state.msg,所以对它的改动并不应该影响组件的重新渲染。

因此在组件的 render effect 执行之前,如果通过 cleanup 清理依赖,我们就可以删除之前 state.msg 收集的 render effect 依赖。这样当我们修改 state.msg 时,由于已经没有依赖了就不会触发组件的重新渲染,符合预期。

有点複杂,不过读多几次就明白cleanUp的作用。

以上就是reactive的主要内容。