react學習筆記(下)

這裏有一份簡潔的前端知識體系等待你查收,看看吧,會有驚喜哦~若是以爲不錯,麻煩star哈~前端


設計維度


目標

爲了解決用戶的問題,技術自己要達成什麼目標。這層定義「作到什麼」。react


命令式編程VS聲明式編程

命令式編程

命令「機器」如何去作事情(how),這樣無論你想要的是什麼(what),它都會按照你的命令實現。git

聲明式編程

告訴「機器」你想要的是什麼(what),讓機器想出如何去作(how)。github

在React中,每一個組件經過render函數返回「這個組件應該長得什麼樣」,而不去描述「怎麼樣去讓這個組件長成這個樣子」。算法

聲明式編程的好處

  • 讓開發者的工做簡化了
  • 減小了重複工做
  • 留下了改進的空間:好比React Fiber,雖然算法改頭換面,可是組件卻幾乎不用改,由於組件只操心「顯示什麼」而不操心「如何顯示」啊,固然不受影響了。
  • 提供了全局協調能力:在React的將來,每一個組件仍是隻聲明「想要畫成什麼樣子」,但React卻能夠改進協調算法,讓React組件根據不一樣優先級來渲染,提升用戶感知性能,可是React組件的代碼不須要改變

react的核心理念之一就是函數式編程。編程


JSX

在JS中寫HTML標記,這體現了高內聚。要達到這種效果,就必須依賴JSX。redux

JSX 的本質不是模板引擎,而是動態建立組件的語法糖,它容許咱們在JS代碼中直接寫HTML標記。最終生成的代碼就是React.CreateElement。後端

若是在 JSX 中往 DOM 元素中傳入自定義屬性,React 是不會渲染的。若是要使用 HTML 自定義屬性,要使用 data- 前綴,這與 HTML 標準也是一致的。然而,在自定義標籤中任意的屬性都是被支持的,以 aria- 開頭的網絡無障礙屬性一樣能夠正常使用。設計模式

JSX的優勢數組

  1. 直觀:聲明式建立界面
  2. 靈活:代碼動態建立界面
  3. 易上手:無需學習新的模板語言

約定

  1. 自定義組件以大寫字母開頭
  2. react 認爲小寫的 tag 是原生 DOM 節點,如 div
  3. JSX標記能夠直接使用屬性語法,例如<menu.Item />

實現原理

爲了達到設計目標,該技術採用了什麼原理和機制。實現原理層回答「怎麼作到」的問題。把實現原理弄懂,而且講清楚,是技術人員的基本功。


生命週期函數

生命週期函數指的是在某一個時刻組件會自動調用執行的函數。



也能夠參考網上的這張圖

一些注意點:

無論是掛載階段仍是更新階段,都要到render時才能獲取到更新後的this.state。在componentWillMount、 componentWillReceiveProps、 shouldComponentUpdate 和 componentWillUpdate 中也仍是沒法獲取到更新後的 this.state。

mountComponent 本質上是經過遞歸渲染內容的,因爲遞歸的特性,父組件的 componentWillMount 在其子組件的 componentWillMount 以前調用,而父組件的 componentDidMount 在其子組件的 componentDidMount 以後調用。updateComponent同理。

updateComponent 負責管理生命週期中的 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。在 componentWillReceiveProps 中調用 setState,是不會觸發 re-render 的,而是會進行 state 合併。禁止在 shouldComponentUpdate 和 componentWillUpdate 中調用 setState,這會形成循環調用,直至耗光瀏覽器內存後崩潰。

在 componentWillUnmount 中調用 setState,是不會觸發 re-render 的。

無狀態組件只是一個 render 方法,並無組件類的實例化過程,也沒有實例返回。無狀態組件沒有狀態,沒有生命週期,只是簡單地接受 props 渲染生成 DOM 結構,是一個純粹爲渲染而生的組件。


這裏簡單介紹下各個生命週期函數:

constructor

  1. 用於初始化內部狀態,不多使用
  2. 惟一能夠直接修改 state 的地方

getDerivedStateFromProps

  1. 當 state 須要從 props 初始化時使用
  2. 儘可能不要使用:維護二者狀態一致性會增長複雜度
  3. 每次 render 都會調用
  4. 典型場景:表單控件獲取默認值

componentDidMount

  1. UI 渲染完成後調用
  2. 只執行一次
  3. 典型場景:獲取外部資源

componentWillUnmount

  1. 組件移除時被調用
  2. 典型場景:資源釋放

