用戶
 找回密碼
 立即注冊

QQ登錄

只需一步,快速開始

掃一掃,登錄網站

小程序社區 首頁 教程 實戰教程 查看內容

湖人vs国王:如何使用不到50行代碼實現一個小而美的依賴收集庫?

湖人vs爵士季后赛 www.nbgeh.club Rolan 2019-12-2 00:02

現代web開發,大多數都遵循著視圖與邏輯分離的開發原則,一反面使得代碼更加易懂且易擴展,另一方面帶來的問題就是如何優雅的管理數據。因而,社區誕生了很多優秀的狀態管理庫,比如為React而生的Redux,專為Vue服務 ...

現代web開發,大多數都遵循著視圖與邏輯分離的開發原則,一反面使得代碼更加易懂且易擴展,另一方面帶來的問題就是如何優雅的管理數據。因而,社區誕生了很多優秀的狀態管理庫,比如為React而生的 Redux ,專為 Vue 服務的 Vuex ,還有不限定框架的 Mobx 等等。在為使用這些庫提升開發效率而叫好的同時,我覺得我們也應該從內部去真正的了解它們的核心原理,就比如今天這篇文章的主題 依賴收集 ,就是其中的一個很大的核心知識。這篇文章將會帶您一步一步的以最少的代碼去實現一個小而美的依賴收集庫,同時給您展現如何將這個庫運用到湖人vs爵士季后赛中去實現跨頁面的狀態共享。

二 實現過程

1. 基本原理

依賴收集的基本原理可以概括為以下3步:

  1. 創建一個可觀察(observable)對象
  2. 視圖或者函數(effect)引用這個對象的某個屬性,觸發依賴收集
  3. 改變數據,視圖或者函數自動更新或運行

我們要實現的例子:

import { observable, observe } from "micro-reaction";

const ob = observable({
    a: 1
});

observe(() => console.log(ob.a));

// logs: 1
// logs: 2
ob.a = 2;
復制代碼

下面開始我將一步一步的進行實現過程講解

2. 創建一個可觀察對象

首先,我們需要創建一個可觀察對象,其本質就是將傳入的對象進行代理,并且返回這個代理對象,這里我們使用 es6 的 Proxy 來修改對象的一些行為,從而實現在返回真正對象前作一些攔截操作。

我們定義了一個名叫 observable 方法來代理對象,代碼如下:

export function observable(obj = {}) {
    return createObservable(obj)
}

function createObservable(obj) {
    const proxyObj = new Proxy(obj, handlers());
    return proxyObj
}
復制代碼

可以看到 observable 方法內部就是通過 new Proxy(obj,handler) 生成一個代理對象,傳參分別是原始對象和代理操作方法 handlers , handlers 返回一個對象,定義了對象的原始方法,例如 get 、 set ,通過重新定義這兩個方法,我們可以修改對象的行為,從而完成代理操作,我們來看看 handlers 方法。

function handlers() {
    return {
        get: (target, key, receiver) => {
            const result = Reflect.get(target, key, receiver);
            return result
        },
        set: (target, key, value, receiver) => {
            const result = Reflect.set(target, key, value, receiver);
            return result
        }
    }
}
復制代碼

如上,我們在 get 和 set 方法里面沒有做任何操作,取值賦值操作都是原樣返回。

3. 關聯副作用函數effect

完成了對數據的初始定義,我們明確下我們的目的,我們的最終目的是數據改變,副作用函數 effect 自動運行,而這其中的關鍵就是必須有個地方引用我們創建的代理對象,從而觸發代理對象內部的 get 或者 set 方法,方便我們在這兩個方法內部做一些依賴收集和依賴執行的工作。

因而,這里我們定義了一個 observe 方法,參數是一個 Function ,我們先看看這個方法的實現:

export function observe(fn) {
    <!--這一行可以先忽略,后面會有介紹-->
    storeFns.push(fn);
    <!--Reflect.apply()就相當于fn.call(this.arguments)-->
    Reflect.apply(fn, this, arguments)
}
復制代碼

