Vue3源码阅读指南——响应式数据

写在前面

最近一直在读Vue3.0的源代码,也给Vue3.0贡献了一些代码,因此想开个坑记录一下自己阅读Vue3.0源代码的一些心得,供大家参考。
本篇文章主要会对响应式数据部分(reactivity)进行解读和阐释。

在vue3.0的源代码中,源码集中在packages目录下,其响应式数据的源代码集中在packages/reactivity目录下。

先导知识1——Vue2响应式数据

我们都知道vue2的响应式数据是通过Object.definePropery实现的。

Object.defineProperty(obj, prop, descriptor)
obj:要在其上定义属性的对象。
prop:要定义或修改的属性的名称。
descriptor:将被定义或修改的属性描述符。

descriptor具有以下两种可选值:
get:给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象。

set:给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

举个例子:

<body>
<div id="app">
  <input type="text" id="txt">
  <p id="show"></p>
</div>

<script>
  let obj = {
     };

  Object.defineProperty(obj, 'txt', {
     
    get: function () {
     
      return obj
    },
    set: function (newValue) {
     
      document.getElementById('txt').value = newValue;
      document.getElementById('show').innerHTML = newValue;
    }
  });
  document.addEventListener('keyup', function (e) {
     
    obj.txt = e.target.value;
  })
</script>
</body>

当input出发keyup事件时,obj的txt属性被重新设置,此时会被set descriptor捕获到,并且更改其值,达到响应式的效果。

先导知识2——Proxy

但是在Vue3里面,用的并不是defineProperty而是Proxy。

let p = new Proxy(target, handler);
target:用Proxy包装的目标对象(可以是任何类型的对象)。
handler:一个对象,其属性是当执行一个操作时定义代理的行为的函数,其可以定义的行为有以下几种。

1、get	获取某个key值
2、set	设置某个key值
3、has	使用in操作符判断某个key是否存在
4、apply	函数调用,仅在代理对象为function时有效
5、ownKeys	获取目标对象所有的key
6、construct	函数通过实例化调用,仅在代理对象为function时有效
7、isExtensible	判断对象是否可扩展,Object.isExtensible的代理
8、deleteProperty	删除一个property
9、defineProperty	定义一个新的property
10、getPrototypeOf	获取原型对象
11、setPrototypeOf	设置原型对象
12、preventExtensions	设置对象为不可扩展
13、getOwnPropertyDescriptor	获取一个自有属性 (不会去原型链查找) 的属性描述

举个例子:

let obj = {
     }

const  test = new Proxy(obj, {
     
  get(target, name){
     
    console.log("Get.")
  },
  set(target, key, value) {
     
    console.log("Set.")
  },
  defineProperty(target, prop, descriptor) {
     
    console.log("define.")
  },
  deleteProperty(target, prop) {
     
    console.log("delete.")
  },
});

可以看到Proxy从功能上来讲完全能够替代defineProperty,唯一的缺点就是兼容性不好,Proxy是javascript ES6的规范,这意味着Proxy在一些老旧的浏览器上无法使用(IE:你们为什么都在看我?)

Vue3响应式原理

接下来进入正题,我会为大家简单分析一下Vue3的响应式数据的原理,其代码主要在packages/reactivity/reactive.ts中(如果没学过typescript的话请右转百度先去了解一下typescript)

最关键的代码在这里,直接看注释就好:

// 这两个WeakMap用于存储未代理对象和已代理的响应式对象的对应关系,方便查找。
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()

function reactive(target: object) {
     
  // ...无关代码
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
     
  // ... 无关代码,包含判断target是否已经被代理的逻辑

  
  observed = new Proxy(target, handlers)  // 这部分代码就将handlers代理到target对象中
  toProxy.set(target, observed)  // 将<未代理对象,已代理对象>储存到一个WeakMap中,方便查找
  toRaw.set(observed, target)  // 将<已代理对象,未代理对象>储存到一个WeakMap中,方便查找
  
  // ... 无关代码
  
  return observed
}

实现细节1——深度代理

我们知道Proxy的set handler只能感知一层数据,那么对于多层嵌套的数据,Vue3是如何处理的呢?
关键代码在package/reactivity/baseHandlers.ts里:

function createGetter(isReadonly: boolean) {
     
  return function get(target: object, key: string | symbol, receiver: object) {
     
    const res = Reflect.get(target, key, receiver)
	// ...无关代码
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

什么?你在问Reflect是什么?打个比方 :
“有一个全局对象叫做Reflect,上面直接挂载了某些特殊方法,这些方法可以通过Reflect.xxxxx这种形式来使用。”
更详细请参考百度。

简单来说就是,调用更深一层的数据时,Proxy的get handler会触发,所以我们利用Reflect.get(target, key, receiver)对内层数据再进行一次代理。

当然在Vue3中,我们还判断了这个res是否只读、是否是对象,然后分别返回对应的readonly(res)、reactive(res)或者res。

实现细节2——避免多次trigger

如果大家使用过Proxy,就会发现它有个很大的问题——只要对某个对象的任意一个属性进行了更改,其set handler都会触发。比如,如果你对数组进行代理,然后进行push操作,你会发现set handler触发了两次,一次是push数据时触发,一次是修改length属性时触发。

let obj = []

const  test = new Proxy(obj, {
     
  set(target, key, value) {
     
    console.log("Set.")
  }
});

obj.push(1)  // 控制台会输出两次Set.

那这个问题是如何避免的呢?

// 判断val中是否有key
const hasOwn = (val: object,key: string | symbol): key is keyof typeof val => {
     
  return hasOwnProperty.call(val, key)
}

// 判断value和oldValue是否一致
const hasChanged = (value: any, oldValue: any): boolean => {
     
  return value !== oldValue && (value === value || oldValue === oldValue)
}

function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
     
  // ...无关代码
  const oldValue = (target as any)[key]
  const hadKey = hasOwn(target, key)
  const result = Reflect.set(target, key, value, receiver)
  
  if (!hadKey) {
     
    trigger(target, OperationTypes.ADD, key)
  } else if (hasChanged(value, oldValue)) {
     
    trigger(target, OperationTypes.SET, key)
  }
  return result
}

简单来说,就是通过判断 key 是否为 target 自身属性,以及设置val是否跟target[key]相等(如果是在原型链上的自动更新的属性,如Array.length,其val和target[key]必然相等)可以确定 trigger 的类型,并且避免多余的 trigger。

基本就是这样:)

你可能感兴趣的:(前端,前端,Vue)