Vue 3 响应性原理以及源码分析

Vue3通过Proxy实现响应式,提升了性能,解决了Object.defineProperty递归的问题。响应性核心包括track收集依赖、trigger触发更新。Ref封装原始数据,computed实现缓存计算。watch源码解析了依赖收集与触发更新的逻辑。Vue3对数组的响应式监控通过get操作触发,即使数组元素变化也能精准响应。
摘要由CSDN通过智能技术生成

Vue响应式系统的核心依然是对数据进行劫持,只不过Vue3采样点是Proxy类,而Vue2采用的是Object.defineProperty()。Vue3之所以采用Proxy类主要有两个原因:

  • 可以提升性能,Vue2是通过层层递归的方式对数据进行劫持,并且数据劫持一开始就要进行层层递归(一次性递归),如果对象的路径非常深将会非常影响性能。而Proxy可以在用到数据的时候再进行对下一层属性的劫持。
  • Proxy可以实现对整个对象的劫持,而Object.defineProperty()只能实现对对象的属性进行劫持。所以对于对象上的方法或者新增、删除的属性则无能为力。

首先响应式的思路无外乎这样一个模型:

  • 定义某个数据为响应式数据,它会拥有收集访问它的函数的能力。
  • 定义观察函数,在这个函数内部去访问响应式数据,访问到响应式数据的某个key的时候,会建立一个依赖关系key -> reaction观察函数。
  • 检测到响应式数据的key的值更新的时候,会去重新执行一遍它所收集的所有reaction观察函数。

data

if (dataOptions) {
	if (!isFunction(dataOptions)) {
     	warn(...);
    }
    const data = dataOptions.call(publicThis, publicThis);
    if (isPromise(data)) { warn(); }
    if (!isObject(data)) {
     	warn(`data() should return an object.`);
    } else {
    	instance.data = reactive(data);
        {
        	for (const key in data) {
           		checkDuplicateProperties("Data" /* DATA */, key);
                // expose data on ctx during dev
             	if (key[0] !== '$' && key[0] !== '_') {
                	Object.defineProperty(ctx, key, {
                    	configurable: true,
                        enumerable: true,
                        get: () => data[key],
                        set: NOOP
                     });
                }
         	}
      	}
   }
}
  • data必须是函数的校验
  • 调用data函数,得到返回值。对于返回值非Promise对象、非Object,会抛出警告
  • 只有对于返回值是Object,才会调用reactive函数来对返回值进行响应处理
  • 响应式副本保存在实例对象instance的data属性
    从data选项的处理逻辑中可知,data的响应性是通过reactive函数来实现的。

Reactivity(数据是怎么更新的)

在这里插入图片描述
1、存储 total 的计算方式让 price 或 quantities 更新时,total 再计算一次

我们想保存 let toatl = price * quantity 这句代码,所以我们需要一个仓库,然后把代码存储进去, 以便于在第一次运行完代码之后,我们还可以再次调用该代码。

在这里插入图片描述

let effect = function(){ 
  total = price * quantity
}

我们需要调用一个追踪函数 track 去存储我们的代码,然后调用 effect 来计算首次的 total。在之后的某个时刻,再次调用触发函数 trigger 来运行所有存储了的代码。

在我们代码中提到的track()、effect()、trigger() 你都可以在 Vue 3 响应性源码中看到同名的函数

为了存储我们的 effects,我们将使用 dep 变量,它代表依赖关系,用来储存 effects

let dep = new Set()

为了跟踪依赖,我们将 effect 添加到 Set 中。使用 Set 是因为它不允许拥有重复值。当我们尝试添加同样的effect时,它不会变成两个。

function track(){
    dep.add(effect)
}

然后我们需要使用触发函数 trigger 遍历我们存储了的每一个 effect,然后运行它们。

function trigger(){  dep.forEach(effect=>effect())  }

通常,我们的对象会有多个属性,每个属性都需要自己的dep(依赖关系),或者说 effect 的 Set(集)。 ​

现在问题我们要如何存储这些dep,或者说让每个属性拥有(自己的)依赖。

接下来我们要封装价格和数量到一个产品对象中。

let product = {price: 5, quantity: 2}

每一个属性都需要有自己的 dep,而 dep 其实就是一个effect 集。这个 effect 集应该在值发生改变时重新运行。这个 dep 的类型是Set,Set 中的每个值都只是一个我们需要执行的 effect,就像我们的这个计算总数的匿名函数。为了方便在 effect 执行完我们以后再找到它们,我们需要把这些 dep 储存起来,我们要创建一个 depsMap。

