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

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

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

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

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

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

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

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

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