原生小程序狀態管理mpsm

原生小程序狀態管理: mpsm

git地址: mpsm

具有vue+react開發體驗javascript

dvajs的models狀態管理模式, react數據批量更新的特色,以及vue的watch和computed特性, 所有封裝爲一個小程序的狀態管理庫,不只實現了小程序的 【全局狀態管理】 ,解決跨頁通訊,還引入了 【組件圈子】 概念,來實現組件間的數據傳遞,彌補原生小程序組件系統的先天缺陷,擺脫各類父子、兄弟、姐妹、街坊鄰居、七大姑八大姨、遠方表兄弟等等組件間的通訊漿糊困擾。html

數據流

不論是頁面間,仍是組件間,嵌套組件內部,均可以經過簡單的dispach來管理全局狀態或圈子狀態(局部)。 vue

數據流
數據流

使用介紹

徹底兼容原生代碼,已有的業務邏輯代碼,即使不適配也可以使用此庫,不影響已有業務邏輯。java

初始化

//app.js

import {page} from './mpsm/index'
import models from './models/index'

page.init(models, {}, {})

複製代碼

第2、三參數分別爲頁面和組件的公共options,會在每一個頁面或組件生效,對於同名的對象和函數,會進行合併處理。react

頁面或組件註冊

:即只需將Page、Component首字母小寫。git

尾巴:即尾部多調用一次:github

page({ //component
	// ...
})()

複製代碼

方法

方法 說明 備註
page 用於註冊頁面 page()()
component 用於註冊組件 component()()
dispatch 狀態分發 參數object, {type, action, lazy}, 默認lazy: true, 懶更新,表示只更新當前展現頁面的狀態,其它頁面待onShow觸發後再更新
getComponentOps 獲取公共組件配置選項 不包含函數,只是簡單的JSON格式數據
getOps 獲取公共頁面配置選項 不包含函數,只是簡單的JSON格式數據
subscribe 訂閱單一數據源 參數: 'userInfo/setup'
unsubscribe 取消訂閱單一數據源 參數: 'userInfo/setup'
單一數據源的訂閱取消

方法: unsubscribe, subscribe小程序

取消訂閱不使用dva的app.unmodel(),同時,subscription 必須返回 unlisten 方法,用於取消數據訂閱。api

subscribe('userInfo/setup');
unsubscribe('userInfo/setup');
複製代碼

頁面註冊、組件註冊示例

import {dispatch, page, component} from '../../mpsm/index'

page({ // 或者 component
  watch: {
    isLogin(newState, oldState) {
    }
  },
  computed: {
    countComputed(data) {
      return data.count * 2
    }
  },
  data: {
    count: 2
  },
  onLoad() {},
  login() {
    dispatch({
      type: 'userInfo/save',
      payload: {
        isLogin: true
      }
    })
  },
  changeGroupState() {
    this.dispatch({
      type: 'group/index-a-1',
      payload: {
        nameA: 'name'
      }
    })
  },

})(({userInfo}) => {//訂閱全局狀態
  return {
    isLogin: userInfo.isLogin
  }
}, (groups) => {//訂閱圈子狀態,page爲全局圈子,component爲組件所在圈子
  return {
    nameA: groups.nameA && groups.nameA.name || '--'
  }
})
複製代碼
tips:
  1. dispatch用於分發全局狀態,風格與dva保持一致;app

  2. Page和Component實例內置this.dispatch方法,用於分發局部狀態。

三、組件可監聽page的生命週期函數,無需作版本兼容,只需將想要監聽的函數名與page內一直便可,即

// 原生的pageLifetimes可監聽的週期太少,且版本要求高,監聽了onShow,就不須要監聽show了,避免執行兩次
component({
  pageLifetimes: {
      onShow: function () { },
      onHide: function () { },
      onPageScroll: function () { },
    },
})()

複製代碼

目前可監聽的生命週期 ['onShow', 'onHide', 'onResize', 'onPageScroll', 'onTabItemTap', 'onPullDownRefresh', 'onReachBottom']

有了這個功能,就能夠編寫不少自控組件了,好比吸頂效果的導航欄等。