depsMap 是一张储存了每个属性 dep 对象的图(ES6Map),图里有一组键和值。我们将使用我们对象的属性作为键,比如数量或价格。在这种情况下,我们的值就是一个dep(effects集合)。

在这里插入图片描述
depsMap 如下代码描述

const depsMap = new Map()

function track(key) {
  let dep = depsMap.get(key) // 拿到特定的 dep,这里的 dep 是 价格/数量
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(effect) // 添加 effect
}

function trigger(key) {
  let dep = depsMap.get(key) // 获取键的 dep
  if (dep) {
    // 如果存在 dep 运行每个 effect
    dep.forEach(effect => {
      effect()
    })
  }
}
let product = {
  price: 5,
  quantity: 2
}
let total = 0

let effect = () => {
  total = product.price * product.quantity
}
track('quantity')
effect()

其实就是 该对象属性 都映射一个 effect ,effect 是保存关于该对象属性的计算或者处理
上文【 track】 是保存该对象属性 和 effect 之前的映射,depsMap是该对象。

目前为止,我们有一张 depsMap,它存储了每个属性自己的依赖对象(属性到自己依赖对象的映射)。然后每个属性都拥有它们自己的并可以重新运行 effect 的 dep。

在这里插入图片描述
在这里插入图片描述
现在我们从 targetMap 开始重写我们的代码

//目标图存储着每个响应式对象的依赖
const targetMap = new WeakMap()

function track(target,key){
  //获取目标的 deps 图,在我们的例子中是 product
  let depsMap = targetMap.get(target)
  //如果它还不存在,我们将为这个对象创建一个新的deps图
  if(!depsMap){
    targetMap.set(target,(depsMap = new Map()))
  }
  //获得这个属性的依赖对象(quantity)
  let dep = depsMap.get(key)
  //如果它不存在,我们将创建一个新的 Set
  if(!dep){
    depsMap.set(key,(dep = new Set()))
  }
  //把 effect 添加到依赖中
  dep.add(effect)
}
function trigger(target,key){
  //检查此对象是否拥有依赖的属性
  const depsMap = targetMap.get(target)
  // 没有则直接返回
  if(!depsMap){return} 
   //否则,我们将检查此属性是否具有依赖
  let dep = depsMap.get(key)
  //dep 存在,遍历dep,运行每一个 effect
  if(dep){
    dep.forEach(effect => {effect()})
  }
}

let product = {price:5,quantity:2}
let total = 0
let effect = () =>{
  total = product.price * product.quantity
}
//传递产品和数量
track(product,'quantity')
effect()

Proxy & Reflect(何时让数据变化)

由上可知 ,我们仍要手动调用 track 来保存 effect;调用 trigger 来触发 effect。我们想让我们的响应性引擎自动调用 track 和 trigger。那么问题就在于,什么才是调用它们的最好时机呢? ​

从逻辑上来说,在运行 effect 时,如果访问了产品的属性,或者说是使用了GET,就是我们想调用 track 去保存 effect 的时候 。 ​

如果产品的属性改变了,或者说使用了SET,就是我们想调用 trigger 来运行那些保存了的 effect 的时候

Vue 3 中,我们将使用 ES6 Reflect和 ES6Proxy(代理)

这是我们的产品,这里有三种方法打印出对象的属性

let product = {price: 5, quantity: 2}

首先,我们可以使用经典的“点”表示法

console.log('quantity is ' + product.quantity) //Dot notation

我们也可以使用中括号表示法

console.log('quantity is ' + product['quantity']) //Bracket notation

我们还可以使用 ES6 Reflect

console.log('quantity is ' + Reflect.get(product,'quantity'))

Reflect是ES6为了操作对象而新增的API, 为什么要添加Reflect对象呢?它这样设计的目的是为了什么?

1)将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上,那么以后我们就可以从Reflect对象上可以拿到语言内部的方法。

2)在使用对象的 Object.defineProperty(obj, name,{})时,如果出现异常的话,会抛出一个错误,需要使用try catch去捕获,但是使用
Reflect.defineProperty(obj, name, desc) 则会返回false。

let proxiedProduct = new Proxy(product,{}) //proxiedProduct