可以看到,內部執行了傳入的函數,而我們傳入的函數是 () => console.log(ob.a.b) ,函數執行,輸出 ob.a ,引用了代理對象的 a 屬性值,就觸發了代理對象內部的 get 方法。 在 get方法內部我們就可以進行依賴收集。

function handlers() {
    return {
        get: (target, key, receiver) => {
            const result = Reflect.get(target, key, receiver);
            <!--觸發依賴收集-->
            depsCollect({ target, key })
            return result
        },
        set: (target, key, value, receiver) => {
            const result = Reflect.set(target, key, value, receiver);
            return result
        }
    }
}
復制代碼

depsCollect 依賴收集方法需要做的操作就是將當前的依賴也就是 () => console.log(ob.a)這個函數 fn 保存起來,那 fn 怎么傳過來呢? get 方法本身的入參是沒有這個 fn 的,回顧之前的 observe 方法,這個方法有傳入 fn ,其中內部有個 storeFns.push(fn) 這樣的操作,就是通過一個數組將當前依賴函數臨時收集起來??曬饈占揮?,我們還要和對應的屬性進行映射,以便后續某個屬性變化時,我們能夠找出對應的 effect ,故我們定義了一個 Map 對象來存儲相應的映射關系,那需要怎樣的一個映射關系呢?一個對象有多個屬性,每個屬性可能都有對應的 effect ,結構看起來應該是這樣的:

{
    obj:{
        "key-1":fn1,
        "key-2":fn2,
        ....
    }
}
復制代碼

我們定義了一個全局變量 storeReactions 來存儲整個映射關系,它的 key 是 obj ,就是原始對象, obj 的值也是個 Map 結構,存儲了其屬性和 effect 的映射關系。我們的最終目的其實也就是建立一個這樣的關系。理清楚了數據存儲,再來看看我們的 depsCollect 方法,其實就是將臨時保存在 storeFns 里面的函數取出和屬性 key 映射。

// 存儲依賴對象
const storeReactions = new WeakMap();
// 中轉數組,用來臨時存儲當前可觀察對象的反應函數,完成收集之后立即釋放
const storeFns = [];
function depsCollect({ target, key }) {
    const fn = storeFns[storeFns.length - 1];
    if (fn) {
        const mapReactions = storeReactions.get(target);
        if (!mapReactions.get(key)) {
            mapReactions.set(key, fn)
        }
    }
}
復制代碼

至此,我們的依賴收集算是完成了,接下來就是要實現如何監聽數據改變,對應 effect 自動運行了。

4. 數據變更,effect自動運行

數據變更,就是重新設置數據,類似 a=2 的操作,就會觸發代理對象里面的 set 方法,我們只需要在 set 方法里面取出對應的 effect 運行即可。

set: (target, key, value, receiver) => {
        const result = Reflect.set(target, key, value, receiver);
        executeReactions({ target, key })
        return result
    }
    
function executeReactions({ target, key }) {
    <!-- 一時看不懂的,回顧下我們的映射關系 -->
    const mapReactions = storeReactions.get(target);
    if (mapReactions.has(key)) {
        const reaction = mapReactions.get(key);
        reaction();
    }
}
復制代碼

ok,我們的例子的實現過程講解完了,整個實現過程還是很清晰的,最后看看我們的整個代碼,去掉空行不到50行代碼。

const storeReactions = new WeakMap(),storeFns = [];

export function observable(obj = {}) {
  const proxyObj = new Proxy(obj, handlers());
  storeReactions.set(obj, new Map());
  return proxyObj
}

export function observe(fn) {
  if (storeFns.indexOf(fn) === -1) {
    try {
      storeFns.push(fn);
      Reflect.apply(fn, this, arguments)
    } finally {
      storeFns.pop()
    }
  }
}

function handlers() {
  return {
    get: (target, key, receiver) => {
      depsCollect({ target, key })
      return Reflect.get(target, key, receiver)
    },
    set: (target, key, value, receiver) => {
      Reflect.set(target, key, value, receiver)
      executeReactions({ target, key })
    }
  }
}

