Vue中数组的响应式原理解析

为何Vue中对象与数组的侦测方式不同?因为数组的一些方法如pushpop等不会触发getter/setter

为何这些方法不会触发getter/setter?因为这些方法是Array的原型上的方法,并没有在Array本身上面。

如何做?使用拦截器覆盖Array.prototype上的方法,在执行原型上的方法之外做数据的响应式。

实现思路

  1. 将数组的原型存到对象arrayMethods
  2. 找到Array上能够改变数组自身的7个方法 push, pop, shift, unshift, splice, sort, reverse
  3. 将这7个方法进行响应式处理
  4. 处理完成后,用它们把arrayMethods中对应的方法覆盖掉
  5. 将需要进行响应式处理的数组arr__proto__指向arrayMethods,如果浏览器不支持访问__proto__,则直接将响应式处理后的7个方法添加到数组arr
  6. 如果要将一个数组完全实现响应式,需要遍历该数组,将数组中的数组使用该方法进行响应式处理,将对象使用walk方法进行响应式处理

拦截器

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
	'push',
	'pop',
	'shift',
	'unshift',
	'splice',
	'sort',
	'reverse'
]
.forEach(mentod => {
	 // 缓存原始方法
	const original = arrayProto[method];
	Object.defineProperty(arrayMethods, method, {
		value: function mutator(...args) {
			// 返回原始方法
			return original.apply(this, args); 
		},
		enumerable: false,
		writable: true,
		configurable: true
	})
})

将拦截器挂载到数组上

import { arrayMethods } from './array' // 处理好的Array原型对象

// __proto__是否可用
const hasProto = '__proto__' in {};
// 所有属性名,不论是否可枚举(与Object.keys的区别)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

export class Observe {
	// 将value转为响应式
	constructor (value) {
		this.value = value;

		if (Array.isArray(value)) {
			const augment = hasProto ? protoAugment : copyAugment;
			augment(value, arrayMethods, arrayKeys);
		} else {
			this.walk(value); // Object的响应式处理,在其他文章中
		}
	}
}

/**
* __proto__可用的处理方式
* 将target对象的原型对象替换为src
*/
function protoAugment(target, src, keys) {
	target.__proto__ = src;
}

/**
* __proto__不可用的处理方式
* 将src上面的所有属性都copy到target
*/
function copyAugment (target, src, keys) {
	for (let i = 0, len = keys.length; i < len; i ++) {
		const key = keys[i];
		def(target, key, src[key]);
	}
}

// Object.defineProperty()的封装
function def (obj, key, val, enumerable) {
	Object.defineProperty(obj, key, {
		value: val,
		enumerable: !!enumerable,
		writable: true,
		configurable: true
	})
}

收集依赖

function defineReactive(data, key, val) {
    let childOb = observe(val);
    let dep = new Dep(); // 存储依赖
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend();

            if (childOb) childOb.dep.depend(); // 收集
            return val;
        },
        set: function (newVal) {
            if (val === newVal) return;
            dep.notify();
            val = newVal;
        }
    })
}

// 返回val的响应式对象
function observe(val, asRootData) {
    if (!isObject(value)) return;
    let ob;
    // 避免重复侦测
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof observer) {
        ob = value.__ob__;
    } else {
        ob = new Observe(value)
    }
    return ob;
}

问题

有些数组的操作Vue 2.0是拦截不到的:

  • this.list[0] = 2;

  • this.list.length = 0;

上面两种情况不会触发响应式,因为Vue 2.0的实现方式决定了无法对上面种种情况做拦截。
在Vue 3.0中使用Proxy解决了这个问题。

总结

Array在getter中收集依赖,在拦截器中触发依赖,在Observer中保存依赖

Logo

前往低代码交流专区

更多推荐