寫在前面
Omi框架在架構設計的時候就決定把update的控制權交給了開發者,視靈活性比生命還重要。不然的話,如果遇到React Fiber要解決的這類問題的話,就需要推翻原有架構重新搞了。
React Fiber
先引用下我們團隊小鮮肉Stark偉-復旦大四 / 騰訊@AlloyTeam在知乎上的回答
React 的核心思想是每次對於介面 state 的改動,都會重新渲染整個 virtual dom,然後新老的兩個 virtual dom 樹進行 diff,對比出變化的地方,然後通過 renderer 渲染到實際的UI介面(這裡可能是瀏覽器的DOM,也可能是native元件)。這樣實質上就是把介面變成一個純粹的狀態機,React 的作用就是把這個狀態機之間的狀態轉換高效率地執行出來。但是存在以下問題:
- 1、不是每一次狀態的變化都要立刻執行。
- 2、不同的狀態變化之間是有輕重緩急之分的,比如『動畫』這種狀態變化的優先順序,出於對使用者體驗的考量,為了避免動畫卡頓或者掉幀,一般比『改變頁面資料』的優先順序更高。
- 3、我們現在的做法只是呼叫 setState 觸發重新渲染,然後 React 會收集一個 tick 內的 state 變化,然後執行,所以有可能大量的計算會在同一時刻阻塞程式。但我們沒法控制 React 運算的時序問題,也不太可能通過手工宣告讓動畫的優先順序比資料變更更高。而 React 作為一個使用者互動的框架,它本應該能讓程式設計師能控制這些東西。所以這個破事要怎麼解決咧?( ⊙ o ⊙ )我們知道,任何的函式呼叫都會有自己的呼叫棧,比如對於 v = f(d) 這個函式來說,函式 f 可能又呼叫了一系列其它的函式,這些函式就包括在 f 的呼叫棧中。關鍵的問題在於,這種原生的呼叫棧是基本不可延遲的,它會立即執行,如果計算量很大的話就會阻塞住程式,讓介面失去響應,這種事情經常發生在 React 的渲染過程中。
或者看顏什麼都不記得適的回答:
狀態轉移時,是在一次 tick 中遞迴遍歷元件樹,找出需要更新的節點 rerender。但是這樣造成了一些問題:
- 在 UI 中,不是所有的狀態轉移都需要立即執行。大量的同時計算可能會導致資源的浪費,以致出現掉幀的狀況,降低使用者體驗。
- 不同型別的狀態轉移應有不同的優先順序,比如點選按鈕出現動畫的優先順序應該比 Fetch API 要高。
- React 是 pull-based 實現的,事務的時序全部由 React 決定。我們沒辦法操控執行事務的時序。
Omi component update
Omi有上面的問題嗎? 沒有。
Omi的賣點之一便是:更自由的更新,每個元件都有update方法,自由選擇你認為最佳的時機進行更新。這樣設計的一大好處是更加靈活,如果想要自動更新整合個mobx或者obajs便可,進可功退可守。
資料和檢視雖然是關係密切,但是解耦的設計還是非常必要,這樣可以應付更多的場景。好處:
- 你可以等某個動畫播放完成再進行update
- 你可以控制update順序
- 你可以update前後幹一些事情而不需要利用生命週期的鉤子函式(有的時候鉤子函式讓連續的邏輯打得粉碎...)
component update說完了嗎?沒有... Omi不僅僅有component update!還有更加強大的 updateSelf。
Omi component updateSelf
先說下兩者的區別:
- update: 更新元件樹
- updateSelf: 更新元件(不包含任何子節點)
如下圖所示:
標紅的代表會進行更新的節點。
場景模擬
class TestComponent extends Omi.Component {
render () {
return `<div>
<h3>{{title}}</h3>
<List name="list" data="listData" />
</div>`;
}
}複製程式碼
元件結構上面程式碼所示:
- 如果呼叫元件例項的update的話,會更新元件本身以及 List元件
- 如果呼叫元件例項的updateSelft的話,會更新元件本身,不會更新List元件
比如我們僅僅修改了this.data.title,就可以呼叫this.updateSelf方法,雖然一般情況下無腦update也能達到同樣的結果,雖然morphdom的DOM diff已經足夠輕量快速,但是一定沒有updateSelf方法快速。上面的例子updateSelf優勢可能不明顯,如果這樣呢:
class TestComponent extends Omi.Component {
render () {
return `<div>
<h3>{{title}}</h3>
<List name="list" data="listData" />
<List name="list" data="listData" />
<Content name="list" data="listData" />
<Slider name="list" data="listData" />
</div>`;
}
}複製程式碼
再或者Content、Slider裡面再巢狀了子元件,子元件又巢狀了子元件,如果僅僅只是需要修改title的話,updateSelf優勢就盡顯無疑。
實現細節
這裡主要說一說updateSelf的實現細節。主要包含兩點:
- 不重新render的情況下拿到子元件的完整的HTML
- 關閉子元件的DOM diff
進行updateSelf的時候,就運算元元件的data發生了變化,也不去改變子元件。因為updateSelf就意思就是更新自身。
所以子元件的HTML不需要使用模板和data生成,只需要component.node.outerHTML就可以了。outerHTML在古老的firefox是不支援的,可以通過建立節點插入然後讀innerHTML進行polyfill。
元件本身的HTML是需要使用模板和data生成,子元件就使用剛剛的outerHTML替換便可。但是問題來了,子元件的DOM diff其實是沒有必要的,雖然morphdom的DOM diff已經足夠輕量快速。但是子元件他們本來就是一模一樣,沒有必要的開銷。所以需要關閉DOM diff~~。然後morphdom沒有ignore相關的配置....
擴充套件 morphdom
API:
morphdom(node, newNodeHTML, {
ignoreAttr: ['attr1','attr2']
} )複製程式碼
比如上面代表只要標記了attr1或者attr2的就是忽略,當然為了規避錯誤,這裡需要嚴格的匹配才會ignore DOM diff。怎麼算嚴格的匹配?就是:
- 當同樣的attr的DOM,並且該attr在ignoreAttr裡才會ignore DOM diff
Omi Store體系addSelfView
Omi Store體系以前通過addView進行檢視收集,store進行update的時候會呼叫元件的update。
與此同時,Omi Store體系也新增了addSelfView的API。
- addView 收集該元件檢視,store進行update的時候會呼叫元件的update
- addSelfView 收集該元件本身的檢視,store進行update的時候會呼叫元件的updateSelf
當然,store內部會對檢視進行合併,比如addView裡面加進去的所有檢視有父子關係的,會把子元件去掉。爺孫關係的會把孫元件去掉。addSelfView收集的元件在addView裡已經收集的也去進行合併去重,等等一系列合併優化。
Omi相關
- Omi官網omijs.org
- Omi的Github地址github.com/AlloyTeam/o…
- 如果想體驗一下Omi框架,可以訪問 Omi Playground
- 如果想使用Omi框架或者開發完善Omi框架,可以訪問 Omi使用文件
- 如果你想獲得更佳的閱讀體驗,可以訪問 Docs Website
- 如果你懶得搭建專案腳手架,可以試試 omi-cli
- 如果你有Omi相關的問題可以 New issue
- 如果想更加方便的交流關於Omi的一切可以加入QQ的Omi交流群(256426170)