function depsCollect({ target, key }) {
  const fn = storeFns[storeFns.length - 1];
  if (fn) {
    const mapReactions = storeReactions.get(target);
    if (!mapReactions.get(key)) {
      mapReactions.set(key, fn)
    }
  }
}

function executeReactions({ target, key }) {
  const mapReactions = storeReactions.get(target);
  if (mapReactions.has(key)) {
    const reaction = mapReactions.get(key);
    reaction();
  }
}
復制代碼

5. 多層級數據結構

到目前為止,我們實現的還只能觀察單級的對象,如果一個對象的層級深了,類似 ob.a.b 的結構,我們的庫就無法觀察數據的變動, effect 也不會自動運行。那如何支持呢?核心原理就是在 get 方法里面判斷返回的值,如果返回的值是個對象,就遞歸調用 observable 方法,遞歸調用完,接著運行 observe 方法就會構建出完整的一個屬性 key 和反應 effect 的映射關系。

function handlers() {
    return {
        get: (target, key, receiver) => {
            const result = Reflect.get(target, key, receiver);
            depsCollect({ target, key })
            if (typeof result === 'object' && result != null && storeFns.length > 0) {
                return observable(result)
            }
            return result
        }
    }
}
復制代碼

回到 ob.a.b 這樣的結構,此時實際的代理對象應該是這樣的 {proxy(proxy(c))} ,如果這個時候我們去修改數據,比如 ob.a.b = 2 這樣。

ob.a.b = 2 的運行過程會是怎樣?要知道js這門語言是先編譯后執行的,所以js引擎首先會去分析這段代碼(編譯階段),先分析左邊的表達式 ob.a.b ,故先會編譯 ob.a ,觸發了第一次 get 方法,在 get 方法中, result 得到的值是個對象,如果按照上述代碼,又去重新觀察這個對象,會導致 observe 方法中構建好的映射關系丟失,其中就是對象 {b:1} 中 key 為 b 對應的 fn 丟失,因為我們存儲 fn 是在 observe 方法中執行的,那怎么辦呢?方法是我們應該在第一次 observable 方法執行的時候,將每一個 key 對應的代理對象都保存起來,在賦值操作再一次觸發 get 方法的時候,如果已經代理過,直接返回就行,不需要重新代理。

// 存儲代理對象
const storeProxys = new WeakMap();
export function observable(obj = {}) {
    return storeProxys.get(obj) || createObservable(obj)
}
function createObservable(obj) {
    const proxyObj = new Proxy(obj, handlers());
    storeReactions.set(obj, new Map())
    storeProxys.set(obj, proxyObj)
    return proxyObj
}
function handlers() {
    return {
        get: (target, key, receiver) => {
            const result = Reflect.get(target, key, receiver);
            depsCollect({ target, key })
            <!--如果代理存儲中有某個key對應的代理直接返回即可-->
            const observableResult = storeProxys.get(result);
            if (typeof result === 'object' && result != null && storeFns.length > 0) {
                return observable(result)
            }
            return observableResult || result
        }
    }
}
復制代碼

如此, ob.a.b = 2 ,控制臺就會依次輸出 1 和 2 ,另外說一句,數組也是對象,故動態增加數組的值或者賦值操作都能觸發響應的 effect 。

const ob = observable({
  a: {
    b: 1,
    c: []
  }
});

observe(() => console.log(ob.a.c.join(", ")));
//logs: 2
ob.a.c.push(2);
復制代碼

三 如何結合小程序使用

全部完整代碼我已發布到我的github中,名字叫做 micro-reaction ,這個庫完全無依賴的,純粹的,故可以為其它界面框架狀態管理提供能量,由于小程序跨頁面狀態共享相關的庫不多,故這里以小程序舉例,如何結合 micro-reaction 實現跨頁面狀態共享。

1. 核心原理

