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

Posted 若叶岂知秋vip

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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

以上是关于Vue3.0源码系列响应式原理 - Reactivity的主要内容,如果未能解决你的问题,请参考以下文章

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

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

vue3.0的proxy响应式原理简单实现

敲黑板,划重点!!!Vue3.0响应式实现原理 —— proxy()

敲黑板,划重点!!!Vue3.0响应式实现原理 —— proxy()

敲黑板,划重点!!!Vue3.0响应式实现原理 —— proxy()