我所認識的前端數據流

歷史老是在新思想的火花碰撞中演進,從React的橫空出世,前端開始慢慢從jQuery的蠻荒時代過渡到三大陣營「羣雄逐鹿」,幾大框架都是在解決數據層和視圖層之間的驅動關係。當數據發生變化了以後,由框架自身來控制對視圖層的渲染操做,而問題的關鍵剛好就在於依賴收集,如何才能知道數據發生變化了呢?因此Vue使用了defineProperty直接劫持數據的原始操做、angular使用髒檢查監聽全部可能發生的數據變動,React約定只能使用自帶的setStateAPI來觸發數據的變動。而也就是處於這個方興未艾的時代,咱們才慢慢開始思考數據流這個概念。javascript

提出問題

如今咱們有一個原始對象數據,想要在這個數據發生變化的時候,有一個回調方法能夠直接被觸發執行,例如:前端

var obj =  {
        name:'Jack'
    }
    someFunc(function(){
        //當obj的值被改變時,想要自動觸發此函數
        console.log(obj.name);
    });
    obj.name = 'Nico';//或者執行某個其餘的改變值的函數
複製代碼

固然,咱們這個對象的數據類型確定是不固定的,也有多是Array、Map、Set之類的,當它執行了一些原生的方法(好比push、splice之類的)導致它的值發生了變化,咱們都須要在約定的回調函數獲得執行。java

解決問題

咱們暫且不論業內流行的那些數據流框架,若是是咱們本身來作,應該如何實現。若是不依賴任何api的話,咱們能夠直接寫一個簡單的觀察者模型,若是使用defineProperty的話,能夠直接劫持set、get方法,下面將對這種方式作簡單的code處理。react

  • 觀察者模式
//假設咱們的數據對象是類型固定的{name:string}
function Observer(obj) {
    var self = this;
    this._listener = {};
    Object.keys(obj).forEach(function (key) {
        self._listener[key] = [];
    });
}

Observer.prototype.subscribe = function (key, func) {
    if (!this._listener[key]) {
        this._listener[key] = [];
    }
    this._listener[key].push(func)
}

Observer.prototype.publish = function () {
    var key = Array.prototype.slice.call(arguments);
    var clients = this._listener[key];
    for (var i = 0; i < clients.length; i++) {
        clients[i].apply(this, arguments);
    }
}

var object = {name: 'Jack'};
var observer = new Observer(object);
observer.subscribe('name', function () {
    console.log('Hello '+object.name);
});

function changeName(val) {
    if (object.name !== val) {
        object.name = val;
        observer.publish('name');
    }
}

changeName('Nico');//Hello Nico

複製代碼

你們這裏並不須要吐槽代碼的粗糙,總的來講這段簡陋的代碼能夠實現咱們想要的效果,能夠做爲一種解決問題的思路:直接訂閱數據對象的某個屬性,當這個屬性的值發生變化時,執行訂閱的回調。雖然看起來是千瘡百孔,但最大的問題是,這都是假設在數據結構固定的基礎上的,若是結構變了,一切都將變得不可控制。git

  • defineProperty
var object = {name: 'Jack'};
var value;
Object.defineProperty(object, 'name', {
    enumerable: true,
    configurable: true,
    set: function (val) {
        value = val;
        reaction();
    },
    get: function () {
        return value;
    }
});

function reaction() {
    console.log("Hello " + object.name);
}

object.name = "Nico";//Hello Nico
複製代碼

一樣也是一段粗暴的代碼,相比於上面,這個代碼邏輯的實現彷佛少了很多,不過也有一樣的問題,當咱們數據結構發生變化時,整個實現邏輯都得從新寫。github

既然咱們短期內沒法在當下給出一個萬能的解決方法,不妨看看業內的大佬是如何處理這個問題的,天然而然就不得不提到ReduxMobx,或許,看到這裏您就又要笑了,不過這並不影響我接下來對它們的理解闡述,也但願可以對您有所啓發。算法

Redux的哲學

