具有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 || '--'
}
})
複製代碼
dispatch用於分發全局狀態,風格與dva保持一致;app
Page和Component實例內置this.dispatch方法,用於分發局部狀態。
三、組件可監聽page的生命週期函數,無需作版本兼容,只需將想要監聽的函數名與page內一直便可,即
// 原生的pageLifetimes可監聽的週期太少,且版本要求高,監聽了onShow,就不須要監聽show了,避免執行兩次
component({
pageLifetimes: {
onShow: function () { },
onHide: function () { },
onPageScroll: function () { },
},
})()
複製代碼
目前可監聽的生命週期 ['onShow', 'onHide', 'onResize', 'onPageScroll', 'onTabItemTap', 'onPullDownRefresh', 'onReachBottom']
有了這個功能,就能夠編寫不少自控組件了,好比吸頂效果的導航欄等。
export default {
namespace: '',
state: {
},
subscriptions: {
},
effects: {
},
reducers: {
}
}
複製代碼
詳情參閱 dva
屬性 | 說明 | 備註 |
---|---|---|
namespace | 命名空間 | 必須 |
state | 狀態 | object |
subscriptions | 單一數據源的訂閱,page.init時執行, 暫不具有done參數 | 參數{dispatch, history, select} |
effects | 可進行一些異步操做 | |
reducers | 純函數 |
詳情參閱 dva
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,我差點就信了!!!!
什麼叫組件?相對獨立,具備明確約定的接口,依賴語境,可自由組合,可拓展,可獨立部署,亦可複合使用!!!你說你除了相對獨立,你說你還有啥,你說你還有個啥!!!
對於一個組件來講,在最初編寫時,不該該去關心本身會被掛載到哪,本身只須要爲圈子提供數據, 甚至是數據鍵名也不用關心,放到圈子裏,誰愛用誰用,無需回調函數來協助傳遞數據, 這纔是做爲組件可移植可複用的意義,在一個圈子裏,不存在父子兄弟的概念,也沒有哪一個組件生來是給人當兒子的,組件間的往來只有最純粹的數據通訊。
<!--組件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']
複製代碼
用於強制更新某個圈子中的狀態
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']
複製代碼
用於更新所在圈子中的狀態
this.dispatch({
type: 'data', // group
payload: {
nameA1: 'name'
}
})
複製代碼
參數 | 說明 | 備註 |
---|---|---|
type | 格式: 更新類型,更新類型:data 或 group,data表示更新圈子數據的同時,也將payload中的數據更新至this.data, 即this.setData(payload) | string |
payload | 須要更新的狀態值 | object |
更新類型
之因此會有data更新類型,是由於組件提供的數據名稱,有可能會被改寫,因此組件不要去監聽本身的數據,那是給外人用的。
有時候組件向圈子貢獻了數據,但自身並不須要這些數據更新到data,因此會有group更新類型
騰訊官方的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中記錄下來,便少了一次遍歷篩選拆分。