Vue3.0源码系列(一)响应式原理 - Reactivity

更多 vue3 源码分析尽在:www.cheng92.com/vue

该系列文章,均以测试用例通过为基准一步步实现一个 vue3 源码副本(学习)。

文字比较长,如果不想看文字可直接转到这里看脑图

简介

reactivity 是 vue next 里面通过 proxy + reflect 实现的响应式模块。

源码路径: packages/reactivity

入口文件:packages/reactivity/src/index.ts

疑问点解答:

  1. shallowReactive 相当于浅复制,只针对对象的一级 reactive,嵌套的对象不会 reactive

    参考:测试代码 reactive.spec.ts

    test('should keep reactive properties reactive', () => {
          const props: any = shallowReactive({ n: reactive({ foo: 1 }) })
          props.n = reactive({ foo: 2 })
          expect(isReactive(props.n)).toBe(true)
        })
    

完整的 reactivity 模块代码链接。

阶段代码链接

  1. 测试用例 reactive.spec.ts 通过后的代码链接
  2. 测试用例 effect.spec.ts通过后的代码链接
  3. 05-21号 git pull 后的更新合 并之后的 reactive.js
  4. 将 reactive.js 拆分成 effect.js + baseHandlers.js
  5. 完成 collection handlers(set + get)
  6. 完成 collection Map, Set 支持
  7. 支持 Ref 类型
  8. 支持 computed 属性

文中重点链接

  1. vue 中是如何防止在 effect(fn) 的 fn 中防止 ob.prop++ 导致栈溢出的?
  2. vue 中为何能对 JSON.parse(JSON.stringify({})) 起作用的?
  3. 集合 handlers 的 get 函数实现 this 问题
  4. Key 和 rawKey 的问题(get 中),为什么要两次 track:get?
  5. 为什么 key1 和 toReactive(key1) 后的 key11 前后 set 会改变 key1 对应的值???
  6. 如果 Ref 类型放在一个对象中 reactive 化会有什么结果???
  7. 计算属性的链式嵌套使用输出结果详细分析过程(想要透彻computed请看这里!!!)

遗留问题

  1. DONE ownKeys 代理收集的依赖不能被触发。
  2. TODO Ref:a 类型在对象中执行 obj.a++ 之后依旧是 Ref 类型的 a ???

更新

2020-05-21 21:19:07 git pull

模块结构

  1. __tests__/ 测试代码目录
  2. src/ 主要代码目录

src 目录下的文件:

  1. baseHandler.ts 传入给代理的对象,代理 Object/Array 时使用的 Handlers。
  2. collectionHandlers.ts 传入给代理的对象,代理 [Week]Set/Map类型时使用的 Handlers。
  3. computed.ts 计算属性代码
  4. effect.ts
  5. operations.ts 操作类型枚举
  6. reactive.ts 主要代码
  7. ref.ts

Proxy 和 Reflect 回顾

将 reactive -> createReactiveObject 简化合并:

function reactive(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
  // ... 必须是对象 return

  // ... 已经设置过代理了
  let observed = null

  // ... 本身就是代理

  // ... 白名单检测

  // ... handlers

  // new 代理
  let handlers = baseHandlers || collectionHandlers || {} // ...
  observed = new Proxy(target, handlers)

  // 缓存代理设置结果到 toProxy, toRaw

  return observed
}

增加一个 reactive 对象:

const target = {
  name: 'vuejs'
}

const observed = reactive(target, null, null, {
  get: function (target, prop, receiver) {
    console.log(target, prop, receiver === observed, 'get')
  }
})

console.log(target, observed)
	

输出结果:

{name: “vuejs”} Proxy {name: “vuejs”}

=> original.name
“vuejs”
=> observed.name
index.js:28 true “name” true “get”
undefined
=> observed === original
false

访问 target, observed 的属性 name 结果如上,observed 是被代理之后的对象。

  1. Observed.name 输出结果是 handler.get 执行之后的结果,因为没任何返回所以是 undefined
  2. get(target, prop, receiver) 有三个参数,分别代表
    • target: 被代理的对象,即原始的那个 target 对象
    • prop: 要获取对象的属性值的 key
    • receiver: 代理之后的对象,即 observed

其他主要几个代理方法

  1. set 赋值的时候触发,对应 Reflect.set(target, prop, value)
  2. get 取值的时候触发,对应 Reflect.get(target, prop, reciver)
  3. ownKeys 使用 for...in 时触发,对应 Reflect.ownKeys(target)
  4. has 使用 prop in obj 时触发,对应语法 : ... in ...
  5. deleteProperty 使用 delete obj.name 触发,对应 delete obj.name
  6. apply 被代理对象是函数的时候,通过 fn.apply() 时触发,handler 里对应 fn()
  7. construct 构造器,new target() 时触发
  8. getPrototypeOf 调用 Object.getPrototypeOf(target) 触发,返回对象 或 null
  9. setPrototypeOf 设置对象原型时触发,如: obj.prototype = xxx
let original = {
  name: 'vuejs',
  foo: 1
}

original = test

const observed = reactive(original, null, null, {
  get: function (target, prop, receiver) {
    console.log(target === original, prop, receiver === observed, 'get')

    return Reflect.get(...arguments)
  },
  set: function (target, prop, value) {
    console.log(prop, value, 'set')
    Reflect.set(target, prop, value)
  },
  ownKeys: function (target) {
    console.log('get own keys...')
    return Reflect.ownKeys(target)
  },
  has: function (target, key) {
    console.log('has proxy handler...')
    return key in target
  },
  deleteProperty: function (target, key) {
    console.log(key + 'deleted from ', target)
    delete target[key]
  },
  // 适用于被代理对象是函数类型的
  apply: function (target, thisArg, argList) {
    console.log('apply...', argList)
    target(...argList)
  },
  construct(target, args) {
    console.log('proxy construct ... ', args)
    return new target(...args)
  },
  // 必须返回一个对象或者 null,代理 Object.getPrototypeOf 取对象原型
  getPrototypeOf(target) {
    console.log('proxy getPrototypeOf...')
    return null
  },
  setPrototypeOf(target, proto) {
    console.log('proxy setPrototypeOf...', proto)
  }
})