你看到代理中的第二个参数了吗?它叫 handler(处理程序,是一个对象),在 handler 中,你可以传递一个 trap(诱捕器)。trap 可以让我们拦截基本操作,如属性查找,枚举或函数调用。 ​

但是当外界每次对obj进行操作时,就会执行handler对象上的一些方法。handler中常用的对象方法如下:

  1. get(target, propKey, receiver)
  2. set(target, propKey, value, receiver)
  3. has(target, propKey)
  4. deleteProperty(target, args):
  5. ownKeys(target, object, args)

深入理解 ES6中的 Reflect

let product = {price: 5, quantity: 2} 
let proxiedProduct = new Proxy(product,{
    get(target,key){//声明两个参数
        console.log('Get was call with key = ' + key)
    return target[key] //Bracket notation
  }
}) 

console.log(proxiedProduct.quantity) //(1)

当我们调用console.log()( 1),它会通过get(target)调用我们的 proxiedProduct。在这种情况下,这个目标就是我们传递的 product,它成了 target 的值。我们的 key 是 quantity,因为我们想得到 quantity,我们的代码就会输出“Get was call with key = quantity”(get 被调用了,它的key = 数量)。它返回该属性的值。

现在我们把代码中的中括号表示法变成 Reflect

在 Proxy 里使用 Reflect,我们会有一个附加参数,称为 receiver(接收器),它将传递到我们的 Reflect调用中。它保证了当我们的对象有继承自其它对象的值或函数时 this 指针能正确的指向使用(的对象),这将避免一些我们在 vue2 中有的响应式警告,打印结果和之前一样

let product = {price: 5, quantity: 2} //product
let proxiedProduct = new Proxy(product,{//proxiedProduct
    get(target,key,receiver){
        console.log('Get was call with key = ' + key)
    return Reflect.get(target,key,receiver) // <-现在我们把代码中的中括号表示法变成 Reflect
  }
  // 我们的代理还需要拦截 set 方法 
  set(target,key,value,receiver){
    console.log('Set was called with key = '+ key + ' and value ' + value)
    return Reflect.set(target,key,value,receiver)
  }
}) 

console.log(proxiedProduct.quantity)
// Get was call with key = quantity
// 2

reactive的函数

用 handler 包装我们的 get 和 set方法 到常量处理程序中,最后我们将创建一个新的 Proxy,传递我们的 target 和我们的 handler

function reactive(target){
  const handler = {
    get(target,key,receiver){
      console.log('Get was call with key = ' + key)
      return Reflect.get(target,key,receiver)
    },
    set(target,key,value,receiver){
      console.log('Set was called with key = '+ key + ' and value ' + value)
      return Reflect.set(target,key,value,receiver)
    }
  }
  return new Proxy(target,handler)
}

现在,我们声明产品时,我们只需传递一个对象到响应式函数中

let product = reactive({price: 5, quantity:2 })

它(响应性函数)将返回一个代理对象,我们会把这个代理对象当作原始对象来用。然后我们将更改产品的数量并将它输出到控制台。

let product = reactive({price: 5, quantity:2 })
product.quantity = 4
console.log(product.quantity)
// Set was called with key = quantity and value 4
// Get was call with key = quantity
// 4

结合以上所有知识

function reactive(target){
  const handler = {
    get(target,key,receiver){
      let result = Reflect.get(target,key,receiver)
      track(target,key) // 时机 -》触收集effect
      return result
    },
    set(target,key,value,receiver){
      let oldValue = target[key]
      let result = Reflect.set(target,key,value,receiver)
      if(oldValue != result){
        trigger(target,key) // 触发数据变化执行effect
      }
      return result
    },
  }
  return new Proxy(target,handler)
}

let product = reactive({price: 5, quantity: 2 })

现在我们已经不再需要手动调用 track() 和 trigger() 了 撒花❀❀❀❀❀

Ref实现

简单来说ref就是:原始数据=>响应式数据 的过程

Ref 接受一个值,并返回一个响应的,可变的 Ref 对象,Ref 对象只有一个“.value”属性,它指向内部的值。它就相当于从一个文件复制粘贴过来的值。

  1. ref本质是将一个数据变成一个对象,这个对象具有响应式特点
  2. ref接受的原始数据可以是原始值也可以是引用值,返回的对象本质都是RefImpl类的实例
  3. 无论传入的原始数据时什么类型,当原始数据发生改变时,并不会影响响应数据,更不会触发UI的更新。但当响应式数据发生改变,对应界面UI是会自动更新的,注意不影响原始数据。所以ref中,原始数据和经过ref包装后的响应式数据是无关联的