models 全局狀態

export default {
  namespace: '',
  state: {
    
  },
  subscriptions: {

  },
  effects: {

  },
  reducers: {

  }

}

複製代碼

model 屬性

詳情參閱 dva

屬性 說明 備註
namespace 命名空間 必須
state 狀態 object
subscriptions 單一數據源的訂閱,page.init時執行, 暫不具有done參數 參數{dispatch, history, select}
effects 可進行一些異步操做
reducers 純函數

model 方法

詳情參閱 dva

select、put、history
tips:
  1. put只用於簡單的model內部的action分發,未封裝put.resolve等方法,可以使用async/await實現同等效果;
  2. 依據小程序特性,history回調中兩個參數分別爲當前頁面路由信息和上一頁路由信息,爲一個對象,格式如。
export default {
  subscriptions: {
    setup({dispatch, history, select}) {
      const callback = (current, prev) => {
              dispatch({
                type: 'save',
                payload: {
                  prev, // {route: 'pages/index/index',options: {id: 123}}
                  current
                }
              })
            }
      history.listen(callback)
      return () => history.unlisten(callback)
    
    }
  }
}

複製代碼

組件圈子

傳統的組件數據流

對於嵌套組件間的數據通訊,每每存在所謂的父子組件關係,內層組件想要向外層組件或其它分支上的組件傳遞數據,每每經過外層的組件經過監聽函數來接收並派發,層層傳遞,這種 這種層層轉接的模式,繁瑣且須要專門的函數去維護,不利於組件的拓展和移植。

傳統組件數據流
傳統組件數據流

小程序的官方組件數據流

而對於小程序的原生組件系統來講,behaviors, relations, definitionFilter, triggerEvent, getRelationNodes等編寫組件須要使用的屬性或方法, 真的是辣眼睛!小程序官方組件系統基本喪失了做爲組件的意義, 它還在努力地更新升級, 時不時文檔中出現"不推薦使用這個字段,而是使用另外一個字段代替,它更增強大且性能更好"的字眼。

更大更好???我要是沒使用過你的這些api,我差點就信了!!!!

無法接
無法接

小程序組件數據流
小程序組件數據流

什麼叫組件?相對獨立,具備明確約定的接口,依賴語境,可自由組合,可拓展,可獨立部署,亦可複合使用!!!你說你除了相對獨立,你說你還有啥,你說你還有個啥!!!

啥

組件圈子數據流

對於一個組件來講,在最初編寫時,不該該去關心本身會被掛載到哪,本身只須要爲圈子提供數據, 甚至是數據鍵名也不用關心,放到圈子裏,誰愛用誰用,無需回調函數來協助傳遞數據, 這纔是做爲組件可移植可複用的意義,在一個圈子裏,不存在父子兄弟的概念,也沒有哪一個組件生來是給人當兒子的,組件間的往來只有最純粹的數據通訊。

以下圖中的組件C與組件F之間的數據通訊,一個提供數據,另外一個直接去使用就好了,無論層級多少,直接通訊,沒有多餘的步驟。

圈子數據流
圈子數據流

<!--組件a in page-->
    <a group-name="index-a-1" group-keys="{{ {name: 'nameA1'} }" group-data="{{ {a: 1} }}"></a>
複製代碼

只需給組件賦值一個group-name屬性,便給組件分配了一個圈子, 組件內this.dipatch({})分發地payload即是該組件給圈子貢獻的數據, 也是該組件對外約定的數據值。

group-keys是用於避免字段名稱衝突的,示例中可將a貢獻地數據鍵值更改成nameA1, 而值不變,因此圈子中地組件很純粹,只向所在圈子提供必要的數據值, 至於你將組件放置何處,數據給誰用,名稱怎麼修改,都與我無關。

group-data是組件所依賴的外部值,是一個內置屬性,可在組件的watch配置中進行監聽。

頁面、組件實例對象

