標籤:

如何評價React的新功能Time Slice 和Suspense?


各位看官好,方正,來了。

自從Dan老哥加入React team以後,React帶來了很多變化,其中Time SlicingSuspense是Dan老哥在「Beyond React 16 by Dan Abramov - JSConf Iceland」上演講的重要題目。

兩個性能問題

這兩點主要用來對付兩個問題:CPU速度和網路IO

兩個性能問題:cpu+io

可以使用React的非同步渲染來解決

就讓我來給大家解釋一下,這兩個玩意兒。

Time Slicing

以下是解釋

時間分片

  • React 在渲染(render)的時候,不會阻塞現在的線程
  • 如果你的設備足夠快,你會感覺渲染是同步的
  • 如果你設備非常慢,你會感覺還算是靈敏的
  • 雖然是非同步渲染,但是你將會看到完整的渲染,而不是一個組件一行行的渲染出來
  • 同樣書寫組件的方式

也就是說,這是React背後在做的事情,對於我們開發者來說,是透明的,具體是什麼樣的效果呢?

親愛的Dan為我們做了一個有趣的Demo:

有圖表三個圖表,有一個輸入框,以及上面的三種模式

這個組件非常的巨大,而且在輸入框每次輸入東西的時候,就會進去一直在渲染。為了更好的看到渲染的性能,Dan為我們做了一個表。

我們先看看,同步模式:

同步模式下,我們都知道,我們沒輸入一個字元,React就開始渲染,當React渲染一顆巨大的樹的時候,是非常卡的,所以才會有shouldUpdate的出現,在這裡Dan也展示了,這種卡!

我們再來看看第二種(Debounced模式):

Debounced模式簡單的來說,就是延遲渲染,比如,當你輸入完成以後,再開始渲染所有的變化。

這麼做的壞處就是,至少不會阻塞用戶的輸入了,但是依然有非常嚴重的卡頓。

切換到非同步模式:

非同步渲染模式就是不阻塞當前線程,繼續跑。在視頻里可以看到所有的輸入,表上都會是原諒色的。

註:簡單的來說,react的非同步渲染其實就是拉長了render的時間,一次跑一點,所以機器性能很差的,會看到react渲染時有稍微的延遲,但是不是卡頓。

Suspense

Suspense主要解決的就是網路IO問題。網路IO問題其實就是我們現在用Redux+saga等等一系列亂七八糟的庫來解決的「副作用」問題。

這一次,這些副作用,React官方終於出手來幫我們做了,我們來看一下Dan在演示中是怎麼做的。

非常簡單的組件,從Json中用id拿到movie信息,然後展示

這是一個很正常的操作,接下來我們看看新的Suspense的做法。

新增一個createFetcherApi

解釋一下,fetchMovieDetails是一個返回promise的api,類似我們的fetch封裝一下。然後注意看

const movie = movieDetailsFetcher.read(props.id)

這行代碼,這行代碼Dan說了,非常的具有爭議性。這行代碼做的與之前的

const movie = movieDetailsJSON[props.id]

是一樣的效果,只不過是從網路中拿來的,會有延遲。

然後拿到數據以後,我們調用一個新的api叫做

this.deferSetState(...);//之前的setState非同步新版本

來綁定數據。

這就與我們之前書寫組件的方式有很大的差異,因為我們知道,在react 的 render()函數中,非同步去fetch數據是沒用的,因為非同步函數會在下一個JS事件循環中才會進行,此時,已經渲染完畢了。所以拿到數據了也沒用。

調用render函數-&>發現有非同步請求-&>懸停,等待非同步請求結果-&>再渲染展示數據

由此可以看到,我們可以用一種同步的方式去書寫代碼,就像我們寫async/await一樣!是不是非常的爽!?這就是Suspense的核心功能

那麼我們來解釋一下Suspense