console.log(observed.name) // -> true "name" true "get"
observed.name = 'xxx' // -> name xxx set
for (let prop in observed) {
} // -> get own keys...
'name' in observed // -> has proxy handler
delete observed.foo // foo deleted from { name: 'xxx', foo: 1 }

function test() {
  console.log(this.name, 'test apply')
}

observed.apply(null, [1, 2, 3]) // apply... (3) [1, 2, 3]
// 注意点:proxy-construct 的第二个参数是传入构造函数时的参数列表
// 就算是以下面方式一个个传递的
new observed(1, 2, 3) // proxy construct ...  (3) [1, 2, 3]
Object.getPrototypeOf(observed) // proxy getPrototypeOf...
observed.prototype = {
  bar: 2
}

// prototype {bar: 2} set
// index.js:31 true "prototype" true "get"
// index.js:90 {bar: 2}
console.log(observed.prototype)

需要注意的点:

  1. construct 的代理 handler 中的第二个参数是一个参数列表数组。
  2. getPrototypeOf 代理里面返回一个正常的对象 或 null表示失败。

reactive 函数

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  // 这里对只读的对象进行判断,因为只读的对象不允许修改值
  // 只要曾经被代理过的就会被存到 readonlyToRaw 这个 WeakMap 里面
  // 直接返回只读版本
  if (readonlyToRaw.has(target)) {
    return target
  }
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

传入一个 target 返回代理对象。

createReactiveObject

真正执行代理的是这个函数里面。

参数列表

  1. target 被代理的对象
  2. toProxy 一个 WeakMap 里面存储了 target -> observed
  3. toRawtoProxy 刚好相反的一个 WeakMap 存储了 observed -> target
  4. baseHandlers 代理时传递给 Proxy 的第二个参数
  5. collectionHandlers 代理时传递给 Proxy 的第二个参数(一个包含四种集合类型的 Set)

函数体

下面是将 reactivecreateReactiveObject 进行合并的代码。

事先声明的变量列表:

// 集合类型的构造函数,用来检测 target 是使用 baseHandlers
// 还是 collectionHandlers
const collectionTypes = new Set([Set, Map, WeakMap, WeakSet])
// 只读对象的 map,只读对象代理时候直接返回原始对象
const readonlyToRaw = new WeakMap()
// 存储一些只读或无法代理的值
const rawValues = new WeakSet()

合并后的 reactive(target, toProxy, toRaw, basehandlers, collectionHandlers) 函数

function reactive(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
  // 只读的对象
  if (readonlyToRaw.has(target)) {
    return target
  }
  // ... 必须是对象 return
  if (target && typeof target !== 'object') {
    console.warn('不是对象,不能被代理。。。')
    return target
  }

  // toProxy 是一个 WeakMap ,存储了 observed -> target
  // 因此这里检测是不是已经代理过了避免重复代理情况
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    console.log('target 已经设置过代理了')
    return observed
  }

  // ... 本身就是代理
  // toRaw 也是一个 WeakMap 存储了 target -> observed
  // 这里判断这个,可能是为了防止,将曾经被代理之后的 observed 传进来再代理的情况
  if (toRaw.has(target)) {
    console.log('target 本身已经是代理')
    return target
  }

  // ...... 这里省略非法对象的判断,放在后面展示 ......

  // 根据 target 类型决定使用哪个 handlers
  // `Set, Map, WeakSet, SeakMap` 四种类型使用 collectionHandlers 集合类型的 handlers
  // `Object, Array` 使用 basehandlers
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers

  // new 代理
  observed = new Proxy(target, handlers)

  // 缓存代理设置结果到 toProxy, toRaw
  toProxy.set(observed, target)
  toRaw.set(target, observed)
  return observed
}
  1. readonlyToRaw.has(target) 检测是否是只读对象,直接返回该对象

  2. 检测 target是引用类型还是普通类型,只有引用类型才能被代理

  3. toProxy 中存储了 target->observed 内容,检测 target 是不是已经有代理了

  4. toRaw 中存储了 observed->target 检测是否已经是代理了

  5. 五种不合法的对象类型,不能作为代理源

    // ... 白名单检测,源码中调用的是 `canObserve` 这里一个个拆分来检测
      // 1. Vue 实例本身不能被代理
      if (target._isVue) {
        console.log('target 是 vue 实例,不能被代理')
        return target
      }
    
      // 2. Vue 的虚拟节点,其实就是一堆包含模板字符串的对象解构
      // 这个是用来生成 render 构建 DOM 的,不能用来被代理
      if (target._isVNode) {
        console.log('target 是虚拟节点,不能被代理')
        return targtet
      }
    
      // 限定了只能被代理的一些对象: 'Object, Array, Map, Set, WeakMap, WeakSet`
      // Object.prototype.toString.call(target) => [object Object] 取 (-1, 8)
      // 其实 `Object` 构造函数字符串
      const toRawType = (target) =>
        Object.prototype.toString.call(target).slice(8, -1)
      if (
        !['Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet'].includes(
          toRawType(target)
        )
      ) {
        console.log(
          `target 不是可代理范围对象('Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet')`
        )
        return target
      }
    
      // 那些被标记为只读或者非响应式的WeakSets的值
      if (rawValues.has(target)) {
        return target
      }
    
      // 被冻结的对象,是不允许任何修改操作的,不可用作响应式对象
      if (Object.isFrozen(target)) {
        return target
      }
    
  6. 根据 target 的类型检测采用哪种类型的 handlers,集合类型使用 collectionhandlers,对象类型采用 baseHandlers

  7. 创建代理 new Proxy(target, handlers)

  8. 缓存代理源及代理结果到 toProxy, toRaw 避免出现重复代理的情况

  9. 返回代理对象 observed

使用 reactive

为了区分两种代理类型(集合类型,普通对象(对象和数组)),这里使用两个对象(setTarget, objTarget),创建两个代理(setObserved, objObserved),分别传入不同的代理 handlers,代码如下:

const toProxy = new WeakMap()
const toRaw = new WeakMap()

const setTarget = new Set([1, 2, 3])
const objTarget = {
  foo: 1,
  bar: 2
}