示例代码1:

let origin = 0; //原始数据为原始值
let count = ref(origin);
function add() {
  count.value++;
}

示例代码2:

let origin = { val: 0 };//原始数据为对象
let count = ref(origin);
function add() {
  count.value.val++;
}

经测试,我们发现,传递的原始数据orgin可以是原始值也可以是引用值,但是需要注意,如果传递的是原始值,指向原始数据的那个值保存在返回的响应式数据的.value中,如上count.value;如果传递的一个对象,返回的响应式数据的.value中对应有指向原始数据的属性,如上count.value.val

不管传递数据类型的数据给ref,无论是原始值还是引用值,返回的响应式数据对象本质都是由RefImpl类构造出来的对象。但不同的是里头的value,一个是原始值,一个是Proxy对象(原始数据为对象),最终Vue会根据传入的数据是不是对象isObject(val),如果是对象本质调用的是reactive,否则返回原始数据

unction ref(raw){
  const r = {
    get value(){
      //调用跟踪函数,追踪我们正在创建的对象r,键是"value",然后返回原始值(传入值)
      track(r,'value')
      return raw
    },
    //setter 接收一个新值,把新值赋值给原始值(raw)
    set value(newVal){
      raw = convert(newVal)
      // convert有判断 数据是object 还是原始类型
      //调用触发函数
      trigger(r,'value',raw )
    },
  }
  //返回对象
  return r
}
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
}

实现computed()方法

