本篇是以一步一步實(shí)現(xiàn)Vue的響應(yīng)式-對(duì)象觀測(cè)為基礎(chǔ),實(shí)現(xiàn)Vue中對(duì)數(shù)組的觀測(cè)。
數(shù)組響應(yīng)式區(qū)別于對(duì)象的點(diǎn)
const data = {
age: [1, 2, 3]
};
data.age = 123; // 直接修改
data.age.push(4); // 方法修改內(nèi)容
如果是直接修改屬性值,那么跟對(duì)象是沒有什么區(qū)別的,但是數(shù)組可以調(diào)用方法使其自身改變,這種情況,訪問器屬性setter是攔截不到的。因?yàn)楦淖兊氖菙?shù)組的內(nèi)容,而不是數(shù)組本身。
setter攔截不到,就會(huì)導(dǎo)致依賴不能觸發(fā)。也就是說,關(guān)鍵點(diǎn)在于觸發(fā)依賴的位置。
起因都是由于數(shù)組的方法,所以我們想的是,數(shù)組方法在改變數(shù)組內(nèi)容時(shí),把依賴也觸發(fā)了。這觸發(fā)依賴是我們自定義的邏輯,總結(jié)起來就是,想要在數(shù)組的原生方法中增加自定義邏輯。
原生方法內(nèi)容是不可見的,我們也不能直接修改原生方法,因?yàn)闀?huì)對(duì)所有數(shù)組實(shí)例造成影響。但是,我們可以實(shí)現(xiàn)一個(gè)原生方法的超集,包含原生方法的邏輯與自定義的邏輯。
const arr = [1, 2, 3];
arr.push = function(val) {
console.log('我是自定義內(nèi)容');
return Array.prototype.push.call(this, val);
};
攔截?cái)?shù)組變異方式
覆蓋原型
數(shù)組實(shí)例的方法都是從原型上獲取的,數(shù)組原型上具有改變?cè)瓟?shù)組能力的方法有7個(gè):
- unshift
- shift
- push
- pop
- splice
- sort
- reverse
構(gòu)造一個(gè)具有這7個(gè)方法的對(duì)象,然后重寫這7個(gè)方法,在方法內(nèi)部實(shí)現(xiàn)自定義的邏輯,最后調(diào)用真正的數(shù)組原型上的方法,從而可以實(shí)現(xiàn)對(duì)這7個(gè)方法的攔截。當(dāng)然,這個(gè)對(duì)象的原型是真正數(shù)組原型,保證其它數(shù)組特性不變。
最后,用這個(gè)對(duì)象替代需要被變異的數(shù)組實(shí)例的原型。
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);
};
});
在數(shù)組實(shí)例上直接新增變異方法
連接數(shù)組原型與訪問器屬性getter
對(duì)象的dep是在defineReactive函數(shù)與訪問器屬性getter形成的閉包中,也就是說數(shù)組原型方法中是訪問不到這個(gè)dep的,所以這個(gè)dep,對(duì)于數(shù)組類型來說是不能使用了。
因此,我們需要構(gòu)建一個(gè)訪問器屬性與數(shù)組原型方法都可以訪問到的Dep類實(shí)例。所以構(gòu)建的位置很重要,不過正好有個(gè)位置滿足這個(gè)條件,那就是Observer類型的構(gòu)造函數(shù)中,因?yàn)樵L問器屬性與數(shù)組原型都是可以訪問到數(shù)組本身的。
class Observer {
constructor(data) {
...
this.dep = new Dep();
def(data, '__ob__', this);
...
}
...
}
在數(shù)組本身綁定了一個(gè)不可迭代的屬性ob,其值為Observer類的實(shí)例。現(xiàn)在,數(shù)組原型方法中可以訪問到dep了,進(jìn)行依賴觸發(fā):
methods.forEach(method => {
const originMethod = arrayProto[method];
arrayProto[method] = function (...args) {
const ob = this.__ob__;
const result = originMethod.apply(this, args);
// 觸發(fā)依賴
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只能收集到純對(duì)象類型的依賴,如果是數(shù)組類型,就用新增的childOb中的dep去收集依賴。也就是說,childOb是Observer類的實(shí)例,來看看dep的實(shí)現(xiàn):
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類的實(shí)例,如果有就直接使用這個(gè)值并返回,這里說明ob標(biāo)記了一個(gè)值是否被觀測(cè)。如果沒有,在value是純對(duì)象或數(shù)組類型的情況下,用value為參數(shù)實(shí)例化Observer類實(shí)例作為返回值。
完整代碼
// 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;
}
}
總結(jié)
響應(yīng)式的關(guān)鍵點(diǎn)就在于讀取數(shù)據(jù)->收集依賴,修改數(shù)據(jù)->觸發(fā)依賴,由于數(shù)組的特殊性,所以要去攔截?cái)?shù)組變異的方法,但本質(zhì)其實(shí)并沒有變。