const setObserved = reactive(setTarget, toProxy, toRaw, null, {
  get(target, prop, receiver) {
    console.log(prop, 'set get...')
    // return Reflect.get(target, prop, receiver)
  },
  // set/map 集合类型
  has(target, prop) {
    const ret = Reflect.has(target, prop)

    console.log(ret, target, prop, 'set has...')
    return ret
  }
})
const objObserved = reactive(
  objTarget,
  toProxy,
  toRaw,
  {
    // object/arary, 普通类型
    get(target, prop, receiver) {
      console.log(prop, 'object/array get...')
      return Reflect.get(target, prop, receiver)
    }
  },
  {}
)

输出代理的结果对象如下:console.log(setObserved, objObserved)

结果:Proxy {1, 2, 3} Proxy {foo: 1, bar: 2}

然后出现了错误,当我试图调用 setObserved.has(1) 的时候报错了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LhGLcgm6-1626260142852)(http://qiniu.ii6g.com/1589614203.png?imageMogr2/thumbnail/!100p)]

获取 setObserved.size 属性报错,不同的是 set proxy handler 有被调用,这里应该是调用 Reflect.get() 时候报错了:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vco8Zuhw-1626260142857)(http://qiniu.ii6g.com/1589614685.png?imageMogr2/thumbnail/!100p)]

google 之后这里有篇文章里给出了问题原因和解决方案

解决方法,在 get proxy handler 里面加上判断,如果是函数就使用 target去调用:

const setObserved = reactive(setTarget, toProxy, toRaw, null, {
  get(target, prop, receiver) {
    switch (prop) {
      default: {
        // 如果是函数,经过代理之后会丢失作用域问题,所以要
        // 重新给他绑定下作用域
        console.log(prop, 'get...')
        return typeof target[prop] === 'function'
          ? target[prop].bind(target)
          : target[prop]
      }
    }
  },
 

结果:

Proxy {1, 2, 3} Proxy {foo: 1, bar: 2}
-> setObserved.has(1)
has get…
true

baseHandlers.ts

这个文件模块出现了几个 handlers 是需要弄清楚的,比如:

baseHandlers.ts 里面和 Array, Object 有关的四个:

  1. mutableHandlers
  2. readonlyHandlers
  3. shallowReactiveHandlers,
  4. shallowReadonlyHandlers

collectionHandlers.ts 里和集合相关的两个:

  1. mutableCollectionHandlers
  2. readonlyCollectionHandlers

在上一节讲过 createReactiveObject 需要给出两个 handlers 作为参数,一个是针对数组和普通对象的,另一个是针对集合类型的。

下面分别来看看两个文件中分别都干了什么???

列出文件中相关的函数和属性:

属性:

// 符号集合
const builtInSymbols = new Set(/* ... */);
// 四个通过 createGetter 生成的 get 函数
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)

// 三个数组函数 'includes', 'indexOf', 'lastIndexOf'
const arrayInstrumentations: Record<string, Function> = {}

// setter
const set = /*#__PURE__*/ createSetter()
const shallowSet = /*#__PURE__*/ createSetter(true)

函数:

// 创建 getter 函数的函数
function createGetter(isReadonly = false, shallow = false) { /* ... */ }

// 创建 setter 函数的函数
function createSetter(shallow = false) { /* ... */ }

// delete obj.name 原子操作
function deleteProperty(target: object, key: string | symbol): boolean { 	/*...*/ 
}

// 原子操作 key in obj
function has(target: object, key: string | symbol): boolean { /* ... */ }

// Object.keys(target) 操作,取对象 key
function ownKeys(target: object): (string | number | symbol)[]  {/*...*/}

四个要被导出的 handlers

export const mutableHandlers: ProxyHandler<object> = {/*...*/}
export const readonlyHandlers: ProxyHandler<object> = {/*...*/}
export const shallowReactiveHandlers: ProxyHandler<object> = {/*...*/}
export const shallowReadonlyHandlers: ProxyHandler<object> = {/*...*/}

接下来一个个来分析分析,看看每个都有什么作用???

先从 createGetter 说起吧 ->

为了下面方便调试,对上面的 reactive() 进行了简化,只保留了与 handlers 有关的部分:

const collectionTypes = new Set([Set, Map, WeakMap, WeakSet])

function reactive(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
  // 简化
  if (typeof target !== 'object') return target

  //... isVue, VNode...

  let observed = null

  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers

  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}

const toProxy = new WeakMap(),
  toRaw = new WeakMap()

createGetter(isReadonly = false, shallow = false)

参数:

  1. isReadonly = false
  2. shallow = false

简化之后的 createGetter,先用它来创建一个 get 然后创建一个 baseHandler: mutableHandlers 可变的 handlers

{
  // 很明显这个 proxy handler get, 简化之后...
  return function get(target, key, receiver) {
    const res = Reflect.get(...arguments)
    // ... 省略1,如果是数组,且是 includes, indexOf, lastIndexOf 操作
    // 直接返回它对应的 res
    // ... 省略2,如果是符号属性,直接返回 res

    // ... 省略3, 浅 reactive,不支持嵌套

    // ... 省略4,isRef 类型,判断是数组还是对象,数组执行 track(...), 对象返回 res.value

    // 非只读属性,执行 track(),收集依赖
    !isReadonly && track(target, 'get', key)

    console.log(res, key, 'get...')
    // return res
    // 非对象直接返回原结果,如果是对象区分只读与否
    return typeof res === 'object' && res !== null
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          res // ... readonly(res)
        : reactive(res, toProxy, toRaw, mutableHandlers)
      : res
  }
}

上面我们省略了暂时不关心的是哪个部分:

  1. 数组类型且 key 是 ['includes', 'indexOf', 'lastIndexOf'] 其中任一一个
  2. 符号属性处理
  3. ref 类型处理

目前我们只关心如何创建 get 和一个最简单的 basehandler: mutableHandler

使用 createGetter: get

// 示例 1
const objTarget = {
  foo: 1,
  bar: { 
    name: 'bar'
  }
}

// 将 createGetter 生成的 get -> mutableHandlers 传入 reactive
const objObserved = reactive(objTarget, toProxy, toRaw, mutableHandlers)

这里 get 我认为只有两个目的:

递归 reactive,就在最后返回的时候检测 res 结果时候

这里我们首先来验证下递归 reactive 问题,即当我们访问对象中嵌套对象里面的属性时候,实际上是不会触发 get 的,我们在 createGetterreturn 前面加上一句 return res

也就是说不检测结果是不是对象,而直接返回当前取值的结果:

=> objObserved.foo
“foo” “get…”
1
=> objObserved.bar
{name: “bar”} “bar” “get…”
{name: “bar”}
{name: “bar”} “bar” “get…”
=> objObserved.bar.name
{name: “bar”} “bar” “get…”
“bar”
=> const bar = objObserved.bar
{name: “bar”} “bar” “get…”
undefined
=> bar.name
“bar”

分析上面的测试结果:

  • objObserved.foo 直接取对象的成员值,触发了 proxy get
  • objObserved.bar 取对象的对象成员,触发了 proxy get
  • objObserved.bar.name 取嵌套对象的成员,触发了 proxy get但请注意实际上触发 get 的是 objObserved.bar 得取值过程,因为输出的 res{name: "bar"},也就是说取 bar.namename时候实际并没有触发 proxy get,这说明 proxy get 只能代理一级。
  • 为了证明代理只能代理一级,下面通过 bar = objObserved.bar 再去取 bar.name 就很明显并没有触发 proxy get

通过上面的分析,这也就是为什么要在 return 的时候去检测是不是对象,如果是对象需要进行递归 reactive的动作。

那么,我们将 return res 注释掉再来看看结果如何:

=> objObserved.foo
1 “foo” “get…”
1
=> objObserved.bar
{name: “bar”} “bar” “get…”
Proxy {name: “bar”}
=> objObserved.bar.name
{name: “bar”} “bar” “get…”
bar name get…
“bar”
=> const bar = objObserved.bar
{name: “bar”} “bar” “get…”
bar.name
=> bar name get…
“bar”

看到差异没,首先从 objObserved.bar.name 就可看出差异了,这里首先触发的实际是 objObserved.barproxy get,此时 return 的时候发现结果是个对象,因此将 bar 传入 reactive(bar) 进一步代理,完成之后取 bar.name 的时候 bar 已经是 reactive 对象了,因此就在 {name: “bar”} “bar” “get…” 后面紧跟着出现了bar name get… 输出。

此时,无论后面是赋值到变量 bar 再取 bar.name 结果一样会触发对应的 proxy get,毕竟对象是引用类型,类似指针一样,新增了一个变量指向它,它依旧在哪里。

到此,最基本的 proxy get 响应式也完成了,并且能做到嵌套对象的 reactive 化,感觉相比 vue3 之前的通过 defineProperty 实现更加清晰容易理解。

收集依赖(track)

既然有了响应式数据,那么接下来的重点就是如果利用其特性为我们做点事情,但是它又如何知道为我们做什么的,这个时候就有了所谓的“收集依赖”。

“收集依赖”就是在 get 取值期间发生的,也就是 createGetter 中的 track() 调用时触发了依赖收集动作。

track() 相关的代码在 effect.ts 中:

函数定义:

export function track(target: object, type: TrackOpTypes, key: unknown){}

有三个参数:

  1. target:proxy get 时候传递给 proxy 的那个对象
  2. type: 要 track 的类型,有三种: get, has,iterate,分别是取值,检测属性存在性,以及迭代时。
  3. Key: 针对 target 对象里面的属性,收集依赖到 targetMap -> depsMap -> dep:Set

简化 track(target, type)代码:

// trackType -> get, has, iterate
function track(target, type, key) {
  // ...省略1 检测 shouldTrack 和 activeEffect 标记

  // 取 target 自己的依赖 map ,如果没有说明是首次,需要给它创建一个
  // 空的集合,这里使用 Map 而不是 WeakMap,为的是强引用,它涉及到
  // 数据的更新触发 UI 渲染,因此不该使用 WeakMap,否则可能会导致依赖丢失问题
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  // 接下来对 key 取其依赖
  // 如果属性的依赖不存在,说明该对象是首次使用,需要创建其依赖库
  // 且这里使用了 `Set` 是为了避免重复注册依赖情况,避免数据的更新导致重复触发
  // 同一个 update 情况
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }

  // 注册实际的 update: activeEffect 操作
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
  }
}