getSnapshotBeforeUpdate

  1. 在元素被渲染並寫入 DOM 以前調用,這樣,你在 DOM 更新前捕獲 DOM 信息(例如:滾動位置)。
  2. 在頁面 render t前調用,state 已更新
  3. 典型場景:獲取 render 以前的 DOM 狀態

componentDididUpdate

  1. 每次 UI 更新時被調用
  2. 典型場景:頁面須要根據 props 變化從新獲取數據

shouldComponentUpdate

  1. 決定 VDOM 是否要重繪
  2. 通常能夠由 PureComponent 自動實現
  3. 典型場景:性能優化

componentWillReceiveProps

注意下update階段,觸發組件update有兩種狀況,props或者state的修改。

能夠看到,這兩種狀況生命週期函數是有重合的。惟一的不一樣就是props改變時,會先調用 componentWillReceiveProps

  1. 一個組件要從父組件接受參數
  2. 若是這個組件第一次存在於父組件中,不會執行
  3. 若是這個組件以前已經存在於父組件中,纔會執行

MV* 與 Flux

MVC/MVVM

MVC/MVVM 簡稱 MC* 模式,其中 MVVM 是從 MVC 演進而來的。

MVC 是一種架構設計模式,它經過關注數據界面分離,來鼓勵改進應用程序結構。具體地 說,MVC 強制將業務數據(Model)與用戶界面(View)隔離,用控制器(Controller)管理邏 輯和用戶輸入。

Model 負責保存應用數據,和後端交互同步應用數據,或校驗數據。

View 是 Model 的可視化表示,表示當前狀態的視圖。

Controller負責鏈接 View 和 Model,Model 的任何改變會應用到 View 中,View 的操做會經過 Controller應用到 Model 中。

Controller 管理了應用程序中 Model 和 View 之間的邏輯和協調。

MVC 的致命缺點:混亂的數據流動方式,此外,前端 MVC 模式的實現各有各的理解,千奇百怪。



MVVM 出現於 2005 年,最大變化在於 VM(ViewModel)代替了 C(Controller)。其關鍵「改 進」是數據綁定(DataBinding),也就是說,View 的數據狀態發生變化能夠直接影響 VM,反之 亦然。



Flux 的解決方案

Flux 的核心思想就是數據和邏輯永遠單向流動



Flux 核心思想,也就是中心化控制。中心化控制讓全部的請求與改變都只能經過 action 發出,統一 由 dispatcher 來分配。這樣View就能夠保持高度簡潔,發生問題時也便於定位。比起 MVC 架構下數據或邏 輯的改動可能來自多個徹底不一樣的源頭,Flux 架構追查問題的複雜度和困難度顯然要小得多。

Flux 的不足:冗餘代碼過多,每一個應用中都須要手動建立一個 dispatcher 的示例,這仍是讓不少開發者以爲煩惱

若是非要把Flux 和MVC 作一個結構對比,那麼, Flux 的Dispatcher 至關於MVC 的Controller, Flux 的Store 至關於MVC 的Model, Flux 的View 固然就對應MVC 的View了,至於多出來的這個Action ,能夠理解爲對應給MVC 框架的用戶請求

Redux

Redux 是一個可預測的狀態容器。簡單地說,在摒棄了傳統 MVC 的發佈/訂閱模式並經過 Redux 三大原則強化對狀態 的修改後,使用 Redux 可讓你的應用狀態管理變得可預測、可追溯。

redux的相關知識繁多,還包含了Mobx、dva,爲此我將他抽離出來,請看這裏


render的執行

  1. 當組件的state和props發生改變時,render函數就會從新執行
  2. 父組件render函數被執行時,它的子組件的render函數都將被從新運行一次

applyMiddleWare

applyMiddleWare 的實現:

  1. 拿到原生的store跟dispatch
  2. 對dispatch作了一層擴展
  3. 將原生store中的dispatch覆蓋掉

爲何須要VDOM?

若是沒有VDOM,state改變,如何渲染頁面?

最原始的作法:

  1. state 數據
  2. JSX 模板
  3. 數據 + 模板 結合,生成真實DOM,來顯示
  4. state發生改變
  5. 數據 + 模板 結合,生成真實DOM,替換原來的DOM

這樣作的缺陷:

  1. 第一次生成了完整的DOM片斷
  2. 第二次生成了完整的DOM片斷
  3. 第二次的DOM替換第一次的DOM,很是耗性能

