跳到主要內容
黯羽輕揚每天積累一點點

深入 React

免費2017-08-20#JS#react原理#react内部原理#react进阶#react dva#react saga

由表及裡瞭解 React,既見樹木也看森林

設計思想

想表達什麼?React 怎樣理解 Application?

應用是個狀態機,狀態驅動視圖

v = f(d)

v是视图
f是组件
d是数据/状态

與 FP 有什麼關係?

把函式式思想引入前端,透過 PureComponent 組合來實作 UI

最大好處是讓 UI 可預測,對同樣的 f 輸入同樣的 d 一定能得到同樣的 v

可以把各個 f 單獨拎出來測試,組合起來肯定沒有問題,從理論上確定了元件品質是可靠的,組合出來的整個應用的 UI 也是可靠的

目標

想解決什麼問題?定位?

A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

針對構建 UI 提供一種元件化的方案

能解決什麼問題?

  • 元件化

  • UI 可靠性

  • 資料驅動視圖

效能目標

For many applications, using React will lead to a fast user interface without doing much work to specifically optimize for performance.

尋找成本與收益的平衡點,不刻意去做效能優化,還能寫出*效能不錯(非最優)*的應用

實際上,React 所作的效能優化主要體現在:

  • 事件代理,全域一個事件監聽

    • 自己有完整的捕獲冒泡,是為了抹平 IE8 的 bug

    • 物件池複用 event 物件,減少 GC

  • DOM 操作整合,減少次數

但無論怎樣,效能肯定不及年邁的(經驗豐富的)FEer 手寫的原生 DOM 操作版

虛擬 DOM

透過什麼方式解決問題?

在 DOM 樹之上加一層額外的抽象

元件化方式:提供元件 class 範本、生命週期 hook、資料流轉方式、局部狀態代管

執行時:用虛擬 DOM 樹管理元件,建立並維護到真實 DOM 樹的映射關係

虛擬 DOM 有什麼作用?

  • 批次處理提升效能

  • 降低 diff 開銷

  • 實作「資料繫結」

具體實作

JSX -> React Element -> 虚拟DOM节点 ..> 真实DOM节点
          描述对象
  1. 編譯時,翻譯 JSX 得到 createElement

  2. 執行 createElement 得到 React Element 描述物件

  3. 根據描述物件建立虛擬 DOM 節點

  4. 整合虛擬 DOM 節點上的狀態,建立真實 DOM 節點

虛擬 DOM 樹的節點集合是真實 DOM 樹節點集合的超集,多出來的部分是自定義元件(Wrapper)

結構上,內部樹佈局是森林,維護在 instancesByReactRootID

  • 現有 app 引入 React 時,會有多個 root DOM node

  • 純 React 的應用,森林裡一般只有 1 棵樹

單向資料流

瀑布模型

propsstate 把元件組織起來,元件間資料流向類似於瀑布

資料流向總是從祖先到子孫(從根到葉子),不會逆流

  • props:管道

  • state:水源

單向資料流是由狀態丟棄機制決定的,具體表現為:

  • 狀態變化引發的資料及 UI 變化都只會影響下方的元件

  • 渲染視圖時向下流,表單互動能回來,引發另一次向下渲染

單向資料流是對渲染視圖過程而言的,子孫的 state 如何改變都不會影響祖先,除非通知祖先更新其 state

state 與 props

state 是最小可變狀態集,特點:

  • 私有的。由元件自身完全控制,而不是來自上方

  • 可變的。會隨時間變化

  • 獨立存在。無法透過其他 state 或者 props 計算出來

props 是不可變的,僅用來填充視圖範本:

props       React Element描述对象
-----> 组件 ---------------------> 视图

資料繫結?

2 個環節

  1. 相依性收集(靜態相依/動態相依)

  2. 監聽變化

首次渲染時收集 data-view 的映射關係,後續確認資料變化後,更新資料對應的視圖

3 種實作方式

實作方式相依性收集監聽變化案例
getter & settergettersetter 監聽變化Vue
提供資料模型解析範本所有資料操作都走框架 API,通知變化Ember
髒檢查解析範本在合適的時機,取最新的值和上次的比較,檢查變化Angular
虛擬 DOM diff幾乎不收集setState 通知變化React

從相依性收集的粒度來看:

  • Vue 透過 getter 動態收集相依性粒度最細,最精確

  • Ember 和 Angular 都是透過靜態範本解析來找出相依性

  • React 最粗枝大葉,幾乎不收集相依性,整個子樹重新渲染

state 變化時,重新計算對應子樹的內部狀態,對比找出變化(diff),然後在合適的時機應用這些變化(patch)

細粒度的相依性收集是精確 DOM 更新的基礎(哪些資料影響哪個元素的哪個屬性),無需做額外的猜測和判斷

虛擬 DOM diff 演算法

React 不收集相依性,只有 2 個已知條件:

  • 這個 state 屬於哪個元件

  • 這個 state 變化只會影響對應子樹

子樹範圍對於最終視圖更新需要的 DOM 操作而言太大了,需要細化(diff)

tree diff

樹的 diff 是個相對複雜(NP)的問題,先考慮一個簡單場景:

    A           A'
   / \   ?    / | \
  B   C  ->  G  B   C
 / \  |         |   |
