Vue3与2是如何监测数组变化的

Vue3与2是如何监测数组变化的_第1张图片

前言

Vue3.x已经发布一段时间了,更新内容也是比较多的,但是比较让我感兴趣的还是如何对Vue2无法直接监测数组变化的优化,今天抽时间来简单看一下实现原理。


一、Vue2为什么不能监测数组的变化

首先从表象上来看,Vue2对数组的响应式实现是有些不足的:

  • 无法监测数组的新增
  • 无法监测用索引改变数组的操作

先来简单分析下为什么会存在上述问题:
我们知道Vue2是通过Object.defineProperty方法来进行数据监测的,但是这个方法是无法监测到数据的变化吗?其实并不是,我们来看下下面的例子:

// 设置对象
function defineReactive(data, key, value) {
       
	Object.defineProperty(data, key, {
         
	    enumerable: true,    
	    configurable: true,     
	    get: function() {
      
	      console.log('获取数据', key, value);          
	      return value;  
	    },     
	    set: function(newVal) {
       
	      console.log('设置数据', key, newVal);         
	      value = newVal;   
	    }  
  	});
}

// 为每个属性设置代理(Vue源码中的walk方法)
function observe(data) {
     
	Object.keys(data).forEach(function(key) {
      
	    defineReactive(data, key, data[key]);  
	});
}

let testArr = ['a', 'b', 'c'];
observe(testArr);

testArr[0]; // "获取数据" "0" "a"
testArr[0] = 1;

Vue3与2是如何监测数组变化的_第2张图片
从上述结果可以看出,Object.defineProperty是可以对数组进行监测的,但是Vue2为什么没用呢,其实是出于性能的考虑,数据一般会被频繁的改动,每次的改动都需要遍历整个数组,给数组属性重新observe,这样会极大的消耗性能,因此在Vue2中hack了Array上的一些方法。
https://github.com/vuejs/vue/issues/8562

/* * not type checking this file because flow doesn't play well with * dynamically accessing methods on Array prototype */
import {
      def } from '../util/index';

// 复制数组构造函数的原型,Array.prototype也是一个数组。
const arrayProto = Array.prototype

// 创建对象,对象的__proto__指向arrayProto,所以arrayMethods的__proto__包含数组的所有方法。
export const arrayMethods = Object.create(arrayProto)

// 下面的数组是要进行重写的方法
const methodsToPatch = [  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse']

// 遍历methodsToPatch数组,重写其中的方法
methodsToPatch.forEach(function (method) {
      
  // 缓存原有方法
  const original = arrayProto[method]  
  // def方法定义在lang.js文件中,是通过object.defineProperty对属性进行重新定义。  
  // 即在arrayMethods中找到我们要重写的方法,对其进行重新定义  
  def(arrayMethods, method, function mutator (...args) {
        
    const result = original.apply(this, args)    
    const ob = this.__ob__    
    let inserted    
    switch (method) {
           
      // ,对于push,unshift会新增索引,所以需要手动observe     
      case 'push':      
      case 'unshift':        
        inserted = args        
        break// splice方法,如果传入了第三个参数,也会有新增索引,所以也需要手动observe      
      case 'splice':        
        inserted = args.slice(2)        
        break}    
    // push,unshift,splice触发后,手动observe,其他方法的变更会在当前的索引上进行更新,不需要执行observe
    if (inserted) ob.observeArray(inserted)// notify change    
    ob.dep.notify()    
    return result  
  })
})

所以数组调用以上这些方法的时候是会触发数据绑定的变化的。
另外,$set也检测数组变动,看下源码:

function set (target, key, val) {
     
  //... 
  if (Array.isArray(target) && isValidArrayIndex(key)) {
     
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val
  }
  if (key in target && !(key in Object.prototype)) {
     
    target[key] = val;
    return val
  }
  //... 
  defineReactive$$1(ob.value, key, val);
  ob.dep.notify();
  return val
}

同样的道理,只是一个数组的判断,之后去observe,然后notify。

二、Vue3是如何监测数组变化的

Vue3是用Proxy来进行对象、数组的代理,其实只要理解了Proxy就明白Vue3为什么会抛弃Object.defineProperty了:

  • Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
    由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。
  • Object.defineProperty对新增属性需要手动进行Observe。
    由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。
  • Proxy 具有更多的拦截支持,可以做的更精细化的控制
const handler = {
     
    set(target, key, value) {
     
        console.log('set', key);
        target[key] = value;
        return true;
    },
    get(target, key) {
     
        console.log('get', key);
        return target[key];
    }
};
const target = ['a', 'b', 'c'];

const proxy = new Proxy(target, handler);
proxy[0] = 'li';
proxy[1];
console.log(target);

Vue3与2是如何监测数组变化的_第3张图片


总结

仔细分析不难看出,Object.defineProperty与Proxy的区别在于一个是只能对属性拦截,对象或数组监测时必须遍历,另一个是对对象本身的劫持,确实方便了很多,但是Proxy本身的支持可能还是要看浏览器的支持程度的,不过Vue3也可能对其做了polyfill,可以拉下源码再瞅瞅。

你可能感兴趣的:(es6,js,vuejs,vue,array,javascript)