改進的作法:

  1. state 數據
  2. JSX 模板
  3. 數據 + 模板 結合,生成真實DOM,來顯示
  4. state發生改變
  5. 數據 + 模板 結合,生成真實DOM,不直接替換原來的DOM
  6. 新的DOM(DocumentFragment) 和 原始的DOM 作對比,找差別
  7. 只替換有變更的DOM元素

這樣作的缺陷:性能提高不明顯,由於對比DOM也消耗了性能

react的作法

  1. state 數據
  2. JSX 模板
  3. 數據 + 模板結合,生成VDOM(VDOM就是一個JS對象,用他來描述真實DOM)
  4. 用VDOM,生成真實DOM,來顯示
  5. state發生改變
  6. 生成新的VDOM (極大提高性能)
  7. 比較原始VDOM和新的VDOM的區別 (極大提高性能)
  8. 只替換有變更的DOM元素

優勢:

  1. 性能提高了
  2. 方便與其餘平臺集成,跨端應用得以實現

VDOM原理

JSX的運行基礎就是VDOM。

VDOM的運行機制是廣度優先分層比較。

VDOM 的兩個假設:

  1. 組件的DOM結構是相對穩定的(不多發生跨層移動的場景)
  2. 類型相同的兄弟節點能夠被惟一標識

事件系統

VDOM 在內存中是以對象的形式存在的,若是想要在這些對象上添加事件,就會很是簡單。

React 基於 VDOM 實現了一個 SyntheticEvent (合成事件)層,咱們所定義的事件處理器會接收到一個 SyntheticEvent 對象的實例,它徹底符合 W3C 標準,不會存在任何 IE 標準的兼容性問題。而且與原生的瀏覽器事件同樣擁有一樣的接口,一樣支持事件的冒泡機制,咱們可使用 stopPropagation() 和 preventDefault() 來中斷它。全部事件都自動綁定到最外層上。若是須要訪問原生事件對象,可使用 nativeEvent 屬性。

合成事件的實現機制

在 React 底層,主要對合成事件作了兩件事:事件委派和自動綁定

事件委派

react 並不會把事件處理函數直接綁定到真實的節點上,而是把全部事件綁定到結構的最外層,使用一個統一的事件監聽器。

事件監聽器上維持了一個映射來保存全部組件內部的事件監聽和處理函數。

當組件掛載或卸載時,只是在這個統一的事件監聽器上插入或刪除一些對象

當事件發生時,首先被這個統一的事件監聽器處理,而後在映射裏找到真正的事件處理函數並調用。

這樣作簡化了事件處理和回收機制,效率也有很大提高。

自動綁定

在 React 組件中,每一個方法的上下文都會指向該組件的實例,即自動綁定 this 爲當前組件。 並且 React 還會對這種引用進行緩存,以達到 CPU 和內存的最優化。在使用 ES6 classes 或者純函數時,這種自動綁定就不復存在了,咱們須要手動實現 this 的綁定。

常見的綁定方法有:

  • 雙冒號語法:<button onClick={::this.handleClick}>Test</button>
  • 構造器內使用bind綁定
  • 箭頭函數

合成事件與原生事件對比

事件對象

原生 DOM 事件對象在 W3C 標準和 IE 標準下存在着差別。在低版本的 IE 瀏覽器中,只能使用 window.event 來獲取事件對象。

而在 React 合成事件系統中,不存在這種兼容性問題,在事件處理函數中能夠獲得一個合成事件對象。

事件類型

React 合成事件的事件類型是 JS 原生事件類型的一個子集

事件傳播與阻止事件傳播

事件傳播分爲捕獲階段、目標階段、冒泡階段。

事件捕獲在程序開發中的意義不大,還有兼容性問題。因此,React 的合成事件則並無實現事件捕獲,僅僅支持了事件冒泡機制。

阻止事件傳播:阻止原生事件傳播須要使用 e.preventDefault(),不過對於不支持該方法的瀏覽器(IE9 以 下),只能使用 e.cancelBubble = true 來阻止。而在 React 合成事件中,只須要使用 e.preventDefault() 便可。

事件綁定方式

原生事件有三種方式:

  • 直接在DOM元素中綁定: <button onclick="alert(1)">Test</button>
  • 在JS中,經過爲元素的事件屬性賦值的方式實現綁定:el.onclick = e => {console.log(e)}
  • 經過事件監聽函數來實現綁定:el.addEventListener("click", ()=>{},false); el.attachEvent("onclick", ()=>{})

React 合成事件的綁定方式則簡單得多:<button onClick={this.handleClick}>Test</button>


獲取真實DOM的方式

要獲取真實的DOM節點有兩種方式,一種是經過e.target,一種是ref。