代码实现主要有三个过程:

  1. 检测全局的 targetMap 中是不是有 target 自己的依赖仓库(Map)
  2. 检测 depsMap = targetMap.get(target) 中是不是有取值 key 对应的依赖集合 dep
  3. 注册 activeEffect对象,然后将当前 target-key-dep 注册到 activeEffect,然后发现每个 activeEffect会有自己的 deps 保存了所有对象 key 的依赖。

收集依赖的过程如图:,执行取值 activeEffect.deps 中就会新增一个 Set

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZERAwjuj-1626260142859)(http://qiniu.ii6g.com/1589694976.png?imageMogr2/thumbnail/!100p)]

到这里,依赖收集算是完成,但并不是很明白 activeEffect 具体是做什么的???

既然依赖收集,要搞明白 activeEffect 是做什么的,估计的从 set 入手了,下面来实现 set 从而完成一个完整的 get -> dep -> set -> update 的过程。

go on…

createSetter(shallow = false)

源码简化版:

function createSetter(shallow = false) {
  // 标准的 proxy set
  return function set(target, key, value, receiver) {
    // 取旧值
    const oldValue = target[key]

    // 先不管 shallow mode

    // 还记得 reactive 里面的 toRaw啊,对象这里就是取出
    // value 的原始对象 target,前提是它有 reactive() 过
    // 才会被存入到 toRaw: observed -> target 中
    // 暂时简化成: toRaw.get(value)
    value = toRaw.get(value)

    // ... 省略,ref 检测

    const hadKey = hasOwn(target, key)
    // 先执行设置原子操作
    const result = Reflect.set(target, key, value, receiver)

    // 只有对象是它自身的时候,才触发 dep-update(排除原型链)
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 新增属性操作
        trigger(target, 'add', key, value)
      } else if (hasChanged(value, oldValue)) {
        // 值改变操作,排除 NaN !== NaN 情况
        trigger(target, 'set', key, value, oldValue)
      }
    }

    return result
  }
}

