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