但能不使用ref儘可能不用


注意事項

1、原生事件

componentDidMount 會在組件已經完成安裝而且在瀏覽器中存在真實的 DOM 後調用,此時咱們就能夠完成原生事件的綁定。

在 React 中使用 DOM 原生事件時,必定要在組件卸載時手動移除,不然很 可能出現內存泄漏的問題。而使用合成事件系統時則不須要,由於 React 內部已經幫你妥善地處理了。

2、合成事件與原生事件混用

儘可能避免在 React 中混用合成事件和原生 DOM 事件。

阻止 React 事件冒泡的行爲只能用於 React 合成事件系統 中,且沒辦法阻止原生事件的冒泡。反之,在原生事件中的阻止冒泡行爲,卻能夠阻止 React 合成事件的傳播。

React 的合成事件系統只是原生 DOM 事件系統的一個子集。它僅僅實現了 DOM Level 3 的事件接口,而且統一了瀏覽器間的兼容問題。有些事件 React 並無實現,或者受某些限制沒辦法去實現,好比 window 的 resize 事件。


優劣侷限

每種技術實現,都有其侷限性,在某些條件下能最大化的發揮效能,缺乏了某些條件則暴露出其缺陷。優劣侷限層回答「作得怎麼樣」的問題。對技術優劣侷限的把握,更有利於應用時總結最佳實踐,是分析各類「坑」的基礎。


演進趨勢

技術是在迭代改進和不斷淘汰的。瞭解技術的前生後世,分清技術不變的本質,和變化的脈絡,以及與其餘技術的共生關係,能體現你對技術發展趨勢的關注和思考。這層體現「將來如何」。


擁抱異步渲染

react v16.0.0 引入了叫 Fiber 這個全新的架構。這個架構使得 React 用異步渲染成爲可能,但要注意,這個改變只是讓異步渲染(async rendering)成爲「可能」,React 卻並無在 v16 發佈的時候馬上開啓這種「可能」,也就是說,React 在 v16 發佈以後依然使用的是同步渲染。

不過,雖然異步渲染沒有馬上採用,Fiber 架構仍是打開了通向新世界的大門,React v16 一系列新功能幾乎都是基於 Fiber 架構。

要面向 React 將來,咱們首先要理解這個異步渲染的概念。

同步渲染的問題

長期以來,React 一直用的是同步渲染,這樣對 React 實現很是直觀方便,可是會帶來性能問題。

當要渲染的組件樹很是龐大,JS的單線程遇到react的同步渲染,結果就是同步渲染霸佔 JS 惟一的線程,其餘的操做什麼都作不了,在這 1 秒鐘內,若是用戶要點擊什麼按鈕,或者在某個輸入框裏面按鍵,都不會看到當即的界面反應,這也就是俗話說的「卡頓」。

在同步渲染下,要解決「卡頓」的問題,只能是儘可能縮小組件樹的大小,以此縮短渲染時間,可是,應用的規模老是在增大的,不是說縮小就能縮小的,雖然咱們利用定義 shouldComponentUpdate 的方法能夠減小沒必要要的渲染,可是這也沒法從根本上解決大量同步渲染帶來的「卡頓」問題。

異步渲染:兩階段渲染

React Fiber 引入了異步渲染,有了異步渲染以後,React 組件的渲染過程是分時間片的,不是一口氣從頭至尾把子組件所有渲染完,而是每一個時間片渲染一點,而後每一個時間片的間隔均可去看看有沒有更緊急的任務(好比用戶按鍵),若是有,就去處理緊急任務,若是沒有那就繼續照常渲染。

根據 React Fiber 的設計,一個組件的渲染被分爲兩個階段:第一個階段(也叫作 render 階段)是能夠被 React 打斷的,一旦被打斷,這階段所作的全部事情都被廢棄,當 React 處理完緊急的事情回來,依然會從新渲染這個組件,這時候第一階段的工做會重作一遍;第二個階段叫作 commit 階段,一旦開始就不能中斷,也就是說第二個階段的工做會穩妥當當地作到這個組件的渲染結束。

兩個階段的分界點,就是 render 函數。render 函數以前的全部生命週期函數(包括 render)都屬於第一階段,以後的都屬於第二階段。

開啓異步渲染,雖然咱們得到了更好的感知性能,可是考慮到第一階段的的生命週期函數可能會被重複調用,不得不對歷史代碼作一些調整。

在 React v16.3 以前,render 以前的生命週期函數(也就是第一階段生命週期函數)包括這些:

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • componentWillMount
  • render