D   E F         D   E

diff(treeA, treeA') 結果應該是:

1.insert G before B
2.move   E to     F
3.remove F

如果要電腦來做的話, 好找, 的判定就比較複雜了,首先要把樹的相似程度量化(比如加權編輯距離),並確定相似度為多少時,刪+增 划算(操作步驟更少)

React diff

對虛擬 DOM 子樹做 diff 就面臨這樣的問題,考慮 DOM 操作場景的特點:

  • 局部小改動多,大片的改動少(效能考慮,用顯示隱藏來規避)

  • 跨層級的移動少,同層節點移動多(比如表格排序)

假設:

  • 假設不同類型的元素對應不同子樹(不考慮「向下看子樹結構是否相似」, 的判斷就沒難度了)

  • 前後結構都會帶有唯一的 key,作為 diff 依據,假定同 key 表示同元素(降低比較成本)

這樣 tree diff 問題就被簡化成了 list diff(字串編輯問題):

  1. 走訪新的,找出 增/移

  2. 走訪舊的,找出 刪

本質是一個很弱的字串編輯演算法,所以,即便不考慮 diff 開銷,單從最終的實際 DOM 操作來看,效能也不是最優的(相比手動操作 DOM)

另外,保險起見, React 還提供了 shouldComponentUpdate 鉤子,允許人工干預 diff 過程,避免誤判

狀態管理

狀態共享與傳遞

  • 兄弟 -> 兄弟。提升共享狀態,保證自上而下的單向資料流

  • 子 -> 父。由父預先傳入 cb(函式 props)

  • ? -> 遠房親戚。遠距離通訊很難解決,需要手動接力,要么透過 context 共享

透過提升狀態來共享,能減少孤立狀態,減少 bug 面,但畢竟比較麻煩。元件間遠距離通訊問題沒有好的解決方案

另一個問題是在複雜應用中,狀態變化(setState)散落在各個元件中,邏輯過於分散,存在維護上的問題

Flux

為了解決狀態管理的問題,提出了 Flux 模式,目標是讓資料可預測

基本思路

(state, action) => state

具體做法

  • 用顯性資料,不用衍生資料(先宣告後使用,不臨時造資料)

  • 分離資料和視圖狀態(把資料層抽出來)

  • 避免階層更新帶來的階層影響(M 與 V 之間互相影響,資料流不清楚)

結構

         产生action               传递action           update state
view交互 -----------> dispatcher -----------> stores --------------> views

Flux圖解

特點是 store 比較,負責根據 action 更新內部 state 及把 state 變化同步到 view

container 與 view

container 其實就是 controller-view:

  • 用來控制 view 的 React 元件

  • 基本職能是收集來自 store 的訊息,存到自己的 state 裡

  • 不含 props 和 UI 邏輯

Redux 的取捨

action  与Flux一样,就是事件,带有type and data(payload)
    同样手动dispatch action
---
store  与Flux功能一样,但全局只有1个,实现上是一颗不可变的状态树
    分发action,注册listener。每个action经过层层reducer得到新state
---
reducer  与arr.reduce(callback, [initialValue])作用类似
    reducer相当于callback,输入当前state和action,输出新state

                  call             new state
action --> store ------> reducers -----------> view

用一棵不可變狀態樹維護整個應用的狀態,無法直接改變,發生變化時,透過 action 和 reducer 建立新的物件

reducer 的概念相當於 node 中間件,或者 gulp 外掛,每個 reducer 負責狀態樹的一小部分,把一系列 reducer 串聯起來(把上一個 reducer 的輸出作為當前 reducer 的輸入),得到最終輸出 state

對比 Flux

  • 把 store 數量限定為 1

  • 去掉了 dispatcher,把 action 傳遞給所有頂層 reducer,流向相應子樹

  • 把根據 action 更新內部 state 的部分獨立出來,分解到各 reducer

能去掉 dispatcher 是因為純函式 reducer 可以隨便組合,不需要額外管理順序

react-redux

Redux 與 React 沒有任何關係,Redux 作為狀態管理層可以配合任何 UI 方案使用,例如 backbone、angular、React 等等

react-redux 用來處理 new state -> view 的部分,也就是說,新 state 有了,怎樣同步視圖?

container

container 是一種特殊的元件,不含視圖邏輯,與 store 關係緊密。從邏輯功能上看就是透過 store.subscribe() 讀取狀態樹的一部分,作為 props 傳遞給下方的普通元件(view)

connect()

一個看起來很神奇的 API,主要做 3 件事:

  • 生成 container

  • 負責把 dispatch 和 state 資料作為 props 注入下方普通元件

  • 內建效能優化,避免不必要的更新(內建 shouldComponentUpdate)

Provider 是怎麼回事?

目的:避免手動逐層傳遞 store

實作:在頂層透過 context 注入 store,讓下方所有元件共享 store

生態

  • 除錯工具 DevTools

  • 平台 React Native

  • 元件庫 antd Material-UI

  • 發展 Rax

  • 狀態管理層 Redux Saga Dva

評論

暫無評論,快來發表你的看法吧

提交評論