注意: 對配置中的函數,以及setData進行了簡單封裝,在頁面註冊或組件註冊options的函數中, setData的數據並不會立刻更新,而是合併收集,待當前函數執行完畢後, 先模擬計算出computed,與收集的結果合併,再diff,最後setData一次。 與react更新機制相似,對於函數中出現的異步操做,不會進行數據收集,而是直接模擬計算值,diff,接着setData

頁面

對圈子狀態的管理擁有最高權限

屬性

屬性 說明 備註
$groups 獲取當前頁面內的全部圈子狀態值 object, 只讀
this.$groups['index-a-1'] 
複製代碼

方法

dispatch

用於強制更新某個圈子中的狀態

this.dispatch({
      type: 'data/index-a-1',
      payload: {
        nameA1: 'name'
      }
    })
複製代碼
參數 說明 備註
type 格式: 更新類型/圈子名稱,更新類型:data 或 group,data表示更新圈子數據的同時,也將payload中的數據更新至this.data, 即this.setData(payload) string
payload 須要更新的狀態值 object

組件

只更新所在圈子狀態

屬性

屬性 說明 備註
$groups 獲取所在頁面內的全部圈子狀態值 object, 只讀
$group 獲取所在圈子狀態值 object, 只讀
this.$group['nameA1'] 
複製代碼

方法

dispatch

用於更新所在圈子中的狀態

this.dispatch({
      type: 'data', // group
      payload: {
        nameA1: 'name'
      }
    })
複製代碼
參數 說明 備註
type 格式: 更新類型,更新類型:data 或 group,data表示更新圈子數據的同時,也將payload中的數據更新至this.data, 即this.setData(payload) string
payload 須要更新的狀態值 object

tips

更新類型

之因此會有data更新類型,是由於組件提供的數據名稱,有可能會被改寫,因此組件不要去監聽本身的數據,那是給外人用的。

有時候組件向圈子貢獻了數據,但自身並不須要這些數據更新到data,因此會有group更新類型

狀態更新機制

狀態更新機制
狀態更新機制

定製diff

騰訊官方的westore庫,爲小程序定製了一個diff,本想用在本身庫裏, 但使用中發現diff結果不太對,他會將刪除的屬性值重置爲null,我並不須要爲null, 當去遍歷對象的屬性值,就會多出一個爲null的屬性,莫名其妙,刪了就是刪了,沒必要再保留, 而且其內部實現,在進行diff前先進行一次同步下key鍵,這個徹底不必,浪費性能。 westore的diff應該是用在全局狀態新舊狀態上,其結果違反setData的數據拼接規則, 也不符合原生數據更新的習慣, 因此我得本身寫一個diff,先列出基本狀況:

一、不改變原生setData的使用習慣;

二、明確diff使用的場景,針對具體場景定製:屬性更新時須要diff,setData時須要diff;

三、diff返回結果的格式,setData會用到,須要知足 a.b[0].a這樣的格式,另外若是屬性變化,還須要根據鍵值調用watch;

四、參與對比的兩個參數都有一個共同特色,就是都是拿局部變化的數據,去對比全量舊數據,屬性的話,二者的鍵值對更是對等的,因此無需同步鍵值;

定製的diff:

diff({
       a: 1, b: 2, c: "str", d: { e: [2, { a: 4 }, 5] }, f: true, h: [1], g: { a: [1, 2], j: 111 }
   }, {
       a: [], b: "aa", c: 3, d: { e: [3, { a: 3 }] }, f: false, h: [1, 2], g: { a: [1, 1, 1], i: "delete" }, k: 'del'
   })
複製代碼

diff結果

const diffResult = {
    result: {
      a: 1,
      b: 2,
      c: "str",
      'd.e[0]': 2,
      'd.e[1].a': 4,
      'd.e[2]': 5,
      f: true,
      g: {
        a: [1,2],
        j: 111
      },
      h: [1]
    },
    rootKeys: {
      a: true,
      b: true,
      c: true,
      d: true,
      g: true,
      h: true,
    }
   }
複製代碼

記錄rootKeys是由於屬性監聽不須要 'a.b'這樣的格式,在diff中記錄下來,便少了一次遍歷篩選拆分。

相關文章
相關標籤/搜索