React 官方告誡開發者,雖然目前全部的代碼均可以照常使用,可是將來版本中會廢棄掉,爲了未來,使用 React 的程序應該快點去掉這些在第一階段生命函數中有反作用的功能。

一個典型的錯誤用例,也是我被問到作多的問題之一:爲何不在 componentWillMount 裏去作AJAX?componentWillMount 但是比 componentDidMount 更早調用啊,更早調用意味着更早返回結果,那樣性能不是更高嗎?

首先,一個組件的 componentWillMount 比 componentDidMount 也早調用不了幾微秒,性能沒啥提升;並且,等到異步渲染開啓的時候,componentWillMount 就可能被中途打斷,中斷以後渲染又要重作一遍,想想,在 componentWillMount 中作 AJAX 調用,代碼裏看到只有調用一次,可是實際上可能調用 N 屢次,這明顯不合適。相反,若把 AJAX 放在 componentDidMount,由於 componentDidMount 在第二階段,因此絕對不會屢次重複調用,這纔是 AJAX 合適的位置

getDerivedStateFromProps

到了 React v16.3,React 乾脆引入了一個新的生命週期函數 getDerivedStateFromProps,這個生命週期函數是一個 static 函數,在裏面根本不能經過 this 訪問到當前組件,輸入只能經過參數,對組件渲染的影響只能經過返回值。沒錯,getDerivedStateFromProps 應該是一個純函數,React 就是經過要求這種純函數,強制開發者們必須適應異步渲染。

static getDerivedStateFromProps(nextProps, prevState) {
  //根據nextProps和prevState計算出預期的狀態改變,返回結果會被送給setState
}
複製代碼

React v16發佈時,還增長了異常處理的生命週期函數。

若是異常發生在第一階段(render階段),React就會調用getDerivedStateFromError,若是異常發生在第二階段(commit階段),React會調用componentDidCatch。這個區別也體現出兩個階段的區分對待。

適應異步渲染的組件原則

當 React 開啓異步渲染的時候,你的代碼應該作到在 render 以前最多隻能這些函數被調用:

  • 構造函數
  • getDerivedStateFromProps
  • shouldComponentUpdate

倖存的這些第一階段函數,除了構造函數,其他兩個全都必須是純函數,也就是不該該作任何有反作用的操做。


Suspense帶來的異步操做革命

Suspense 應用的場合就是異步數據處理,最多見的例子,就是經過 AJAX 從服務器獲取數據,每個 React 開發者都曾爲這個問題糾結。

若是用一句話歸納 Suspense 的功用,那就是:用同步的代碼來實現異步操做。

React 同步操做的不足

React 最初的設計,整個渲染過程都是同步的。同步的意思是,當一個組件開始渲染以後,就必須一口氣渲染完,不能中斷,對於特別龐大的組件樹,這個渲染過程會很耗時,並且,這種同步處理,也會致使咱們的代碼比較麻煩。

當咱們開始渲染某個組件的時候,假設這個組件須要從服務器獲取數據,那麼,要麼由這個組件的父組件想辦法拿到服務器的數據,而後經過 props 傳遞進來,要麼就要靠這個組件自力更生來獲取數據,可是,沒有辦法經過一次渲染完成這個過程,由於渲染過程是同步的,不可能讓 React 等待這個組件調用 AJAX 獲取數據以後再繼續渲染。

經常使用的作法,須要組件的 render 和 componentDidMount 函數配合。

  1. 在 componentDidMount 中使用 AJAX,在 AJAX 成功以後,經過 setState 修改自身狀態,這會引起一次新的渲染過程。
  2. 在 render 函數中,若是 state 中沒有須要的數據,就什麼都不渲染或者渲染一個「正在裝載」之類提示;若是 state 中已經有須要的數據,就能夠正常渲染了,但這也一定是在 componentDidMount 修改了 state 以後,也就是隻有在第二次渲染過程當中才能夠。

下面是代碼實例:

class Foo extends React.Component {
  state = {
    data: null,
  };

  render () {
    if (!this.state.data) {
      return null;
    } else {
      return <div>this.state.data</div>;
    }
  }

  componentDidMount () {
    callAPI ().then (result => {
      this.setState ({data: result});
    });
  }
}
複製代碼

這種方式雖然可行,咱們也照這種套路寫過很多代碼,但它的缺點也是很明顯的。

  • 組件必需要有本身的 state 和 componentDidMount 函數實現,也就不可能作成純函數形式的組件。
  • 須要兩次渲染過程,第一次是 mount 引起的渲染,由 componentDidMount 觸發 AJAX 而後修改 state,而後第二次渲染才真的渲染出內容。
  • 代碼囉嗦,十分囉嗦。