懸停(也可以叫做暫停)

  • 引入新的api,可以使得任何state更新暫停,直到條件滿足時,再渲染(像async/await)
  • 可以在任何一個組件里放置非同步獲取數據,而不用做多餘的設置
  • 在網速非常快的時候,可設置,整個數據到達Dom,更新完畢以後再渲染
  • 在網速非常慢的時候,可設置,精確到單個組件的等待、以及更新,然後再渲染
  • 會給我們提供 high-level (createFetcher)和 low-level( Placeholder, Loading ) 的 API,可以供給業務代碼和一些小組件的書寫。

總結一下:React的未來是什麼?

我們回顧一下react 16給我們帶來的一些列重要變化:

  1. render/純組件能夠return任何數據結構,以及CreatePortal Api
  2. 新的context api,嘗試代替一部分redux的職責
  3. babel的&<&>操作符,方便用戶返回數組
  4. 非同步渲染/時間切片(time slicing),成倍提高性能
  5. componentDidCatch,錯誤邊界,框架層面上提高用戶debug的能力
  6. 未來的Suspense,優雅處理非同步副作用

這麼一些列的變化,我們可以清晰的看到,redux作者,現任react的大哥dan,以及其背後的團隊,正在嘗試將React打造成一個「大而全」的框架,而不是像原來那樣,讓社區自己發展,React只做view層。

對於熟練的開發者來說,這是非常非常棒的事情,但是同時,react的api也開始慢慢多了起來,對於初學者來說,是一個不友好的信號。

1

回過頭來說,Time Slice,得益於fiber,使得在執行任務的期間可以隨時暫停,跑去干別的事情,這個特性使得react能在性能極其差的機器跑時,仍然保持有良好的性能。

在很大程度上,我們即使不手動優化react,我們也能夠獲得非常好的性能表現,這對於不要過早優化的項目來說,是一個非常好的信號,如創業公司的項目

2

Suspense功能想解決從react出生到現在都存在的「非同步副作用」的問題,而且解決得非常的優雅,使用的是「非同步但是同步的寫法」,我個人認為,這是最好的解決非同步問題的方式。

調用render函數-&>發現有非同步請求-&>懸停,等待非同步請求結果-&>再渲染展示數據

總的來說,框架的拼比,已經從「代碼易維護」-&>「性能之戰」-&>「快速構建應用」演變到了如今「快速構建高性能並且易維護應用」。


今早阮一峰老師轉發了一則新聞,說 Dan宣布,Rip Redux(安息吧Redux)其實,Dan在演講中並沒有這麼說,並沒有這麼說,並沒有這麼說,他只是說「某些問題不用redux去解決了」。

參考文獻:

Sneak Peek: Beyond React 16 - React Blog?

reactjs.org


Time Slice 很早之前就公布了。可以從各種 demo 里看到很明顯的性能提升,非常厲害!

不過這次 Talk 中更讓人激動的應該說是 Suspense 了,Dan 幾天前就在 twitter 里製造「懸念」:

"things that would take me hours to build before, I can now do in 30 minutes."

直到昨晚的 Talk,」懸念「 被揭開:

我們可以看一下 demo 中的示例代碼:

import { createFetcher, Placeholder, Loading } from ../future;
// ...

const movieDetailsFetcher = createFetcher(
fetchMovieDetails
);

function MovieDetails(props) {
const movie = movieDetailsFetcher.read(props.id);

return (
&{movie.title}& );
}

通過 」來自未來「 的幾個 API,達到了這樣的效果。fetchMovieDetails 是返回 Promise 的非同步方法,在通過 createFetcher 的包裝之後,就可以直接在純函數組件的 render 方法里,同步的調用 read 方法獲得數據,再正常 render。

這看起來」違背「了 React 的常識:不能在 render 中發起請求。但 Dan 告訴我們,先試試看效果。運行效果當然是非常完美的。

這是咋跑起來的?