總所周知Redux是由Facebook開源的一個數據流解決方案框架,幾經FluxReFlux的洗禮儼然已經是套成熟的框架,不過它的初衷是爲了給React量身打造一個數據流解決方案的,誰讓React堅稱本身只是一個View層的處理呢。但從結果來看,Redux並不是必定要結合React來使用,它提出的是一種函數式模塊式的數據流設計方案,咱們徹底能夠配合jQuery或者其餘來一塊兒使用。咱們經過上述例子使用Redux來使用,再次來領悟一下它的魅力:typescript

var Redux = require('redux');

var object = {
    name: 'Jack'
}
var store = Redux.createStore(function (initState = object, action) {
    switch (action.type) {
        case 'change':
            return Object.assign({}, initState, {
                name: action.value
            });
        default:
            return initState;
    }
});

store.subscribe(function () {
    console.log('Hello ' + store.getState().name);
});

store.dispatch({
    type: 'change',
    value: 'Nico'
});
複製代碼

可能你們會對這段代碼再熟悉不過了。這種函數式、模塊式的代碼風格使得整個數據流的處理方式看起來十分的優雅,這也是Redux最具備魅力的地方。仔細觀摩一下,總體的思路無非也是先傳入咱們的原始數據對象object,而後將訂閱一個回調函數(subscribe),最後執行數據的更改(dispatch)。可能與咱們上述所說的觀察者模式不一樣之處在於,這裏會直接把數據更改的操做當作初始化的參數傳入到Redux裏面(固然,Redux稱此爲reducer)。那咱們是否也能按照這個思想將上面那段代碼改造一下,使其變得能夠像Redux那樣使用呢:redux

//定義store
function createStore(reducer) {
    let currentState = undefined;
    let listeners = [];

    function dispatch(action) {
        currentState = reducer(currentState, action);
        for (var i in listeners) {
            listeners[i]();
        }
        return currentState;
    }

    dispatch({
        type: 'INIT'
    });
    return {
        dispatch: dispatch,
        subscribe: function (callback) {
            listeners.push(callback);
        },
        getState: function () {
            return currentState;
        }
    }
}

//建立store
let store = createStore(function (state = {name: "Jack"}, action) {
    switch (action.type) {
        case 'change':
            return Object.assign({}, state, {
                name: action.value
            });
        default:
            return state;
    }
});
store.subscribe(function () {
    console.log('Hello '+store.getState().name);//Hello Nico
})

store.dispatch({
    type: 'change',
    value: 'Nico'
});
複製代碼

瞟一眼,不,或許你真的沒有看錯,這二十行左右的代碼確實使其能夠像Redux同樣運行(其實Redux源碼去掉註釋可能也就才兩百行左右,不過裏面會多一些像是combineReducers以及供插件使用的applyMiddleware之類的接口),總體思路跟咱們上述所說的觀察者模式幾乎沒有區別,難能難得的是,誰又能想到能夠以這樣的一種形式來組織代碼呢。api

理性的思考一下,這樣處理的確能夠解決咱們一開始提出的問題,但隨着而來新的「問題」(純屬我的看法),第一,咱們每一次dispatch都會致使回調函數被觸發,這在React裏面使用或許並非問題,但若是結合jQuery之類的沒有虛擬DOM的diff算法框架來使用,這種無差異的觸發方式就顯得有點難受了;第二,statereducer裏面結構被直接修改,也會致使一些意想不到的bug,從上述代碼裏面便可見端倪,這是一個mutable的數據,因此Redux一再強調不能直接修改state,應該是經過返回一個新數據的形式來進行。這種一步到位隔離數據操做反作用的思想的確能解決不少問題,但全部的數據操做都將要使用一個一個的reducer來進行,這種「龐大的」代碼組織方式着實讓人有點不舒服。

Mobx的實現

Mobx一樣是一個幾經戰火洗禮的庫,咱們也首先來看一下用它來解決咱們的問題:

var Mobx = require('mobx');
var object = Mobx.observable({
    name: 'Jack'
});
Mobx.autorun(function () {
    console.log('Hello ' + object.name);
});
object.name = 'Nico'
複製代碼