理想中的代碼形式

而 Suspense 就是爲了克服上述 React 的缺點。

在瞭解 Suspense 怎麼解決這些問題以前,咱們不妨本身想象一下,若是要利用 AJAX 獲取數據,代碼怎樣寫最簡潔高效?

我先來講一說本身設想的最佳代碼形式。首先,我不想寫一個有狀態的組件,由於經過 AJAX 獲取的數據每每也就在渲染用一次,不必存在 state 裏;其次,想要使數據拿來就用,不須要通過 componentDidMount 走一圈。因此,代碼最好是下面這樣:

const Foo = () => {
  const data = callAPI ();
  return <div>{data}</div>;
};
複製代碼

夠簡潔吧,但是目前的 React 版本作不到啊!

由於 callAPI 確定是一個異步操做,不可能得到同步數據,沒法在同步的 React 渲染過程當中立足。

不過,如今作不到,不表明未來作不到,未來 React 會支持這樣的代碼形式,這也就是 Suspense。

有了Suspense,咱們能夠這樣寫代碼:

const Foo = () => {
  const data = createFetcher (callAJAX).read ();
  return <div>{data}</div>;
};
複製代碼

接下來,咱們就介紹一下 Suspense 的原理。

在 React 推出 v16 的時候,就增長了一個新生命週期函數 componentDidCatch。若是某個組件定義了 componentDidCatch,那麼這個組件中全部的子組件在渲染過程當中拋出異常時,這個 componentDidCatch 函數就會被調用。

能夠這麼設想,componentDidCatch 就是 JS 語法中的 catch,而對應的 try 覆蓋全部的子組件,就像下面這樣:

try {
  //渲染子組件
} catch (error) {
  // componentDidCatch被調用
}
複製代碼

Suspense 就是巧妙利用 componentDidCatch 來實現同步形式的異步處理。

Suspense 提供的 createFetcher 函數會封裝異步操做,當嘗試從 createFetcher 返回的結果讀取數據時,有兩種可能:一種是數據已經就緒,那就直接返回結果;還有一種多是異步操做尚未結束,數據沒有就緒,這時候 createFetcher 會拋出一個「異常」。

你可能會說,拋出異常,渲染過程不就中斷了嗎?

的確會中斷,不過,createFetcher 拋出的這個「異常」比較特殊,這個「異常」其實是一個 Promise 對象,這個 Promise 對象表明的就是異步操做,操做結束時,也是數據準備好的時候。當 componentDidCatch 捕獲這個 Promise 類型的「異常」時,就能夠根據這個 Promise 對象的狀態改變來從新渲染對應組件,第二次渲染,確定就可以成功。

下面是 createFetcher 的一個簡單實現方式:

var NO_RESULT = {};

export const createFetcher = task => {
  let result = NO_RESULT;

  return () => {
    const p = task ();

    p.then (res => {
      result = res;
    });

    if (result === NO_RESULT) {
      throw p;
    }

    return result;
  };
};
複製代碼

在上面的代碼中,createFetcher 的參數 task 被調用應該返回一個 Promise 對象,這個對象在第一次調用時會被 throw 出去,可是,只要這個對象完結,那麼 result 就有實際的值,不會再被 throw。

還須要一個和 createFetcher 配合的 Suspense,代碼以下:

class Suspense extends React.Component {
  state = {
    pending: false,
  };

  componentDidCatch (error) {
    // easy way to detect Promise type
    if (typeof error.then === 'function') {
      this.setState ({pending: true});

      error.then (() =>
        this.setState ({
          pending: false,
        })
      );
    }
  }

  render () {
    return this.state.pending ? null : this.props.children;
  }
}
複製代碼

上面的 Suspense 組件實現了 componentDidCatch,若是捕獲的 error 是 Promise 類型,那就說明子組件用 createFetcher 獲取異步數據了,就會等到它完結以後重設 state,引起一次新的渲染過程,由於 createFetcher 中會記錄異步返回的結果,新的渲染就不會拋出異常了。

使用 createFetcher 和 Suspense 的示例代碼以下:

const getName = () =>
  new Promise (resolve => {
    setTimeout (() => {
      resolve ('Morgan');
    }, 1000);
  });

const fetcher = createFetcher (getName);

const Greeting = () => {
  return <div>Hello {fetcher ()}</div>;
};

const SuspenseDemo = () => {
  return (
    <Suspense> <Greeting /> </Suspense>
  );
};
複製代碼

