Omi架構與React Fiber

當耐特發表於2017-03-29

寫在前面

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: 更新元件(不包含任何子節點)

如下圖所示:

Omi架構與React Fiber

Omi架構與React Fiber

標紅的代表會進行更新的節點。

場景模擬

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)

Omi架構與React Fiber

相關文章