相比於上述Redux的代碼,最大的體會就是代碼量大大的減小了,配置好了以後能夠直接操做對象就能在回調獲得觸發了。可想而知,必定是經過劫持數據的原始賦值方式來進行,可是相比於咱們上述所說的definePropertyMobx顯然有更加健全的數據處理方式,不過萬變不離其宗,這句話提及來很簡單,也能夠徹底就此一律而論,可是裏面的技術細節的實現仍是很是講究的。關於Mobx源碼的講解,網上確定是有很多文章了,我也是幾經波折,從從零開始用 proxy 實現 mobx這篇文章中才慢慢領悟到其中奧祕。說到這裏,插一句題外話,實在是想強烈給你們推薦一下大佬@ascoders博客

固然你確定猜到了我接下來要爲你展現什麼了,看完上面代碼,我也相信你內心也是沒有什麼壓力的,都是從最基礎簡單的,而對Mobx的分解也「力圖」一如既往。

從能知足咱們最基礎使用的開始,顯而易見,咱們只須要兩個函數便可,一個是監聽原始數據對象,會把原始數據對象看成參數傳進去,裏面會對其的賦值作劫持操做,咱們姑且稱之爲observable;同時還須要一個函數,把咱們須要進行的回調函數傳進去,當數據發生變化的時候這個回調函數將會被觸發,這裏稱之爲observe

那麼以上兩個函數何以可以實現咱們的需求呢?整個數據流框架的核心在於依賴收集觸發回調。依賴收集確定是綁定在數據的get方法上,也就說只要執行了取數,咱們就能夠知道哪一個數據的哪一個字段須要作「監聽」,用於觸發回調:

new Proxy(object,{
        /** * * @param target 須要取數的原始數據對象 * @param key 須要取數的原始數據對象的key值 * @param receiver */
    get(target, key, receiver) {
        let value = Reflect.get(target, key, receiver);
        //接下來咱們就能夠把這個target+key的關係作一個「監聽」處理
        ...
        return value;
      },
    })
複製代碼

剩下的觸發回調方法確定是在數據的set操做上面,意思便是,當數據被變動了,咱們也根據object+key執行其對應的回調函數:

new Proxy(object,{
    set(target, key, value, receiver) {
        const oldValue = Reflect.get(target, key, receiver);
        const result = Reflect.set(target, key, value, receiver);
        if (value !== oldValue) {
        //根據target+key執行對應的回調函數
         ...
        }
        return result;
      }
    })
複製代碼

(這裏,咱們只把須要觸發的值「存儲」起來,而後在其被賦值的時候觸發,例如咱們的回調裏面只須要objectname屬性,咱們只會在name屬性被變動時才觸發回調,這種方式稱之爲惰性求值)

因爲數據的setget是徹底獨立的兩個操做,想要在set裏面執行get所對應的取值回調函數,因而一個持久化的全局對象運應而生——globalState,它裏面會有一個屬性專門用來作target+key的關係存儲,命名爲objectReactionBindings

class GlobalState {
        public objectReactionBindings = new WeakMap<object,Map<PropertyKey,Set<Reaction>>> ();
    }
複製代碼

暫且能夠沒必要細究這個對象的數據結構,須要知道的是,這裏面會存儲target+key的關係對象,這樣咱們就能完成在get中經過target+key進行依賴收集,而後set中再次經過這個target+key完成所對應的回調觸發。

經過以上分析,咱們的observable函數要怎麼寫已經初見端倪了,還有一個observe的回調應該怎麼「搞」還沒說明,因爲咱們確定得在數據被賦值以前就要知道具體須要「監聽」的是哪些數據,否則當數據被改變了都不知道應該觸發哪些回調,而依賴收集上面已經提到必然是經過get方法的觸發,天然而然,咱們須要在數據初始化的時候就執行一次observe裏面的回調函數,這樣就能完成數據的收集了。

考慮到咱們回調觸發是在set方法中執行的,這個回調將被「掛載」到globalState,爲了擴展其餘的一些操做使這個「回調」更加靈活些,咱們更但願它是一個能夠專門用來觸發回調的「反應」對象,裏面不只會專門存儲這個回調函數,還能夠擴展一些其餘參數的操做,咱們稱之爲Reaction