上面的 getName 利用 setTimeout 模擬了異步 AJAX 獲取數據,第一次渲染 Greeting 組件時,會有 Promise 類型的異常拋出,被 Suspense 捕獲。1 秒鐘以後,當 getName 返回實際結果的時候,Suspense 會引起從新渲染,這一次 Greeting 會顯示出 hello Morgan。

上面的 createFetcher 和 Suspense 是一個很是簡陋的實現,主要用來讓讀者瞭解 Suspense 的工做原理,正式發佈的 Suspense 確定會具有更強大的功能。

Suspense 帶來的 React 使用模式改變

Suspense 被推出以後,能夠極大地減小異步操做代碼的複雜度。

以前,只要有 AJAX 這樣的異步操做,就必需要用兩次渲染來顯示 AJAX 結果,這就須要用組件的 state 來存儲 AJAX 的結果,用 state 又意味着要把組件實現爲一個 class。總之,咱們須要作這些:

  • 實現一個 class;
  • class 中須要有 state;
  • 須要實現 componentDidMount 函數;
  • render 必需要根據 this.state 來渲染不一樣內容。

有了 Suspense 以後,不須要作上面這些瑣事,只要一個函數形式組件就足夠了。

在介紹 Redux 時,咱們提到過在 Suspense 面前,Redux 的一切異步操做方案都顯得繁瑣,讀者如今應該可以經過代碼理解這一點了。

很惋惜,目前 Suspense 還不支持服務器端渲染,當 Suspense 支持服務器端渲染的時候,那就真的會對 React 社區帶來革命性影響。


函數化的 Hooks

Hooks 的目的,簡而言之就是讓開發者不須要再用 class 來實現組件。

useState

Hooks 會提供一個叫 useState 的方法,它開啓了一扇新的定義 state 的門,對應 Counter 的代碼能夠這麼寫:

import {useState} from 'react';

const Counter = () => {
  const [count, setCount] = useState (0);

  return (
    <div> <div>{count}</div> <button onClick={() => setCount (count + 1)}>+</button> <button onClick={() => setCount (count - 1)}>-</button> </div>
  );
};
複製代碼

注意看,Counter 擁有本身的「狀態」,但它只是一個函數,不是 class。

useState 只接受一個參數,也就是 state 的初始值,它返回一個只有兩個元素的數組,第一個元素就是 state 的值,第二個元素是更新 state 的函數。

這個例子中,咱們能夠利用 count 能夠讀取到這個 state,利用 setCount 能夠更新這個 state。

由於 useState 在 Counter 這個函數體中,每次 Counter 被渲染的時候,這個 useState 調用都會被執行,useState 本身確定不是一個純函數,由於它要區分第一次調用(組件被 mount 時)和後續調用(重複渲染時),只有第一次才用得上參數的初始值,然後續的調用就返回「記住」的 state 值。

讀者看到這裏,內心可能會有這樣的疑問:若是組件中屢次使用 useState 怎麼辦?React 如何「記住」哪一個狀態對應哪一個變量?

React 是徹底根據 useState 的調用順序來「記住」狀態歸屬的,假設組件代碼以下:

const Counter = () => {
  const [count, setCount] = useState (0);
  const [foo, updateFoo] = useState ('foo');

  // ...
};
複製代碼

每一次 Counter 被渲染,都是第一次 useState 調用得到 count 和 setCount,第二次 useState 調用得到 foo 和 updateFoo。

React 不知道你把 useState 等 Hooks API 返回的結果賦值給什麼變量,可是它也不須要知道,它只須要按照 useState 調用順序記錄就行了。

正由於這個緣由,Hooks,千萬不要在 if 語句或者 for 循環語句中使用!

像下面的代碼,確定會出亂子的:

const Counter = () => {
  const [count, setCount] = useState (0);
  if (count % 2 === 0) {
    const [foo, updateFoo] = useState ('foo');
  }
  const [bar, updateBar] = useState ('bar');
};
複製代碼

由於條件判斷,讓每次渲染中 useState 的調用次序不一致了,因而 React 就錯亂了。

useEffect

除了 useState,React 還提供 useEffect,用於支持組件中增長反作用的支持。

在 React 組件生命週期中若是要作有反作用的操做,代碼放在哪裏?

固然是放在 componentDidMount 或者 componentDidUpdate 裏,可是這意味着組件必須是一個 class。

在 Counter 組件,若是咱們想要在用戶點擊「+」或者「-」按鈕以後把計數值體如今網頁標題上,這就是一個修改 DOM 的反作用操做,因此必須把 Counter 寫成 class,並且添加下面的代碼:

