一文搞定數(shù)據(jù)響應(yīng)式原理
在Vue中,其中最最最核心的一個(gè)知識(shí)點(diǎn)就是數(shù)據(jù)響應(yīng)式原理,數(shù)據(jù)響應(yīng)式原理歸結(jié)起來(lái)就包含兩大部分:偵測(cè)數(shù)據(jù)變化、依賴收集,了解這兩個(gè)知識(shí)點(diǎn)就了解到了數(shù)據(jù)響應(yīng)式原理的精華。
一、偵測(cè)數(shù)據(jù)變化
能夠幀聽(tīng)到數(shù)據(jù)變化是數(shù)據(jù)響應(yīng)式原理的前提,因?yàn)閿?shù)據(jù)響應(yīng)式正是基于監(jiān)聽(tīng)到數(shù)據(jù)變化后來(lái)觸發(fā)一系列的更新操作。本次介紹數(shù)據(jù)響應(yīng)式原理將基于Vue2.x進(jìn)行,其將數(shù)據(jù)變?yōu)榭杀粋蓽y(cè)數(shù)據(jù)時(shí)主要采用了Object.defineProperty()。
1.1 非數(shù)組對(duì)象
下面先舉一個(gè)非數(shù)組對(duì)象的例子
- const obj = {
- a: {
- m: {
- n: 5
- }
- },
- b: 10
- };
觀察上面的對(duì)象,可以發(fā)現(xiàn)其是存在包含關(guān)系的(即一個(gè)對(duì)象中可能包含另一個(gè)對(duì)象),那么自然會(huì)想到通過(guò)遞歸的方式實(shí)現(xiàn),在Vue中為了保證代碼較高的可讀性,引入了三個(gè)模塊實(shí)現(xiàn)該邏輯:observe、Observer、defineReactive,其調(diào)用關(guān)系如下所示:
1.1.1 observe
這個(gè)函數(shù)是幀聽(tīng)數(shù)據(jù)變化的入口文件,通過(guò)調(diào)用該函數(shù)一方面觸發(fā)了其幀聽(tīng)對(duì)象數(shù)據(jù)變化的能力;另一方面定義了何時(shí)遞歸到最內(nèi)層的終止條件。
- import Observer from './Observer';
- export default function (value) {
- // 如果value不是對(duì)象,什么都不做(表示該遞歸到的是基本類型,其變化可被幀聽(tīng)的)
- if (typeof value !== 'object') {
- return;
- }
- // Observer實(shí)例
- let ob;
- // __ob__是value上的屬性,其值就是對(duì)應(yīng)的Observer實(shí)例(表示其已經(jīng)是可幀聽(tīng)的狀態(tài))
- if (typeof value.__ob__ !== 'undefined') {
- ob = value.__ob__;
- }
- else {
- // 是對(duì)象且該上屬性還是未能夠幀聽(tīng)狀態(tài)的
- ob = new Observer(value);
- }
- return ob;
- }
1.1.2 Observer
這個(gè)函數(shù)的目的主要有兩個(gè):一個(gè)是將該實(shí)例掛載到該對(duì)象value的__ob__屬性上(observe上用到了該屬性,通過(guò)判斷是否有該屬性判斷是否已經(jīng)屬于幀聽(tīng)狀態(tài));另一個(gè)是遍歷該對(duì)象上的所有屬性,然后將該屬性均變?yōu)榭蓭?tīng)的(通過(guò)調(diào)用defineReactive實(shí)現(xiàn))。
- export default class Observer {
- constructor(value) {
- // 給實(shí)例添加__ob__屬性
- def(value, '__ob__', this, false);
- // 檢查是數(shù)組還是對(duì)象
- if (!Array.isArray(value)) {
- // 若為對(duì)象,則進(jìn)行遍歷,將其上的屬性變?yōu)轫憫?yīng)式的
- this.walk(value);
- }
- }
- // 對(duì)于對(duì)象上的屬性進(jìn)行遍歷,將其變?yōu)轫憫?yīng)式的
- walk(value) {
- for (let key in value) {
- defineReactive(value, key);
- }
- }
- }
1.1.3 defineReactive
這個(gè)方法主要是將Object.defineProperty封裝到一個(gè)函數(shù)中,做這一步操作的原因是因?yàn)镺bject.defineProperty設(shè)置set屬性時(shí)需要一個(gè)臨時(shí)變量來(lái)存儲(chǔ)變化前的值,通過(guò)封裝利用閉包的思想引入val,這樣就不需要在函數(shù)外面再設(shè)置臨時(shí)變量了。
- export default function defineReactive(data, key, val) {
- if (arguments.length === 2) {
- val = data[key];
- }
- // 子元素要進(jìn)行observe,至此形成了遞歸
- let childOb = observe(val);
- Object.defineProperty(data, key, {
- // 可枚舉
- enumerable: true,
- // 可配置
- configurable: true,
- // getter
- get() {
- console.log(`訪問(wèn)${key}屬性`);
- return val;
- },
- // setter
- set(newValue) {
- console.log(`改變${key}的屬性為${newValue}`);
- if (val === newValue) {
- return;
- }
- val = newValue;
- // 當(dāng)設(shè)置了新值,這個(gè)新值也要被observe
- childOb = observe(newValue);
- }
- });
- }
1.2 數(shù)組
Object.defineProperty不能直接監(jiān)聽(tīng)數(shù)組內(nèi)部的變化,那么數(shù)組內(nèi)容變化應(yīng)該怎么操作呢?Vue主要采用的是改裝數(shù)組方法的方式(push、pop、shift、unshift、splice、sort、reverse),在保留其原有功能的前提下,將其新添加的項(xiàng)變?yōu)轫憫?yīng)式的。
- // array.js文件
- // 得到Array的原型
- const arrayPrototype = Array.prototype;
- // 以Array.prototype為原型創(chuàng)建arrayMethods對(duì)象,并暴露
- export const arrayMethods = Object.create(arrayPrototype);
- // 要被改寫的7個(gè)數(shù)組方法
- const methodsNeedChange = [
- 'push',
- 'pop',
- 'shift',
- 'unshift',
- 'splice',
- 'sort',
- 'reverse'
- ];
- methodsNeedChange.forEach(methodName => {
- //備份原來(lái)的方法
- const original = arrayMethods[methodName];
- // 定義新的方法
- def(arrayMethods, methodName, function () {
- // 恢復(fù)原來(lái)的功能
- const result = original.apply(this, arguments);
- // 將類數(shù)組對(duì)象轉(zhuǎn)換為數(shù)組
- const args = [...arguments];
- // 數(shù)組不會(huì)是最外層,所以其上已經(jīng)添加了Observer實(shí)例
- const ob = this.__ob__;
- // push/unshift/splice會(huì)插入新項(xiàng),需要將插入的新項(xiàng)變成observe的
- let inserted = [];
- switch (methodName) {
- case 'push':
- case 'unshift': {
- inserted = args;
- break;
- }
- case 'splice': {
- inserted = args.slice(2);
- break;
- }
- }
- // 對(duì)于有插入項(xiàng)的,讓新項(xiàng)變?yōu)轫憫?yīng)的
- if (inserted.length) {
- ob.observeArray(inserted);
- }
- ob.dep.notify();
- return result;
- }, false);
- });
除了改裝其原有數(shù)組方法外,Observer函數(shù)中也將增加對(duì)數(shù)組的處理邏輯。
- export default class Observer {
- constructor(value) {
- // 給實(shí)例添加__ob__屬性
- def(value, '__ob__', this, false);
- // 檢查是數(shù)組還是對(duì)象
- if (Array.isArray(value)) {
- // 改變數(shù)組的原型為新改裝的內(nèi)容
- Object.setPrototypeOf(value, arrayMethods);
- // 讓這個(gè)數(shù)組變?yōu)閛bserve
- this.observeArray(value);
- }
- else {
- // 若為對(duì)象,則進(jìn)行遍歷,將其上的屬性變?yōu)轫憫?yīng)式的
- this.walk(value);
- }
- }
- // 對(duì)于對(duì)象上的屬性進(jìn)行遍歷,將其變?yōu)轫憫?yīng)式的
- walk(value) {
- for (let key in value) {
- defineReactive(value, key);
- }
- }
- // 數(shù)組的特殊遍歷
- observeArray(arr) {
- for (let i = 0, l = arr.length; i < l; i++) {
- // 逐項(xiàng)進(jìn)行observe
- observe(arr[i]);
- }
- }
- }
二、依賴收集
目前對(duì)象中所有的屬性已經(jīng)變成可幀聽(tīng)狀態(tài),下一步就進(jìn)入了依賴收集階段,其整個(gè)流程如下所示:
其實(shí)看了這張神圖后,由于能力有限還不是很理解,經(jīng)過(guò)自己的拆分,認(rèn)為可以分成兩個(gè)步驟去理解。
1.getter中(Object.defineProperty中的get屬性)進(jìn)行收集依賴后的狀態(tài)
2. 緊接著就是觸發(fā)依賴,該過(guò)程是在setter中進(jìn)行,當(dāng)觸發(fā)依賴時(shí)所存儲(chǔ)在Dep中的所有Watcher均會(huì)被通知并執(zhí)行,通知其關(guān)聯(lián)的組件更新,例如數(shù)據(jù)更新的位置是與Dep1所關(guān)聯(lián)的數(shù)據(jù),則其上的Watcher1、Watcher2、WatcherN均會(huì)被通知并執(zhí)行。
說(shuō)了這么多,其中最核心的內(nèi)容無(wú)外乎Dep類、Watcher類、defineReactive函數(shù)中的set和get函數(shù)。
2.1 Dep類
Dep類用于管理依賴,包含依賴的添加、刪除、發(fā)送消息,是一個(gè)典型的觀察者模式。
- export default class Dep {
- constructor() {
- console.log('DEP構(gòu)造器');
- // 數(shù)組存儲(chǔ)自己的訂閱者,這是Watcher實(shí)例
- this.subs = [];
- }
- // 添加訂閱
- addSub(sub) {
- this.subs.push(sub);
- }
- // 添加依賴
- depend() {
- // Dep.target指定的全局的位置
- if (Dep.target) {
- this.addSub(Dep.target);
- }
- }
- // 通知更新
- notify() {
- const subs = this.subs.slice();
- for (let i = 0, l = subs.length; i < l; i++) {
- subs[i].update();
- }
- }
- }
2.2 Watcher類
Watcher類的實(shí)例就是依賴,在其實(shí)例化階段會(huì)作為依賴存儲(chǔ)到Dep中,在對(duì)應(yīng)的數(shù)據(jù)改變時(shí)會(huì)更新與該數(shù)據(jù)相關(guān)的Watcher實(shí)例,進(jìn)行對(duì)應(yīng)任務(wù)的執(zhí)行,更新對(duì)應(yīng)組件。
- export default class Watcher {
- constructor(target, expression, callback) {
- console.log('Watcher構(gòu)造器');
- this.target = target;
- this.getter = parsePath(expression);
- this.callback = callback;
- this.value = this.get();
- }
- update() {
- this.run();
- }
- get() {
- // 進(jìn)入依賴收集階段,讓全局的Dep.target設(shè)置為Watcher本身,就進(jìn)入依賴收集階段
- Dep.target = this;
- const obj = this.target;
- let value;
- try {
- value = this.getter(obj);
- }
- finally {
- Dep.target = null;
- }
- return value;
- }
- run() {
- this.getAndInvoke(this.callback);
- }
- getAndInvoke(cb) {
- const value = this.get();
- if (value !== this.value || typeof value === 'object') {
- const oldValue = this.value;
- this.value = value;
- cb.call(this.target, value, oldValue);
- }
- }
- }
- function parsePath(str) {
- const segments = str.split('.');
- return obj =>{
- for (let i = 0; i < segments.length; i++) {
- if (!obj) {
- return;
- }
- obj = obj[segments[i]];
- }
- return obj;
- };
- }
2.3 defineReactive函數(shù)中的set和get函數(shù)
Object.defineProperty中的getter階段進(jìn)行收集依賴,setter階段觸發(fā)依賴。
- export default function defineReactive(data, key, val) {
- const dep = new Dep();
- if (arguments.length === 2) {
- val = data[key];
- }
- // 子元素要進(jìn)行observe,至此形成了遞歸
- let childOb = observe(val);
- Object.defineProperty(data, key, {
- // 可枚舉
- enumerable: true,
- // 可配置
- configurable: true,
- // getter
- get() {
- console.log(`訪問(wèn)${key}屬性`);
- // 如果現(xiàn)在處于依賴收集階段
- if (Dep.target) {
- dep.depend();
- // 其子元素存在的時(shí)候也要進(jìn)行依賴收集(個(gè)人認(rèn)為主要是針對(duì)數(shù)組)
- if (childOb) {
- childOb.dep.depend();
- }
- }
- return val;
- },
- // setter
- set(newValue) {
- console.log(`改變${key}的屬性為${newValue}`);
- if (val === newValue) {
- return;
- }
- val = newValue;
- // 當(dāng)設(shè)置了新值,這個(gè)新值也要被observe
- childOb = observe(newValue);
- // 發(fā)布訂閱模式,通知更新
- dep.notify();
- }
- });
- }