實驗版本:react
- "chameleon": "1.0.0"
實驗採用了4000條簡單數據類型。點擊按鈕後,數據源變化爲3000條另外的新數據。算法
下面放出直接實驗的代碼:編程
<template>
<view>
<!-- 條件與循環渲染 -->
<view>
<view
c-for="{{array}}"
c-for-index="idx"
c-for-item="itemName"
c-key="city"
class="cell"
>
<text> {{idx}}: {{itemName.city}} {{itemName.name}}</text>
</view>
</view>
<!-- 事件綁定 -->
<view c-bind:tap="changeShow" class="tabButton"><text>切換展現</text></view>
</view>
</template>
methods = {
changeShow() {
if (this.showlist) {
this.array = this.data2
} else {
this.array = this.data1
}
this.showlist = !this.showlist;
}
},
created() {
let data = []
for (var i = 0; i < 4000; i++) {
data.push({
city: '上海' + i,
name: 'Jack' + i + 100
})
}
this.data1 = data
let data2 = []
for (var i = 0; i < 3000; i++) {
data2.push({
city: '北京' + i,
name: 'Jack' + i + 300
})
}
this.data2 = data2
this.array = data
}
複製代碼
實驗中這種切換時的頓挫感是很是明顯的。即便是微信原生也會有可感知的毫秒級的頓挫,但 Chameleon 相比微信原生而言,頓挫感快提高到秒級了。小程序
下面使用微信原生的框架代碼來實現一樣的功能,並測試其性能。微信小程序
小程序的 Performance 目前還不如看出渲染的卡頓指標,但在真機下彷佛原生 App 端實現了渲染的檢測。實際對渲染的影響很是大,實測能夠掉到個位數的幀率。bash
使用 Performance 調試器從調用棧自底向上來看,很快就能定位到問題出在 toJs 函數上。服務器
事實上 deepEq 數據雖然對比才 100ms,而後這做爲底層框架而言,已是個不小的問題了,而 toJS 的性能簡直能夠用 「災難」 來形容,太誇張了。微信
要說清楚 toJS 首先咱們得先來聊聊什麼是 Mobx,以及 Mobx 與 Chameleon 的關係由來。架構
MobX 是一個通過戰火洗禮的庫,它經過透明的函數響應式編程,使得狀態管理變得簡單和可擴展。MobX背後的哲學很簡單:框架
任何源自應用狀態的東西都應該自動地得到。
其中包括UI、數據序列化、服務器通信,等等。Mobx 能夠與不少其餘框架配合,如 React。
下面使用代碼說明,Mobx 神奇的依賴收集代碼。其中 autoRun 和 observable 是 Mobx提供的庫 Api。observable 負責觀察數據,autoRun 負責響應監聽行爲。
const obj = observable({
a: 1,
b: 2
})
autoRun(() => {
console.log(obj.a)
})
obj.b = 3 // 什麼都沒有發生
obj.a = 2 // observe 函數的回調觸發了,控制檯輸出:2
複製代碼
咱們發現這個函數很是智能,用到了什麼屬性,就會和這個屬性掛上鉤,今後一旦這個屬性發生了改變,就會觸發回調,通知你能夠拿到新值了。沒有用到的屬性,不管你怎麼修改,它都不會觸發回調,這就是神奇的地方。
這裏還要引伸一個 Mobx 重要的接口 reaction。 reaction 的第一個參數叫作 數據函數。第二個參數叫作效果 函數。
const todos = observable([
{
title: "Make coffee",
done: true,
}
]);
const reaction2 = reaction(
() => todos.map(todo => todo.title), //對 length 和 title 的變化做出反應
titles => console.log("reaction 2:", titles.join(", ")) // 變化時就打印 titles 的數據
);
todos[0].title = "Make tea"
// 輸出:
// reaction 2: Make tea, find biscuit, explain reactions
複製代碼
() => todos.map(todo => todo.title) 就是 數據函數,當返回的 map 後的 title 數據不一致時就會觸發效果函數。
這裏再也不對 Mobx 的代碼展開了,回到 toJS 與 Mobx 的關係。不過要說清楚,還得再講一下 Chameleon runtime 在 啓動時幹了什麼!
在 Chameleon-runtime 的 MiniRuntimeCore 小程序運行時核心的 start 函數中。 能夠找到以下相關代碼,這段代碼會在小程序初始化階段運行:
const disposer = reaction(dataExprFn, sideEffect, options)
context.__cml_ob_data__ = observable(context.__cml_data__)
function dataExprFn() {
let properties = context.__cml_originOptions__[self.propsName]
let propKeys = enumerableKeys(properties)
// setData 的數據不包括 props
const obData = deleteProperties(context.__cml_ob_data__, propKeys)
return toJS(obData)
}
function sideEffect(curVal, r = {}) {
let diffV
if (_cached) {
diffV = diff(curVal, cacheData)
emit('beforeUpdate', context, curVal, cacheData, diffV)
} else {
_cached = true
diffV = curVal
}
if (type(context.setData) === 'Function') {
context.setData(diffV, walkUpdatedCb(context))
}
cacheData = { ...curVal }
}
複製代碼
爲了更好的看清代碼,作了些許刪減。 下面來解釋一下這段代碼。
好的故事講完了,性能問題 dataExprFn 和 sideEffect 各自佔一個,由於 deepEq 函數在 sideEffect diff 的時候會調用。 咱們先來看看問題最突出的 toJS 函數,爲何 toJS 的性能問題會這麼突出呢。
export default function toJS(source, detectCycles = true, __alreadySeen = [], needPxTransfer = true) {
if (isObservable(source)) {
if (isObservableArray(source)) {
var res = cache([]);
var toAdd = source.map(function (value) { return toJS(value, detectCycles, __alreadySeen); });
res.length = toAdd.length;
for (var i = 0, l = toAdd.length; i < l; i++)
res[i] = toAdd[i];
return res;
}
}
}
複製代碼
這裏一樣作了大量刪減,事實上 toJS 會對多種數據類型作不同的處理,但這裏只看 Array類型 吧,能夠看到 toJS 呈現了遞歸調用,尤爲是還有 isObservable 和 isObservableArray ,以及遍歷 toJS處理。 致使遞歸將性能問題大幅放大。
事實上過後回過頭來看,dataExprFn 只是原封不動將 Observable 轉 js 數據類型,再作出響應。其實只是由於使用了 Mobx 不得沒必要須作這麼一層轉換層,要犧牲性能大的性能,其實這裏徹底是能夠優化獲得。
參考 Mobx 底層的 symbol proxy reflect 三劍客,我這裏也只能建議 Chameleon 剝離 Mobx 的依賴,獨立實現一套更輕量化的數據狀態管理框架。
回到性能問題的第二個根源,deepEq 函數
export default function diff(current, old) {
let out = {}
prefill(current, old) // 填補新老數據的屬性差別
iDiff(current, old, '', out) // 算出出現變化差別的數據,存到 out 中
return out
}
複製代碼
其中 deepEq 函數 在 Mobx 的 comparer.structural 中調用,用來對數據進行深度比較。
function prefill(current, old) {
if (comparer.structural(current, old)) return
if (type(current) === 'Object' && type(old) === 'Object') {
for (let key in old) {
const curVal = current[key]
if (curVal === undefined) {
current[key] = ''
} else {
prefill(curVal, old[key])
}
}
}
}
複製代碼
這裏又刪了一部分代碼,不難發現又是基於深度遍歷的遞歸算法,那麼 comparer.structural 若是有性能問題,也會被大幅放大,哎,又是 mobx 的性能問題。
當咱們把數據的深度變得稍微深一點時,小程序甚至能夠直接死機,你們能夠試試,事實上這個數據並無很極端,你們的業務場景中頗有可能會出現這樣深度的場景。
Chameleon 修改 data 後會發生以下調用棧,紅色和黃色就是本次性能瓶頸的時間點:
不妨再加入微信小程序運行時的過程,咱們能夠觀察的更加清晰:
下面能夠看到微信官網的底層介紹
而後再結合這部分,咱們再把過程從新梳理一遍:
如此來看,不難發現一些高性能場景或者複雜場景的頁面下,不妨使用微信原生,Chameleon 的運行時架構決定了這裏會有必定程度的開銷,即使 Chameleon 的下個版本修復了我上述所說的性能問題。
好比這段代碼,居然觸發了 2次 reactionRunner 方法,要知道目前這個方法是性能問題比較大的函數,更詭異的是 Reaction.runReaction 底層會執行 2次 Reaction.track 方法, 而 Reaction.track 中的 toJs 函數 是這次延遲的最大緣由 。
也就是說 array 中有 400 條數據的話,由於 data 變化了2次, 一次是 this.array ,一次是 this.showlist, 因此 我會 toJS 兩次 400 條的 Obserable 數據,而後由於 Reaction 底層會執行 2次 toJS 方法,因此 本來的 400 條數據被以 1600 的量被 toJS 化。
考慮到 Mobx 底層toJs不太容易替換
能夠在 dataExprFn 那一層就把 Diff 須要更新的數據算出來,這樣直接 toJs 的話,能夠減小不少沒必要要的計算量。但依然治標不治本。。。
能夠自定義一個更高效的基於小程序的 Mobx 庫解決上面的問題,製做一套更高效地 Observatable -> js 的算法