Andrew Clark 在 Talk 結束後公布了一些相關信息,推薦大家直接去看看。大致意思是這樣的:

  • 在 render 方法中,讀取 緩存 中的值
  • 如果緩存中存在這個值,render 方法會正常執行
  • 如果緩存中沒有這個值,緩存 會拋出一個異常,異常內容是一個 Promise
  • 當這個 Promise resolve 的時候,React 會從它之前停下的地方開始重試
  • 上面提到的緩存是通過前不久公布的新 context API 來實現的

Placeholder 和 Loading

接下來,Dan 還給我們展示了兩個更接近真實業務場景的組件。Placeholder 可以通過 delayMs 和 fallback 的配合在 fetcher 的數據還未獲取到時做過渡。而 Loading 可以通過 render prop 來告訴你當前 fetcher 的 loading 狀態。很難準確的通過語言把 demo 實現的效果表達出來,強烈建議去看看演示的視頻。

Async

createFetcher 可以做的其實遠不止數據的獲取,demo 還進一步演示了 Code split 和 image 載入這兩個非同步的渲染問題。

在 createFetcher 中直接使用動態 import 獲取組件

在 createFetcher 中等待圖片的載入來防止 render 時因為圖片載入產生的高度抖動

Suspense

Dan 把這個 feature 稱為 Suspense,應該是取它 「懸念」 這個含義的雙關,和 「暫停,懸停」 的意思。有了它,寫 React 可以變成這樣。

  • 在數據還未準備好時,暫停狀態的更新
  • 對任何組件都可以輕鬆添加非同步的數據,不會產生阻塞
  • 網速快的時候,在整個 tree ready 的時候直接 render
  • 網速不好的時候,可以精確控制 loading 的狀態
  • 會給我們提供 high-level 和 low-level 的 API,可以有 Placeholder, Loading 這樣直接在業務場景中的組件,也會有給庫使用的底層 API


Time Slice其實就是React v16還是beta時宣傳的Fiber的直接應用,參照 React Fiber是什麼 ,這沒有什麼讓人驚喜的,真正讓人掉下巴的是Suspense。

之前,React一直關注於渲染,根據數據產生視圖,不摻和如何獲取數據,尤其不關心如何通過AJAX獲取數據,導致數據獲取的方式千奇百怪,很不統一。現在有了Suspense,是React終於涉足於數據獲取的方案,而且真的非常巧妙。

將來的code基本上類似這樣:

import createFetcher from future-react;
const fetchSomething = ... // AJAX的實現

const SomethingFetcher = createFetcher(fetchSomething);

render() {
const data = SomethingFetcher.read();
return &;
}

注意看,fetch的過程是非同步的,但是上面的代碼卻是同步的,而且也沒有用async/await。

背後的原理,是createFetcher產生的SomethingFetcher在被調用read時,看看cache里是不是有結果了,如果有,就同步返回;如果沒有,那就throw出一個Promise(注意不是throw出Error),這個Promise被React捕獲之後,就會暫停渲染過程,等到這個Promise對象resolve的時刻再重新開始渲染,而這個時候數據也已經獲得了,所以data也就有值了。

不得不說這招真是非常巧妙,表面上看,throw會產生性能問題,但是在渲染過程中throw的代價應該是九牛一毛。


先提供一個我隨便玩了玩Suspense的文章

張立理:超簡化的React Suspense實現?

zhuanlan.zhihu.com圖標

Time Slicing是Fiber的完全體形態,這個我不作過多的分析,更多地是談一下其它部分的觀點:

首先往大了說,從這一刻開始是React與其它React Compatible框架的的分水嶺。之前由於React的精簡,能簡單地衍生出完全與React兼容的框架,如preact、inferno、qreact、nerv等,但從現在開始,React在其新的架構上開始堆疊功能,如果要繼續保持React兼容,這些框架要投入的精力將直線上升。那麼你是不是能跟得住Facebook的腳步呢?

然後,Suspense的最基礎功能是在當下直接能實現的,只要有Error Boundary就行,如我上面的文章中已經給了示例。因此如果你真的覺得Suspense對你很有幫助,而且也只是要做一下「在render里搞個非同步大新聞」的功能,那麼直接我行我上實現一個用得了。