这里主要有几个操作:

  1. shallow mode 检测,已省略。
  2. value = toRaw(value) 如果 value 是 observed,那么可以通过 toRaw 取出被代理之前的对象 target,还记得 reactive() 里面的那个 toRaw, toProxy 缓存操作吧。
  3. 调用 Reflect.set() 先将值设置下去,然后再考虑是否触发依赖
  4. 检测对象原型链,只有当对象是自身的时候才触发依赖
  5. 触发的行为只有两种要么是新增属性(add),要么是更改值(set, 值不变的情况不触发)

这里有个与 createGetter 里面收集依赖 (track())对应的触发依赖函数: trigger

接下来就是要看看 trigger() 里面都做了啥。

function trigger(target, type, key, newValue, oldValue, oldTarget) {
  // step1: 检测是否被 track 过,没有根本就没有依赖
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  // step2: 将 dep 加入到 effects
  // 创建两个 effects, 一个普通的,一个计算属性
  const effects = new Set()
  const computedRunners = new Set()
  // 根据 effect 的选项 computed 决定是添加到那个 Set 中
  const add = (effectsToAdd) =>
    effectsToAdd.forEach(
      (effect) =>
        (effect !== activeEffect || !shouldTrack) &&
        (effect.options.computed
          ? computedRunners.push(effect)
          : effects.push(effect))
    )

  // if ... clear
  if (false) {
    // TODO 清空动作,触发所有依赖
  }
  // 数组长度变化
  else if (false) {
    // TODO 触发更长度变化有关的所有依赖
  } else {
    // 例如: SET | ADD | DELETE 操作
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    const isAddOrDelete =
      type === 'add' || (type === 'delete' && !Array.isArray(target))
    if (isAddOrDelete || (type === 'set' && target instanceof Map)) {
      // 删除或添加操作,或者 map 的设置操作
      add(depsMap.get(Array.isArray(target) ? 'length' : ITERATE_KEY))
    }

    // Map 的添加或删除操作
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  // step3: 执行 effects 中所有的 dep

  const run = (effect) => {
    // 选项提供了自己的调度器,执行自己的
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // 触发应该触发的依赖
  computedRunners.forEach(run)
  effects.forEach(run)
}

主要有三个步骤:

  • step1: 检测是否收集过依赖,如果没有说明可能没有被用过,没什么可触发的
  • step2: 主要是过滤收集到依赖,针对当前更改操作的所有依赖触发(add)
  • step2: 经过第二步的依赖过滤之后,触发所有的依赖(run)

这里面有两个重要的属性(effects,computedRunners)和两个函数(add,run)

add: 过滤,run: 执行。

很明显,到这里,我们还是没有解决,依赖对应的 update 是如何收集的问题,因为 set 也只是将已经收集好 dep 执行而已。

effect.ts

该文件中主要包含三个重要函数:

  1. trigger(target, type, key?, newValue?, oldValue?, oldTarget?) 触发依赖函数
  2. effect->createReactiveEffect(fn, options) 转换依赖函数成ReactiveEffect类型,并且立即执行它。
  3. track(target, type, key)

以及一些辅助函数:

  1. isEffect() 检测是不是 ReactiveEffect 类型
    isEffect = fn => fn?._isEffect === true

  2. stop(effect: ReactiveEffect)
    停止 effect ,如果选项中提供了 onStop 监听该动作,执行它,重置 effect.active。

    export function stop(effect: ReactiveEffect) {
      if (effect.active) {
        cleanup(effect)
        if (effect.options.onStop) {
          effect.options.onStop()
        }
        effect.active = false
      }
    }
    
  3. cleanup(effect: ReactiveEffect)

    // 在 track 的时候,加入 effect 时,对其做一次清理工作
    // 保证 effect.deps 干净
    function cleanup(effect: ReactiveEffect) {
      const { deps } = effect
      if (deps.length) {
        for (let i = 0; i < deps.length; i++) {
          deps[i].delete(effect)
        }
        deps.length = 0
      }
    }
    
  4. pauseTracking()

    // 暂停 track 动作
    export function pauseTracking() {
      trackStack.push(shouldTrack)
      shouldTrack = false
    }
    
  5. enableTracking()

    // 恢复 track 动作
    export function enableTracking() {
      trackStack.push(shouldTrack)
      shouldTrack = true
    }
    
  6. resetTracking()

    // 重置 track,可能 fn 执行失败了,try ... finally ... 丢弃 fn:effect 时候调用
    export function resetTracking() {
      const last = trackStack.pop()
      shouldTrack = last === undefined ? true : last
    }
    

包含的属性变量:

// 保存着 target 对象的所有依赖的 Map <target, dep<Set>>
// target -> Map<key, dep[]>
const targetMap = new WeakMap<any, KeyToDepMap>()
// effect 栈,保存所有的 fn->effect
const effectStack: ReactiveEffect[] = []
// 当前激活状态的 effect
let activeEffect: ReactiveEffect | undefined

export const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
export const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')

// 执行 effect 时,uid++,即每个 effect 都会有自己的唯一的 uid
let uid = 0

// 记录当前 effect 的状态,
let shouldTrack = true
// 当前 effect -> shouldTack
// 每增加一个 effect 记录 shouldTrack = true, push 到 trackStack
// 如果 effect.raw<fn> 执行异常会 pop 掉,还原 shouldTrack -> last, 
// pop trackStack
const trackStack: boolean[] = []

一直到这里我们基本完成了 reactive->get->set->track->trigger->effect 一系列动作,

也该我们测试的时候了,按正常应该会有我们想要的结果,响应式->注册fn:update->取值收集依赖-> 设置触发 fn:udpate 调用

=>>>>>>>>>

比如:

const r = (target) => reactive(target, toProxy, toRaw, mutableHandlers)

const fn = () => console.log('effect fn')
let res = effect(fn, {})
console.log(Object.keys(res), 'after effect')

let dummy
const counter = r({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy, 'before')
counter.num = 7
console.log(dummy, 'after')

上面的例子运行之后,并没有得到我们想要的结果!!!

effect fn
[“id”, “_isEffect”, “active”, “raw”, “deps”, “options”] “after effect”
0 “num” “get…”
0 “before”
0 “after”

按照我们的实现,理论上 after 的结果应该是 7 才对,但结果显示依然是 0,这说明了我们调用 effect(fn) 并没有与上面的 r({ num: 0 }) 发生任何联系,即 fn 并没有被收集到 counter.num 的依赖 deps 中去,那这是为什么呢???


我们来回顾分析下之前所作工作的整个过程(reactive->get->set->track->trigger->effect):

  • reactive 将数据通过 proxy 转成响应式
  • get->track 收集依赖,相关属性:targetMap, depsMap, dep, activeEffect, activeEffect.deps。
  • set->trigger 触发依赖 update 函数,涉及到的 targetMap, depsMap, add, run
  • effect 将 update 函数,转换成 ReactiveEffect 类型

纵观这整个过程,尤其是 get->trackset->trigger -> effect 收集,触发和 effect 三个过程,唯一有可能让他们发生联系的应该就是这个 activeEffect 模块域里的变量,标识着当前处于激活状态的 effect,它的使用几乎贯穿了整个过程(track->trigger->effect,这三个函数也都在 effect.ts 中实现)。

那么接下来…

前面都是简化之后的,现在看看完整的这三个函数实现:

track(target, type, key)

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}

trigger(…)

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }

  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || !shouldTrack) {
          if (effect.options.computed) {
            computedRunners.add(effect)
          } else {
            effects.add(effect)
          }
        } else {
          // the effect mutated its own dependency during its execution.
          // this can be caused by operations like foo.value++
          // do not trigger or we end in an infinite loop
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    const isAddOrDelete =
      type === TriggerOpTypes.ADD ||
      (type === TriggerOpTypes.DELETE && !isArray(target))
    if (
      isAddOrDelete ||
      (type === TriggerOpTypes.SET && target instanceof Map)
    ) {
      add(depsMap.get(isArray(target) ? 'length' : ITERATE_KEY))
    }
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run)
  effects.forEach(run)
}

