本篇是以一步一步实现Vue的响应式-对象观测为基础,实现Vue中对数组的观测。
数组响应式区别于对象的点
const data = {
age: [1, 2, 3]
};
data.age = 123; // 直接修改
data.age.push(4); // 方法修改内容
如果是直接修改属性值,那么跟对象是没有什么区别的,但是数组可以调用方法使其自身改变,这种情况,访问器属性setter是拦截不到的。因为改变的是数组的内容,而不是数组本身。
setter拦截不到,就会导致依赖不能触发。也就是说,关键点在于触发依赖的位置。
起因都是由于数组的方法,所以我们想的是,数组方法在改变数组内容时,把依赖也触发了。这触发依赖是我们自定义的逻辑,总结起来就是,想要在数组的原生方法中增加自定义逻辑。
原生方法内容是不可见的,我们也不能直接修改原生方法,因为会对所有数组实例造成影响。但是,我们可以实现一个原生方法的超集,包含原生方法的逻辑与自定义的逻辑。
const arr = [1, 2, 3];
arr.push = function(val) {
console.log('我是自定义内容');
return Array.prototype.push.call(this, val);
};
image
拦截数组变异方式
覆盖原型
数组实例的方法都是从原型上获取的,数组原型上具有改变原数组能力的方法有7个:
- unshift
- shift
- push
- pop
- splice
- sort
- reverse
构造一个具有这7个方法的对象,然后重写这7个方法,在方法内部实现自定义的逻辑,最后调用真正的数组原型上的方法,从而可以实现对这7个方法的拦截。当然,这个对象的原型是真正数组原型,保证其它数组特性不变。
最后,用这个对象替代需要被变异的数组实例的原型。
const methods = ['unshift', 'shift', 'push', 'pop', 'splice', 'sort', 'reverse'];
const arrayProto = Object.create(Array.prototype);
methods.forEach(method => {
const originMethod = arrayProto[method];
arrayProto[method] = function (...args) {
// 自定义
return originMethod.apply(this, args);
};
});
在数组实例上直接新增变异方法
连接数组原型与访问器属性getter
对象的dep是在defineReactive函数与访问器属性getter形成的闭包中,也就是说数组原型方法中是访问不到这个dep的,所以这个dep,对于数组类型来说是不能使用了。
因此,我们需要构建一个访问器属性与数组原型方法都可以访问到的Dep类实例。所以构建的位置很重要,不过正好有个位置满足这个条件,那就是Observer类型的构造函数中,因为访问器属性与数组原型都是可以访问到数组本身的。
class Observer {
constructor(data) {
...
this.dep = new Dep();
def(data, '__ob__', this);
...
}
...
}
在数组本身绑定了一个不可迭代的属性ob,其值为Observer类的实例。现在,数组原型方法中可以访问到dep了,进行依赖触发:
methods.forEach(method => {
const originMethod = arrayProto[method];
arrayProto[method] = function (...args) {
const ob = this.__ob__;
const result = originMethod.apply(this, args);
// 触发依赖
ob.dep.notify();
return result;
};
});
访问器属性setter中收集依赖:
function defineReactive(obj, key, val) {
const dep = new Dep();
const childOb = observe(val);
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get: function () {
dep.depend();
if (childOb) {
childOb.dep.depend();
}
return val;
},
set: function (newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
dep只能收集到纯对象类型的依赖,如果是数组类型,就用新增的childOb中的dep去收集依赖。也就是说,childOb是Observer类的实例,来看看dep的实现:
function observe(value) {
let ob;
if (value.hasOwnProperty('__ob__') && Object.getPrototypeOf(value.__ob__) === Observer.prototype) {
ob = value.__ob__;
}
else if (isPlainObject(value) || Array.isArray(value)) {
ob = new Observer(value);
}
return ob;
}
首先判断value自身是否有ob属性,并且属性值是Observer类的实例,如果有就直接使用这个值并返回,这里说明ob标记了一个值是否被观测。如果没有,在value是纯对象或数组类型的情况下,用value为参数实例化Observer类实例作为返回值。
完整代码
// Observer.js
import Dep from './Dep.js';
import { protoAugment } from './Array.js';
class Observer {
constructor(data) {
this.data = data;
this.dep = new Dep();
def(data, '__ob__', this);
if (Array.isArray(data)) {
protoAugment(data);
observeArray(data);
}
else if (isPlainObject(data)) {
this.walk(data);
}
}
walk(data) {
const keys = Object.keys(data);
for (let key of keys) {
const val = data[key];
defineReactive(data, key, val);
}
}
}
function observe(value) {
let ob;
if (value.hasOwnProperty('__ob__') && Object.getPrototypeOf(value.__ob__) === Observer.prototype) {
ob = value.__ob__;
}
else if (isPlainObject(value) || Array.isArray(value)) {
ob = new Observer(value);
}
return ob;
}
function observeArray(data) {
for (let val of data) {
observe(val);
}
}
function defineReactive(obj, key, val) {
const dep = new Dep();
let childOb = observe(val);
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get: function () {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(val)) {
dependArray(val);
}
}
return val;
},
set: function (newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
}
});
}
function isPlainObject(o) {
return ({}).toString.call(o) === '[object Object]';
}
function def(obj, key, val) {
Object.defineProperty(obj, key, {
configruable: true,
enumerable: false,
writable: true,
value: val
});
}
// Array.js
const methods = [
'unshift',
'shift',
'push',
'pop',
'splice',
'sort',
'reverse'
];
const arrayProto = Object.create(Array.prototype);
methods.forEach(method => {
const originMethod = arrayProto[method];
arrayProto[method] = function (...args) {
const ob = this.__ob__;
const result = originMethod.apply(this, args);
ob.dep.notify();
return result;
}
});
export function protoAugment(array) {
array.__proto__ = arrayProto;
}
// Dep.js
let uid = 1;
Dep.target = null;
class Dep {
constructor() {
this.id = uid++;
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
for (let sub of this.subs) {
sub.update();
}
}
}
// Watcher.js
import Dep from './Dep.js';
class Watcher {
constructor(data, pathOrFn, cb) {
this.data = data;
if (typeof pathOrFn === 'function') {
this.getter = pathOrFn;
}
else {
this.getter = parsePath(data, pathOrFn);
}
this.cb = cb;
this.deps = [];
this.depIds = new Set();
this.value = this.get();
}
get() {
Dep.target = this;
const value = this.getter();
Dep.target = null;
return value;
}
addDep(dep) {
const id = dep.id;
if (!this.depIds.has(id)) {
this.deps.push(dep);
this.depIds.add(id);
dep.addSub(this);
}
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.data, this.value, oldValue);
}
}
function parsePath(path) {
if (/.$_/.test(path)) {
return;
}
const segments = path.split('.');
return function(obj) {
for (let segment of segments) {
obj = obj[segment]
}
return obj;
}
}
总结
响应式的关键点就在于读取数据->收集依赖,修改数据->触发依赖,由于数组的特殊性,所以要去拦截数组变异的方法,但本质其实并没有变。
网友评论