回歸Suspense本身,我對其的基礎評價是:這是一個實現特定場景下的高效開發的利器,但並沒有神奇到值得你鼓掌。

先來說特定,這樣的開發模式意味著如果你要使用Suspense,你將放棄:

  1. 較遠距離的組件間的數據共享,因為沒有全局狀態的支持。更多的時候,Suspense用在數據獨立的場景下。當然你可以硬生生套回到全局狀態上去,但這時Suspense還有用嗎
  2. 可跟蹤性。很多時候,我們使用Redux等框架並不僅僅是為了數據的共享,不如說大部分時候我們規劃全局狀態的結構時,就是有很多區塊根本不共享的。但是Redux提供的機制、提供的工具確實增加了可跟蹤性與可調試性(如時光機),這些在Suspense上的支持尚未可知
  3. 簡化的可測性。你可以設計FetcherFactory然後再根據場景來mock fetcher重新擁有可測性,但就我看來,做得到這程度的前端……嗯……並不多?
  4. 聲明式編程的堅持。原始的React設計中render是一個純聲明式的,由一個數據(props)驅動的純函數,我不知道有多少人領悟了這裡的「聲明式」的要義,但現在引入Suspense後我認為對聲明式是有破壞性的,render正在向命令式變化,這是我覺得比較悲哀的一點,當更多地人失去了對聲明式的執著之後,一個React應用會變得更難以維護。

從這些方面來看,我武斷地認為,在複雜的應用系統中,使用Suspense應該被限制

從另一個角度說,如果只是為了當前Suspense表現出來的能力,那麼不用Suspense也不會造成任何的麻煩,比如我們現在是這麼做的:

function MovieDetailsInfo({movie}) {
return (
&
&
&{movie.title}& &
& );
}

const fetchMovie = props =&> fetchMovieDetails(props.id);
const MovieDetails = requireFetch(fetchMovie)(MovieDetails);

我就問你有個啥區別。所以我暫時沒有理解最初的那句「things that would take me hours to build before, I can now do in 30 minutes」是想要怎麼推算。

再追加一個缺點。因為Suspense把一個「有狀態的」及「有副作用」的過程引入到了一個原本一直強調是純函數的函數中,對於無法真正理解其核心的人來說,這會變成一個基於誤解被「模仿」成各種錯誤實踐的源頭

比如能不能在render里dispatch一個action呢?能不能在render里調用callback prop通知上游呢?能不能在render里來一發window.location.href = xxx呢?你看你都能非同步載入數據了,我有什麼不能的呢?

於是,一個應用就這麼殘廢了,這是我最最害怕看到的結果。

再來說說優點吧。Suspense這個東西設計允許render中有多少個非同步操作我現在還不知道,但我希望並堅定地認為只會允許一個。

為什麼我這麼說呢?當只有一個的時候,如果你在render函數中希望發起多個非同步請求,你會怎麼做?答案很顯然,就是拆組件。

你看,僅僅因為這樣的一個小小的限制,你就會自覺得用一個非常靠譜的實踐(數據的粒度)去正確地拆解組件了。這是我很想要強調的,優秀的設計能讓人自覺得寫出優秀的代碼(上一段你不是這麼說的喂!)。

關於優秀的設計引導優秀的代碼,是我最近一直在深度和研究的一個話題。近期在制定前後端的API規範的時候,對這一點也感慨頗深。我選擇了一個看上去很特立獨行的規範,一個看上去需要很多封裝工作並沒有現成框架支持的規範,也就是為了在規範的引導下,大家能在設計系統的時候看到問題、提供良好正確的設計。