计算属性两个最大的特点就是

  • 延时计算 计算属性所依赖的值发生改变的时候并不会立即执行getter函数去重新计算新的结果,而是打开重新计算的开关并通知订阅计算属性的副作用函数进行更新。如果当前的计算属性没有依赖集合就不执行重新计算逻辑,如果有依赖触发计算属性的get,这个时候才会调用this.effect()进行重新计算。(也就是说当compute 的值并没有 被其他调用,只是写了 compute 这个开关就不会开也就不会在计算
  • computed 的特性就在于能够缓存计算的值(提升性能),只有当 computed 的依赖发生变化时才会重新计算,否则读取 computed 的值则一直是之前的值

getter 方法会在读取 computed 值的时候执行,而在 getter 方法中有一个叫 _dirty 的变量,它的意思是代表脏数据的开关,默认初始化时 _dirty 被设为 true ,在 getter 方法中表示开关打开,需要计算一遍computed 的值,然后关闭开关,之后再获取 computed 的值时由于 _dirty 是 false 就不会重新计算。这就是 computed 缓存值的实现原理

// 
 server.on('request', (req, res) => {
      setTimeout(() => {
        if (/\d.json/.test(req.url)) {
          const data = {
            content: '我是内容,来自' + req.url
          }
          res.end(JSON.stringify(data));
        }
      }, Math.random() * 2000);
    });

属性内部会创建一个effect对象,只不过这个effect不是立即执行,而是等到取值的时候再执行,从之前computed的用法中,可以看到,computed()函数返回一个对象,并且这个对象中有一个value属性,可以进行get和set操作。

import {isFunction} from './shared/index';
import { effect, track, trigger } from './effect';
export function computed(getterOrOptions) {
    let getter;
    let setter;
    if (isFunction(getterOrOptions)) {
        getter = getterOrOptions;
        setter = () => {};
    } else {
        getter = getterOrOptions.get;
        setter = getterOrOptions.set;
    }
    let dirty = true; // 默认是脏的数据
    let computed;
    // 计算属性本质也是一个effect,其回调函数就是计算属性的getter
    let runner = effect(getter, {
        lazy: true, // 默认是非立即执行,等到取值的时候再执行
        computed: true, // 标识这个effect是计算属性的effect
         scheduler: () => {
	      // 在触发更新时 只是把dirty置为true 
	      // 而不去立刻计算值 所以计算属性有lazy的特性
	      
	      dirty = true
	      trigger(computed, "set", "value"); // 数据变化后,触发value依赖
    	}
    });
    let value;
    computed = {
        get value() {
            if (dirty) {
                value = runner(); // 等到取值的时候再执行计算属性内部创建的effect
                dirty = false; // 取完值后数据就不是脏的了
                track(computed, "get", "value"); // 对计算属性对象收集value属性
            }
    
            return value;
        },
        set value(newVal) {
            setter(newVal);
        }
    }
    return computed;
}

首先要知道,effect函数会立即开始执行,再执行之前,先把effect自身变成全局的activeEffect,以供响应式数据收集依赖。

并且activeEffect的记录是用栈的方式,随着函数的开始执行入栈,随着函数的执行结束出栈,这样就可以维护嵌套的effect关系。

export function trigger(target, type, key, value) {
    const depsMap = targetMap.get(target); // 获取当前target对应的Map
    if (!depsMap) { // 如果该对象没有收集依赖
        console.log("该对象还未收集依赖"); // 比如修改值的时候,没有调用过effect
        return;
    }
    const effects = new Set(); // 存储依赖的effect
    const add = (effectsToAdd) => {
        if (effectsToAdd) {
            effectsToAdd.forEach(effect => {
                effects.add(effect);
            });
        }
    };
    // const run = (effect) => {
    //     effect(); // 立即执行effect
    // }
    // 修改run方法,如果是计算属性的effect则执行其scheduler方法
   + const run = (effect) => {
   +     if (effect.options.scheduler) { // 如果是计算属性的effect则执行其scheduler()方法
   +        effect.options.scheduler(effect);
   +     } else { // 如果是普通的effect则立即执行effect方法
   +         effect();
   +     }
   + }
    /** 
     *  对于effect中使用到的数据,那肯定是响应式对象中已经存在的key,当数据变化后肯定能通过该key拿到对应的依赖,
     * 对于新增的key,我们也不需要通知effect执行。
     * 但是对于数组而言,如果给数组新增了一项,我们是需要通知的,如果我们仍然以key的方式去获取依赖那肯定是无法获取到的,
     * 因为也是属于新增的一个索引,之前没有对其收集依赖,但是我们使用数组的时候会使用JSON.stringify(arr),此时会取length属性,
     * 索引会收集length的依赖,数组新增元素后,其length会发生变化,我们可以通过length属性去获取依赖
    */
    if (key !== null) {
        add(depsMap.get(key)); // 对象新增一个属性,由于没有依赖故不会执行
    }
    if (type === "add") { // 处理数组元素的新增
        add(depsMap.get(Array.isArray(target)? "length": ""));
    }
    // 遍历effects并执行
    effects.forEach(run);
}
// 1. 响应式数据
const data = reactive({ count: 0 })
// 2. 计算属性
const plusOne = computed(() => data.count + 1)
// 3. 依赖收集
effect(() => console.log(plusOne.value))
// 4. 触发上面的effect重新执行
data.count ++

就这个例子来说,data是一个响应式数据。

effect传入的函数因为内部访问到它上面的属性count了,

所以形成了一个count -> effect的依赖 (看上面可知data 是响应的数据就会有 track / trigger 改变 count 触发 trigger 进而执行 effect )。

下次count改变了,这个effect就会重新执行,就这么简单。

先起几个别名便于讲解

// 计算effect
computed(() => data.count + 1)
// 日志effect
effect(() => console.log(plusOne.value))

从依赖关系来看,

  • 日志effect读取了 计算effect
  • 计算effect读取了响应式属性count
    所以更新的顺序也应该是:
    count改变 -> 计算effect更新 -> 日志effect更新

计算effect的dirty置为true,标志着下次读取需要重新求值。
日志effect读取计算effect的value,获得最新的值并打印出来。

watch源码

function watch(source, cb, options) {
	if (!isFunction(cb)) { warn(...); }
    return doWatch(source, cb, options);
}
// doWatch函数
if (isRef(source)) {
	getter = () => source.value;
    forceTrigger = !!source._shallow;
} else if (isReactive(isReactive)) {
	getter = () => source;
    deep = true;
} else if (isArray(source)) {
	isMultiSource = true;
    forceTrigger = source.some(isReactive);
    getter = () => source.map(s => { ... });
} else if (isFunction(source)) {
	getter = () => { ... };
} else {
	getter = NOOP;
    warnInvalidSource(source);
}

watch API支持不同方式的源定义,实际上就是不同类型定义相关的getter函数,其中对于监听源是被reactive处理的对象,需要调用traverse函数来处理
通过对属性获取就会执行其定义的get操作的函数,其中会触发track函数完成上面的目的。对于复杂结构的完全代理对象,这是一个需要关注的性能点。

watch API支持的选项有:

  • immediate:马上执行
  • deep:深度监听
  • flush:该选项控制副作用的处理时机,flush存在三个值:sync(同步的)、pre(组件更新前,默认值)、post(组件更新后)
  • onTrack 和 onTrigger:开发环境下用于调试监听器行为的相关入口

不同的flush值会设置不同的调度器逻辑

watchEffect是如何收集依赖呢?执行种通过effect函数创建的一个函数即activeEffect,该函数中会真正执行所谓的副作用逻辑。当执行runner函数时,就会执行watchEffect的source函数,这个过程中所有响应属性就会与当前activeEffect(即这里的runner)建立关联,这样就完成了所谓的依赖收集。当收集的属性被重新赋值,依据建立的联系就会触发副作用effect被执行,这就是watchEffect的功能了。

参考

vue2和vue3差别

  • Vue2响应系统中对象的每一个属性在get拦截中都会生成一个Dep对象,而Dep对象会与Watcher对象进行关联,通过发布订阅模式来实现整个响应系统;Vue3实际上原理类型,不同在于因为使用Proxy,实际上建立关联的是访问的属性主体与effect,effect可以将其看成Vue2中的Watcher对象,其创建的时机和功能都与Vue2中的Watcher高度相似

  • Vue3中effect只有通过effect函数来创建,Vue3将该函数也暴露出来了,实际上effect概念上来表示副作用(引申自函数式编程中纯函数等概念)本质上就是一个函数,这个函数内部会调用真正副作用逻辑的函数。effect函数存在很多属性,参入的相关逻辑比较复杂。而且effect函数调用只在三个地方computed相关、watch相关、mountComponent,与Vue2中Watcher实例创建的时机非常相似

  • Vue3中通过track来实现依赖收集即建立调用属性与effect联系,trigger来触发视图更新即遍历执行effect队列,执行effect自带的调度器scheduler或者执行自身(mount挂载阶段创建的一个effect实际上其调度器就是queueJob函数,该函数就是将job推入队列指定位置并遍历所有队列,所谓的job就是处理effect)

  • Vue3中深度代理以及浅层代理的改变都是判断相关返回值是否再次代理来实现的,而这个过程中Proxy代理相关的缓存机制会优化这个过程

  • Vue3支持对Map、Set等对象的代理,实际上都是只拦截get操作来实现的,在其自定义处理中会针对不同的实例方法调用分别处理,相关原理实际上涉及到ES规范中提及的内部方法和内部插糟等相关说明
    参考Vue3源码之响应系统

vue2和vue3对数组响应式的区别

vue2当数据编译的时候就将所有的数据都通过object.definePropertyo的set 和get方法收集起来,所以在后续添加某个对象属性的时候这个新加的这个属性并不是响应式的,因为根本没有走object.definePropertyo的set 和get方法,对于数组则需要拦截它的原型方法来实现响应式。
也是由于一开始就要监听所有属性,所以当数据量大的时候性能消耗很大,而且需要 this.$set,来使得新增的属性也拥有响应式的效果,所以在更改响应式属性的时候要通过key值去更改,因为在 object.definePropertyo的set 方法收集了相关依赖,但是我们根据上文可知,vue3只有使用某个属性的时候才进行收集依赖,性能优化不少,并且也不在意一开始的时候定义了哪些属性,所以无论是新加的的key或者是原来的key,都逃不过它的魔爪

那么数组在vue3是怎么监控到的呢?



const raw = []
const arr = new Proxy(raw, {
  get(target, key) {
    console.log('get', key)
    return Reflect.get(target, key)
  },
  set(target, key, value) {
    console.log('set', key)
    return Reflect.set(target, key, value)
  }
})
 
arr.push(1)

对于 raw 这个数组上的所有 get、set 操作,并且调用 Reflect 这个 api 原样处理取值和赋值操作后返回。看看 arr.push(1) 后控制台打印出了什么

get push
get length
set 0
set length

原来一个小小的 push,会触发两对 get 和 set,我们来想象一下流程:

读取 push 方法
读取 arr 原有的 length 属性
对于数组第 0 项赋值
对于 length 属性赋值

这里的重点是第三步,对于第 index 项的赋值,那么下次再 push,可以想象也就是对于第 1 项触发 set 操作。

而我们在例子中读取 data[1],是一定会把对于 1 这个下标的依赖收集起来的,这也就清楚的解释了为什么 push 的时候也能精准的触发响应式依赖的执行。

vue3与vue2的区别之数据响应

参考vue3数组和map的响应式

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值