type IFunc=(...args:any[])=>any;
    class Reaction { 
        private callback:IFunc|null;
        constructor(callback:IFunc){
            this.callback=callback;
        }
    public track(callback?: IFunc) {
    if (!callback) {
      return;
    }
    try {
      callback();
    } finally {
     ...
    }
  }

  public run() {
    if (this.callback) {
      this.callback();
    }
  }
    }
複製代碼

順其天然,咱們的observe函數也能夠寫了,會像這樣處理:

declare type Func=(...args:any[])=>any;
    function observe(callback:Func){
        const reaction = new Reaction(()=>{
            reaction.track(callback);
        });
        reaction.run();
    }
複製代碼

這段代碼裏,reaction被初始化了以後就會執行run方法,這裏執行的邏輯就是咱們上述所提到的對數據的依賴收集在初始化完成以後就立馬執行。

最後,咱們只須要將一開始的setget相關的邏輯補充一下便可:

function observable<T extends object>(obj:T = {} as any):T{
        return new Proxy(obj, {
      get(target, key, receiver) {
        let value = Reflect.get(target, key, receiver);
        bindCurrentReaction(target, key);
        return value;
      },
      set(target, key, value, receiver) {
        const oldValue = Reflect.get(target, key, receiver);
        const result = Reflect.set(target, key, value, receiver);
        if (value !== oldValue) {
          queueRunReactions<T>(target, key);
        }
        return result;
      }
    });
    }
    
    //綁定target+key的reaction到globalState
    function bindCurrentReaction<T extends object>(object: T, key: PropertyKey) {
  const { keyBinder } = getBinder(object, key);
  if (!keyBinder.has(globalState.currentReaction)) {
    keyBinder.add(globalState.currentReaction);
  }
}

//在globalState經過查詢target+key獲得回調並觸發
function queueRunReactions<T extends object>(target: T, key: PropertyKey) {
  const { keyBinder } = getBinder(target, key);
  Array.from(keyBinder).forEach(reaction => {
        reaction.forEach(observer=>{
            observer.run();
        })
  });
}

    
    function getBinder(object: any, key: PropertyKey) {
  let keysForObject = globalState.objectReactionBindings.get(object);
  if (!keysForObject) {
    keysForObject = new Map();
    globalState.objectReactionBindings.set(object, keysForObject);
  }
  let reactionsForKey = keysForObject.get(key);
  if (!reactionsForKey) {
    reactionsForKey = new Set();
    keysForObject.set(key, reactionsForKey);
  }
  return {
    binder: keysForObject,
    keyBinder: reactionsForKey
  };
}
    
複製代碼

固然,實際狀況中還要考慮一些併發執行,debug以及像是對MapSet之類的對象支持,因此實際的代碼和邏輯要遠比上述代碼複雜得多,有興趣能夠去git倉庫看一下源碼。

Mobx一樣也是基於此的實現,只不過Mobx4並不是使用的是Proxy對象作代理處理,而是defineProperty,這使得它須要經過其餘的一些參數對象來完成對數據的採集、綁定。例如常見的Array對象,在Mobx4裏面會對數組的全部操做方式都作劫持處理,這使得其在返回的「新對象」中也能夠像原生對象同樣。

若是說Redux實在讓人用起來有些不爽,那Mobx也並不是天衣無縫,估計它最大的缺點就是,實在是找不到它有什麼缺點了。

最後說到頭,前端裏面不管哪一種數據流工具,都是爲了解決問題而存在的。既然有了數據層的解決方案,剩下的就是打通視圖層的操做了。Vue裏面會經過內置的「數據流」將依賴的DOM節點綁定到具體的數據對象上,以致於它能夠自動完成DOM更新,其本質上說無異於綁定了DOM節點的「Mobx」;React使用數據更新以後的虛擬DOMDiff算法渲染改變部分,本質上說都是爲極大程度上了簡化多餘又繁瑣的視圖操做(否則幹嗎不直接使用jQuery呢)。我我的有點嫌棄Vue繁瑣的綁定式寫法,也不喜歡React的diff算法(這種比較方式在現代瀏覽器中是否真的須要?性能開銷是否是很大啊),理想的方式是,像React同樣使用Vue,不要作多餘的diff處理,也不要多餘框架綁定。

相關文章
相關標籤/搜索