總結一下:

  1. 使用異常來實現Suspense我並不認同
  2. Suspense的設計我認為好用但要限制
  3. 如果你只是想要Suspense的非同步載入數據的能力,自己實現一個就可以開幹了
  4. 其實搞個HOC早就有提供這能力
  5. Suspense表現出來的對render函數的認知的破壞可能會造成潛在的React應用混亂和質量下降
  6. 好的設計往往要同時引導人去寫好的代碼


單說 Suspense 吧。

最近遇到了類似場景,思路和解決方案跟 Suspense 很類似。

  • Render 時讀緩存,緩存存在則正常渲染
  • 緩存不存在時,渲染一個佔位組件,同時創建一個 promise
  • promise resolve 的時候,把之前的佔位組件用新的組件替換掉。

跟 Suspense 相比雖然山寨了點,但是用著還是不錯的。

為了解決的問題是,通過參數動態載入組件的時候,在組件載入完畢之前就可以把它當成一個普通組件使用,而不需要為父組件添加額外生命周期。


原來這才是React這個名字更好的解釋,之前對於React,是偏向開發者的角度來看的,也就是數據響應頁面,通過修改數據去驅動界面修改,而等到React16的出現,我覺得對React的理解的是偏向用戶體驗的,能讓界面快速響應用戶的操作。可能有點文不對題,但是Time Slice和Suspence都是手段,Responsive才是目的。


新特性的介紹其實直接去看官博就足夠了:

Sneak Peek: Beyond React 16 - React Blog?

reactjs.org圖標

既然問的是『如何評價』,那當然是回答一些自己的看法吧。

我一直覺得,前端框架經過這麼多年的發展,軍備競賽已經進入下半場了,因為在頁面性能、開發體驗or效率上,之間已經很難有明顯的差異了(雖然較真的話還是有些區別,但不是那麼重要了)

那麼下一個競爭的熱點顯然就是用戶體驗,如果能讓開發者能在最短時間內快速寫出用戶體驗更佳的應用,那麼就很容易得到青睞。

React 從 v16 開始的一系列新特性(錯誤邊界、Fragment、Portal),大多數都是關注於如何幫助開發者最大程度地提升用戶體驗,這次的 Time Slice 和 Suspense 當然也不例外。尤其是 Suspense 可以很優雅地解決 Loading 態、佔位符以及避免閃屏的問題。

我們曾經的前端工程過於關注一些技術指標,比如大部分前端優化方法都是在告訴你怎麼讓資源體積更小、載入速度更快、首屏時間更短,而讓我們忽略了『用戶體驗』本身。

大部分前端工程對於頁面載入速度的優化已經比較完善了,很難獲得數量級上的優化了。更重要的是,頁面本身載入快慢個幾百毫秒真的很難感知出來,但有沒有做好載入態、有沒有優雅漂亮的內容展現方式(而不是唐突的閃出來),這些細節才是用戶真正能感知到的,也是我們未來更需要重視的,而 React v16 也正在幫助我們完成這些事情。


我在一篇文章中詳細描述了 React 核心成員 Dan 在演講中提到的 Time Slicing 和 Suspense。

編譯青春:React 的未來:Time Slicing 和 Suspense?

zhuanlan.zhihu.com圖標


覺得這個功能可有可無呀。。。不是特別好,而且破壞了聲明。其實這個功能在 Provider 的概念來做就行了,不過還沒項目中使用過,所以不能說完全不好,只是覺得其實不需要官方侵入來做這個事情。

還有就是不要讓它變得更重其實比較好,React 已經幫前端解決不少問題了,就目前非同步數據管理還未統一,可能官方也想提供一個比較好的實踐吧。

(???ω???)搞來搞去,這層用後端的成熟經驗就好。


推薦閱讀:

如何理解 Vue.JS 2016年的 github 星標( Star )數量增長超過 React ?
TypeScript在React高階組件中的使用技巧
koa 實現 react-view 原理
如何看待文章<WHY REACT/REDUX IS AN INFERIOR PARADIGM>?
使用 React.js 的漸進式 Web 應用程序:第 3 部分 - 離線支持和網路恢復能力

TAG:React |