componentDidMount () {
    document.title = `Count: ${this.state.count}`;
  }

  componentDidUpdate () {
    document.title = `Count: ${this.state.count}`;
  }
複製代碼

而有了 useEffect,咱們就不用寫一個 class 了,對應代碼以下:

import {useState, useEffect} from 'react';

const Counter = () => {
  const [count, setCount] = useState (0);

  useEffect (() => {
    document.title = `Count: ${count}`;
  });

  return (
    <div> <div>{count}</div> <button onClick={() => setCount (count + 1)}>+</button> <button onClick={() => setCount (count - 1)}>-</button> </div>
  );
};
複製代碼

useEffect 的參數是一個函數,組件每次渲染以後,都會調用這個函數參數,這樣就達到了 componentDidMount 和 componentDidUpdate 同樣的效果。

雖然本質上,依然是 componentDidMount 和 componentDidUpdate 兩個生命週期被調用,可是如今咱們關心的不是 mount 或者 update 過程,而是「after render」事件,useEffect 就是告訴組件在「渲染完」以後作點什麼事。

讀者可能會問,如今把 componentDidMount 和 componentDidUpdate 混在了一塊兒,那假如某個場景下我只在 mount 時作事但 update 不作事,用 useEffect 不就不行了嗎?

其實,用一點小技巧就能夠解決。useEffect 還支持第二個可選參數,只有同一 useEffect 的兩次調用第二個參數不一樣時,第一個函數參數纔會被調用,因此,若是想模擬 componentDidMount,只須要這樣寫:

useEffect(() => {
  // 這裏只有mount時才被調用,至關於componentDidMount
}, [123]);
複製代碼

在上面的代碼中,useEffect 的第二個參數是 [123],其實也能夠是任何一個常數,由於它永遠不變,因此 useEffect 只在 mount 時調用第一個函數參數一次,達到了 componentDidMount 同樣的效果。

useContext

Context API的設計不完美,在多個 Context 嵌套的時候尤爲麻煩。

好比,一段 JSX 若是既依賴於 ThemeContext 又依賴於 LanguageContext,那麼按照 React Context API 應該這麼寫:

<ThemeContext.Consumer>
  {theme => (
    <LanguageContext.Cosumer> language => { //可使用theme和lanugage了 } </LanguageContext.Cosumer> )} </ThemeContext.Consumer>; 複製代碼

由於 Context API 要用 render props,因此用兩個 Context 就要用兩次 render props,也就用了兩個函數嵌套,這樣的縮格看起來也的確過度了一點點。

使用 Hooks 的 useContext,上面的代碼能夠縮略爲下面這樣:

const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
// 這裏就能夠用theme和language了
複製代碼

這個useContext把一個須要很費勁才能理解的 Context API 使用大大簡化,不須要理解render props,直接一個函數調用就搞定。

可是,useContext也並非完美的,它會形成意想不到的從新渲染,咱們看一個完整的使用useContext的組件。

const ThemedPage = () => {
  const theme = useContext (ThemeContext);

  return (
    <div>
      <Header color={theme.color} />
      <Content color={theme.color} />
      <Footer color={theme.color} />
    </div>
  );
};
複製代碼

由於這個組件ThemedPage使用了useContext,它很天然成爲了Context的一個消費者,因此,只要Context的值發生了變化,ThemedPage就會被從新渲染,這很天然,由於不從新渲染也就沒辦法從新得到theme值,但如今有一個大問題,對於ThemedPage來講,實際上只依賴於theme中的color屬性,若是隻是theme中的size發生了變化可是color屬性沒有變化,ThemedPage依然會被從新渲染,固然,咱們經過給Header、Content和Footer這些組件添加shouldComponentUpdate實現能夠減小沒有必要的從新渲染,可是上一層的ThemedPage中的JSX從新渲染是躲不過去了。

說到底,useContext須要一種表達方式告訴React:「我沒有改變,重用上次內容好了。」

但願Hooks正式發佈的時候可以彌補這一缺陷。

Hooks 帶來的代碼模式改變

Hooks 將大大簡化使用 React 的代碼。

咱們可能再也不須要 class了,只用函數形式來編寫組件。

對於 useContext,它並無爲消除 class 作貢獻,卻爲消除 render props 模式作了貢獻

對了,全部的 Hooks API 都只能在函數類型組件中調用,class 類型的組件不能用,從這點看,很顯然,class 類型組件將會走向消亡。

相關文章
相關標籤/搜索