effect(fn, options)

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}


function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    if (!effect.active) {
      return options.scheduler ? undefined : fn(...args)
    }
    if (!effectStack.includes(effect)) {
      cleanup(effect)
      try {
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn(...args)
      } finally {
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

对比三个函数

过程 shouldTrack/activeEffect
track if (!shouldTrack || activeEffect === undefined) return
trigger add 里面有个判断:if (!shouldTrack || effect !== activeEffect)`才会继续往下执行添加操作
effect effectStack.push(effect)
activeEffect = effect
// enable tracking
trackStack.push(shouldTrack)
shouldTrack = true

对下面测试代码逐行分析:

let dummy
const counter = r({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy, counter, 'before')
counter.num = 7
console.log(dummy, 'after')

  1. const counter = r({sum: 0})
    这里将 { sum: 0 } reactive 代理之后赋值给了 counter 也就是说这个 counter 是个 Proxy:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-48A4P2ZR-1626260142863)(http://qiniu.ii6g.com/1589705626.png?imageMogr2/thumbnail/!100p)]

  2. effect(() => (dummy = counter.num))
    在这里调用 effect(fn) 注册了一个 updater,里面用到了 counter.num 那么就会触发 counter.numproxy get,然后会触发 track() 收集依赖:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bhhc9QmV-1626260142866)(http://qiniu.ii6g.com/1589705890.png?imageMogr2/thumbnail/!100p)]
    并且我们从图中结果可知, fn 实际被立即执行了一次,这是 effect 函数里面的操作。
    按预期,这里的 fn 应该会被收集到 counter.num 的 deps 中。
    我们在 track() 最后加上打印

    if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
        activeEffect?.deps?.push(dep)
        console.log(dep, activeEffect.deps)
      }
    

    结果:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bnvmTwXC-1626260142867)(http://qiniu.ii6g.com/1589706174.png?imageMogr2/thumbnail/!100p)]

    即,activeEffect.deps 以及收集到了 counter.num 的依赖: Map(1) {"num" => Set(1)}
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-67nQkoXx-1626260142869)(http://qiniu.ii6g.com/1589706408.png?imageMogr2/thumbnail/!100p)]

  3. console.log(dummy, counter, 'before')
    经过上面的结果分析,在第2步的时候,确实已经收集到了 counter.num 的 fn:updater,且存放到了 targetMap -> despMap -> num:Set(1) 中。
    因此这里的输出内容是: 0 “num” “get…” 没什么毛病,那继续往下,问题或许处在设置的时候???

  4. counter.num = 7
    最后发现问题所在,原始是个超级低级的问题(捂脸~~,没脸见人~~~)。
    没有创建 set handler 并添加到 mutableHandlers 里面。
    只要添加两句:
    const set = createSetter()
    然后:
    const mutableHandlers = { get, set }
    就能得到我们想要的结果。

  5. console.log(dummy, 'after')
    最后看下最终输出:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlUJIiCr-1626260142870)(http://qiniu.ii6g.com/1589707939.png?imageMogr2/thumbnail/!100p)]

    1 effect(() => (dummy = counter.num))取值时 proxy get 里面的输出

    2: 设置值为 7 之前的输出

    3: 设置值当中的输出
    4: 最后一个log取值 proxy get 的输出
    5: 最后 log 的输出内容

虽然犯了个非常低级的错误,但也正因为这个低级错误,促使自己一步步的去跟踪 get->track, set->trigger, effect 整个过程,从而了解了依赖收集,updater 触发原理。

小结 1

到此一个比较完整的响应式代码也算告一段落,这里贴一下简化后可运行的完整代码(reactive.js)如下:

const hasChanged = (value, oldValue) =>
  value !== oldValue && (value === value || oldValue === oldValue)
const __DEV__ = false
let shouldTrack = true
const ITERATE_KEY = Symbol(__DEV__ ? 'iterate' : '')
const MAP_KEY_ITERATE_KEY = Symbol(__DEV__ ? 'Map key iterate' : '')
const effectStack = []
const trackStack = []
let uid = 0
const reactiveToRaw = new WeakMap()
const rawToReactive = new WeakMap()

// baseHandlers.ts start
const get = createGetter()
const set = createSetter()

// 存放目标依赖的 map: target -> depsMap
// 一个目标,有自己的一个 map 存放依赖
const targetMap = new WeakMap()
let activeEffect = {
  _isEffect: true,
  id: 0,
  active: false,
  raw: null,
  deps: [],
  options: {}
}

function toRaw(observed) {
  return reactiveToRaw.get(observed) || observed
}

function effect(fn, options = {}) {
  // 如果是个 activeEffect 类型,那么其执行函数应该是 fn.raw
  if (fn?._isEffect === true) {
    fn = fn.raw
  }

  // 接下来要创建一个 effect
  const _effect = function reactiveEffect(...args) {
    if (!_effect.active) {
      // 非激活状态
      return options.scheduler ? undefined : fn(...args)
    }

    if (!effectStack.includes(_effect)) {
      // 如果栈中不包含当前的 effect,即没有注册过该 effect
      // 注册过就不需要重复注册了
      // 添加前先执行清理工作 cleanup -> effect.deps[i].delete(effect)

      try {
        shouldTrack = true
        effectStack.push(_effect)
        activeEffect = _effect
        return fn(...args)
      } finally {
        // fn 执行异常了,移除对应的 effect
        effectStack.pop()
        const last = trackStack.pop()
        // 还原状态值
        shouldTrack = last === undefined ? true : last
        // 还原当前激活的 effect
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }

  _effect.id = uid++
  _effect._isEffect = true
  _effect.active = true
  _effect.raw = fn
  _effect.deps = []
  _effect.options = options

  if (!options.lazy) {
    _effect()
  }

  return _effect
}

function trigger(target, type, key, newValue, oldValue, oldTarget) {
  // step1: 检测是否被 track 过,没有根本就没有依赖
  const depsMap = targetMap.get(target)
  if (!depsMap) return

  // step2: 将 dep 加入到 effects
  // 创建两个 effects, 一个普通的,一个计算属性
  const effects = new Set()
  const computedRunners = new Set()
  // 根据 effect 的选项 computed 决定是添加到那个 Set 中
  const add = (effectsToAdd) => {
    effectsToAdd?.forEach(
      (effect) =>
        (effect !== activeEffect || !shouldTrack) &&
        (effect.options.computed
          ? computedRunners.add(effect)
          : effects.add(effect))
    )
  }

  // if ... clear
  if (false) {
    // TODO 清空动作,触发所有依赖
  }
  // 数组长度变化
  else if (false) {
    // TODO 触发更长度变化有关的所有依赖
  } else {
    // 例如: SET | ADD | DELETE 操作
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    const isAddOrDelete =
      type === 'add' || (type === 'delete' && !Array.isArray(target))
    if (isAddOrDelete || (type === 'set' && target instanceof Map)) {
      // 删除或添加操作,或者 map 的设置操作
      add(depsMap.get(Array.isArray(target) ? 'length' : ITERATE_KEY))
    }

    // Map 的添加或删除操作
    if (isAddOrDelete && target instanceof Map) {
      add(depsMap.get(MAP_KEY_ITERATE_KEY))
    }
  }

  // step3: 执行 effects 中所有的 dep

  const run = (effect) => {
    // 选项提供了自己的调度器,执行自己的
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // 触发应该触发的依赖
  computedRunners.forEach(run)
  effects.forEach(run)
}

// trackType -> get, has, iterate
function track(target, type, key) {
  if (!shouldTrack || activeEffect === undefined) return
  // ...省略1 检测 shouldTrack 和 activeEffect 标记

  // 取 target 自己的依赖 map ,如果没有说明是首次,需要给它创建一个
  // 空的集合,这里使用 Map 而不是 WeakMap,为的是强引用,它涉及到
  // 数据的更新触发 UI 渲染,因此不该使用 WeakMap,否则可能会导致依赖丢失问题
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  // 接下来对 key 取其依赖
  // 如果属性的依赖不存在,说明该对象是首次使用,需要创建其依赖库
  // 且这里使用了 `Set` 是为了避免重复注册依赖情况,避免数据的更新导致重复触发
  // 同一个 update 情况
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }

  // 注册实际的 update: activeEffect 操作
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect?.deps?.push(dep)
  }
}
function createGetter(isReadonly = false, shallow = false) {
  // 很明显这个 proxy handler get, 简化之后...
  return function get(target, key, receiver) {
    const res = Reflect.get(...arguments)
    // ... 省略1,如果是数组,且是 includes, indexOf, lastIndexOf 操作
    // 直接返回它对应的 res
    // ... 省略2,如果是符号属性,直接返回 res

    // ... 省略3, 浅 reactive,不支持嵌套

    // ... 省略4,isRef 类型,判断是数组还是对象,数组执行 track(...), 对象返回 res.value

    // 非只读属性,执行 track(),收集依赖
    !isReadonly && track(target, 'get', key)

    console.log(res, key, 'get...')
    // return res
    // 非对象直接返回原结果,如果是对象区分只读与否
    return typeof res === 'object' && res !== null
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          res // ... readonly(res)
        : reactive(res, toProxy, toRaw, mutableHandlers)
      : res
  }
}

function createSetter(shallow = false) {
  // 标准的 proxy set
  return function set(target, key, value, receiver) {
    // 取旧值
    const oldValue = target[key]

    // 先不管 shallow mode

    // 还记得 reactive 里面的 toRaw啊,对象这里就是取出
    // value 的原始对象 target,前提是它有 reactive() 过
    // 才会被存入到 toRaw: observed -> target 中
    // 暂时简化成: toRaw.get(value)
    value = toRaw(value)

    // ... 省略,ref 检测
    console.log(target, key, value, reactiveToRaw, 'set')

    const hadKey = Object.hasOwnProperty(target, key)
    // 先执行设置原子操作
    const result = Reflect.set(target, key, value, receiver)

    // 只有对象是它自身的时候,才触发 dep-update(排除原型链)
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        // 新增属性操作
        trigger(target, 'add', key, value)
      } else if (hasChanged(value, oldValue)) {
        // 值改变操作,排除 NaN !== NaN 情况
        trigger(target, 'set', key, value, oldValue)
      }
    }

    return result
  }
}

const mutableHandlers = {
  get,
  set
}
// baseHandlers.ts end

const collectionTypes = new Set([Set, Map, WeakMap, WeakSet])

function reactive(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
  // 简化
  if (typeof target !== 'object') return target

  //... isVue, VNode...

  let observed = null

  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers

  observed = new Proxy(target, handlers)
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  return observed
}

const r = (target) =>
  reactive(target, rawToReactive, reactiveToRaw, mutableHandlers)

const fn = () => console.log('effect fn')
let res = effect(fn, {})
console.log(Object.keys(res), 'after effect')

// 使用示例
let dummy
const counter = r({ num: 0 })
effect(() => (dummy = counter.num))
console.log(dummy, counter, 'before')
counter.num = 7
console.log(dummy, counter, 'after')

核心函数:

函数名 功能
createGetter->get 创建 proxy 的 get handler,里面会调用 track 收集依赖
createSetter->set 创建 proxy 的 set handler,里面会调用 trigger 触发 targetMap>depsMap>dep:Set依赖执行
track(target, type, key) 收集 target 对象或 target[key] 属性的依赖
trigger(target, type, key?, newValue?, oldValue?, oldTarget?) 触发 target 对象的依赖调用
effect(fn, options) 注册reactive属性的updater

涉及到的核心属性:

ReactiveEffect 类型定义:

export interface ReactiveEffect<T = any> {
  (...args: any[]): T
  _isEffect: true
  id: number
  active: boolean
  raw: () => T
  deps: Array<Dep>
  options: ReactiveEffectOptions
}
属性名 类型 作用
activeEffect ReactiveEffect 记录当前的 effect,在 effect()注册updater的时候置为当前的 RE,在 get->track 里面添加到 targetMap->depsMap->dep 中,且同时更新自己的 activeEffect.deps.push(dep)
effectStack Array<ReactiveEffect> 存放所有的 ReactiveEffect 的数组,也就是说页面中所有的 updater<ReactiveEffect> 都是存在这里面。但是每个 updater 执行完之后就会被移出 effectStack,因为 efffect()调用里面有个 try...finally 无论结果如何都会被 pop 掉。
shouldTrack Boolean 用来追踪当前 effect->activeEffect 的状态
trackStack Array<Boolean> 用来存放当前 effect 的 shouldTrack 状态值
targetMap WeakMap 存放被 reactive 对象依赖的 Map,即:每个 target 在 targetMap 里面有自己的一个 depsMap,里面以 target => <key, Set> 形式存在,key 表示 target 上的一个属性键,Set 存放了该 key 的所有依赖 dep。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QqSGePh7-1626260142871)(http://qiniu.ii6g.com/1589709260.png?imageMogr2/thumbnail/!100p)]层级关系:targetMap:WeakMap -> depsMap:Map -> dep:Set
depsMap Map target 对象里所有属性和其依赖对应的关系集合,如:counter.num 的依赖: { "num" => Set(1) }
reactiveToRaw WeakMap 作为 reactive 的第三个参数 toRaw,保存了 observed->target 关系的 WeakMap。
rawToReactive WeakMap 作为 reactive 的第二个参数 toProxy,保存了 target->observed 关系的 WeakMap,和 reactiveToRaw 刚好相反。
uid Number 每个 effect 都有一个唯一的 id,一直递增。

支持数组 reactive

在这之前都是在对象基础上做的测试,并没有增加数组的支持,比如:jest(所有测试用例都来自官方仓库) ->

test('嵌套的 reactives', () => {
    const original = {
      nested: {
        foo: 1
      },
      array: [{ bar: 2 }]
    }

    const observed = reactive(original)
    expect(isReactive(observed.nested)).toBe(true)
    expect(isReactive(observed.array)).toBe(true)
    expect(isReactive(observed.array[0])).toBe(true)
  })

测试结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W7SPatqf-1626260142875)(http://qiniu.ii6g.com/1589852337.png?imageMogr2/thumbnail/!100p)]

也就是说做到现在,并不支持数组的 reactive,这也是这节将要完善的点。

  1. 数组三个方法(includes, indexOf, lastIndexOf)的依赖收集:

    // 数组三个方法的处理
    const arrayInstrumentations = {}
    // 兼容数组三个索引方法,收集他们相关的依赖
    ;['includes', 'indexOf', 'lastIndexOf'].forEach((key) => {
      arrayInstrumentations[key] = function (...args) {
        const arra = toRaw(this)
        for (let i = 0, l = this.length; i < l; i++) {
          track(arr, 'get', i + '')
        }
    
        // 使用原始方法执行一次(有可能是 reactive 的)
        const res = arr[key](...args)
        if (res === -1 || res === false) {
          // 如果结果失败,使用原始方法再执行一次
          return arr[key](...args.map(toRaw))
        } else {
          return res
        }
      }
    })
    
  2. createGetter -> get 的时候增加数组支持:

    function createGetter(isReadonly = false, shallow = false) {
      return function get(target, key, receiver) {
        const targetIsArray = Array.isArray(target)
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
          return Reflect.get(arrayInstrumentations, key, receiver)
        }
    
        // ...省略
      }
    }
    

    到这里,我们已经可以正常收集到数组的依赖了,测试代码:

    <script type="module">
        import { reactive, effect, targetMap } from './packages/reactive.js'
        let n
        let arr = ['vue', 'reactive']
        const observed = reactive(arr)
        effect(() => (n = observed[0]))
      	// 这里还可以添加多个依赖,比如:effect(() => (m = observed[0]))
      	// 这样,targetMap>depsMap:arr>dep 里面就会有两个了 [f, f]
        console.log({n, targetMap})
    </script>
    

    输出结果:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NHwB2DPH-1626260142876)(/Users/simon/Library/Application Support/typora-user-images/image-20200519095740412.png)]

    • effect(() => (n = observed[0]))会执行一次 fn ,即取了一次数组的 0 下标值,触发了 get
    • 检测到是数组进入数组依赖收集程序arrayInstrumentations ,触发 track 收集依赖

上一篇:“自动获取IP地址”和“使用固定IP地址”的区别是什么?


下一篇:利用DHCP自动分配IP地址 任务1:基于Windows Server 2012的DHCP的实现和应用