描述下場景,有兩個頁面 A 和 B ,全局數據 C , A 和 B 都引用了 C ,之后,頁面 A 中某個交互改變了 C , A 和 B 都需要自動渲染頁面。結合我們的庫, C 肯定是需要 observable的, observe 方法傳入的 fn 是會動態執行的,小程序渲染頁面的方式是 setData 方法,故 observe 方法里面肯定執行了 setData() ,因而只要我們在 observe 方法里面引用 C ,就會觸發依賴收集,從而在下次 C 改變之后, setData 方法重新運行渲染頁面。

2. 關鍵步驟

首先,我們需要拿到每個小程序頁面的 this 對象,以便自動渲染使用,故我們需要代理 Page方法里面傳入的參數,我們定一個了 mapToData 方法來代理,代碼如下:

<!--全局數據-->
import homeStore from "../../store"
<!--將數據映射到頁面,同時出發依賴收集,保存頁面棧對象-->
import { mapToData } from "micro-reaction-miniprogram"
const connect = mapToData((store) => ({ count: store.credits.count }), 'home')

Page(connect({
  onTap(e) {
    homeStore.credits.count++
  },
  onJump(e) {
    wx.navigateTo({
      url: "/pages/logs/logs"
    })
  }
}))
復制代碼

mapToData 方法返回一個函數, function mapToData(fn,name){return function(pageOpt){}},這里用到了閉包,外部函數為我們傳入的函數,作用是將全局數據映射到我們的頁面 data 中并觸發依賴收集,內部函數傳入的參數為小程序頁面本身的參數,里面包含了小程序的生命周期方法,因而我們就可以在內部重寫這些方法,并拿到當前頁面對象并存儲起來供下一次頁面渲染使用。

import { STORE_TREE } from "./createStore"
import { observe, observable } from 'micro-reaction';

function mapToData(fn, name) {
  return function (pageOpt) {
    const { onLoad } = pageOpt;
    pageOpt.onLoad = function (opt) {
      const self = this
      const dataFromStore = fn.call(self, STORE_TREE[name], opt)
      self.setData(Object.assign({}, self.data, dataFromStore))

      observe(() => {
        <!--映射方法執行,觸發依賴收集-->
        const dataFromStore = fn.call(self, STORE_TREE[name], opt)
        self.setData(Object.assign({}, self.data, dataFromStore))
      })

      onLoad && onLoad.call(self, opt)
    }
    return pageOpt
  }
}

export { mapToData, observable }
復制代碼

然后,頁面 A 改變了數據 C , observe 方法參數 fn 自動執行,觸發 this.setData 方法,從而頁面重新渲染,完整代碼點擊 micro-reaction-miniprogram ,也可以點擊查看 在線Demo。

四 總結

希望我的文章能夠讓您對依賴收集的認識更深,以及如何舉一反三的學會使用,此外,最近在學習周愛民老師的《JavaScript核心原理解析》這門課程,其中有句話對我觸動很深,引用的是金庸射雕英雄傳里面的文本: 教而不得其法,學而不得其道 ,意思就是說,傳授的人沒有用對方法,學習的人就不會學懂,其實我自己對學習的方法也一直都很困惑,前端發展越來越快,什么 SSR ,什么 serverless ,什么 前端工程化 ,什么 搭建系統 各種知識概念越來越多,不知道該怎么學習,說不焦慮是不可能的,但堅信只有一個良好的基礎,理解一些技術的本質,才能在快速發展的前端技術浪潮中,不至于被沖走,與局共勉!

最后,在貼下文章提及的兩個庫,歡迎star試用,提pr,感謝~

依賴收集庫 micro-reaction

小程序狀態管理庫 micro-reaction-miniprogram

分享至 : QQ空間
收藏
原作者: skinner 來自: 掘金
猎鱼达人3d万炮视频 在内蒙干什么赚钱 密友聊天赚钱app 大乐透开奖现场17040 腾讯分分彩稳定玩法 14场胜负彩预测新浪 电脑版贵州11选5 天津快乐10分选号走势图 创业赚钱项目冬季 福彩3d跨度走势图表 黄金棋牌游戏大厅 宝马2系旅行版和奔驰b级 广西十三水棋牌游戏 河南22选5走势图带连线 北京单场参考数据 河北快乐扑克4走势图