如何評價React v16.0?

React v16.0 - React Blog


北京時間2017年9月27日,Facebook 官方發布了 React v16.0。相較於之前的 v15.x 版本,v16 版本在異常處理,服務端渲染和核心架構方面(引入 Fiber)都有了重大更新。

讓我們先來瀏覽一下核心的更新點:

  1. render 函數支持返回數組和字元串:終於不需要再將多個同級元素包裹在一個冗餘的 DOM 元素中了,但每個同級元素還是需要唯一的 key 值方便 react 進行更新。而且在未來,react 可能還會提供一個特殊的 jsx 片段來支持無 key 值的 DOM 元素。
  2. 更好的異常處理:在老版本的 react 中,某個組件在 render 階段的運行錯誤可能會 break 掉整個應用,而且拋出的異常信息含義也非常模糊,難以確定錯誤的發生位置。在 v16.0 中,如果某個組件在執行 render 或其他生命周期函數時出錯,整個組件將被從根節點上移除掉,方便開發者快速定位異常組件。在定位到異常組件後,開發者可以為該組件添加 componentDidCatch 方法,並在這個方法中為組件定義一個備用視圖用於渲染異常狀態下的組件。當然,在這個新的生命周期函數中,開發者也可以獲得更加有幫助的錯誤信息進行 debug。這被稱作組件的錯誤邊界,大家可以理解為組件層面的 try catch 聲明。
  3. 新的組件類型 portals:ReactDOM.createPortal(child, container) 可以將子組件直接渲染到當前容器組件 DOM 結構之外的任意 DOM 節點中,這將使得開發對話框,浮層,提示信息等需要打破當前 DOM 結構的組件更為方便。
  4. 更好的服務端渲染:與之前 renderToString 方法不同,新版本提供的 renderToNodeStream 將返回 Readable,可以持續產生位元組流(a stream of bytes)並在下一部分的 document 生成之前將之前已生成的部分 document 傳回給客戶端。通常來講,新的服務端渲染將比老的快3倍以上。在 document 到達客戶端之後,新版本的 react 也將不會再去將客戶端的初次渲染結果與服務端的渲染結果進行比較,而是儘可能地去重用相同的 DOM 元素。
  5. 支持自定義 DOM 元素:新版本將不會再拋出不支持的 DOM 元素錯誤,而是將所有開發者自定義的 DOM 元素都傳遞到相應的 DOM 節點上。
  6. 更小的打包大小:總體體積減少 30%
    1. react is 5.3 kb (2.2 kb gzipped), 老版本 20.7 kb (6.9 kb gzipped)
    2. react-dom is 103.7 kb (32.6 kb gzipped), 老版本 141 kb (42.9 kb gzipped)
    3. react + react-dom is 109 kb (34.8 kb gzipped), 老版本 161.7 kb (49.8 kb gzipped)
  7. MIT 許可:除了最新的 16.0 版本外,Facebook 還發布了使用 MIT 許可的 15.6.2 版本,以方便無法立刻升級的使用者。
  8. 新的核心架構 Fiber:新版本將使用 Fiber 作為底層架構。正是得益於 FIber,上述提到的支持返回數據及錯誤邊界等功能才變得可能。Fiber 相較於之前最大的不同是它可以支持非同步渲染(async rendering),這意味著 React 可以在更細的粒度上控制組件的繪製過程,從最終的用戶體驗來講,用戶可以體驗到更流暢交互及動畫體驗。而因為非同步渲染涉及到 React 的方方面面甚至未來,在 16.0 版本中 React 還暫時沒有啟用,並將在未來幾個月陸續推出。

===

過去一段時間關於專利問題的討論讓 React 著實失去了一部分忠實的支持者,也讓更多的前端開發者開始思考前端開發的地基及本質。大家探討的問題也從選擇哪一個前端框架轉移到了

  1. 前端框架是否應當成為前端開發的最底層依賴,vanilla javascript 是否應該被開發者徹底遺忘?
  2. 使用 vanilla javascript 來做組件庫,並在上層使用各個框架來進行封裝以達到無框架傾向是否是一個好的選擇?
  3. 站在 React 的基礎上,一個更優秀的前端框架到底應該具備哪些功能及特點?

對於前端開發的未來,React v16.0 的這次更新應該已經給出了一個明確的信號,那就是從微觀上以更細的粒度去控制整個渲染過程,以達到流暢的交互與動畫效果;從宏觀上讓渲染本身與平台解耦,使用相同的核心演算法及架構去適配不同平台的介面,以達到最終橫跨所有終端的目的。

對於未來永遠都沒有一個正確答案,而 React 的探索值得所有人尊重。


說個很多人沒有提到的——對大量的React-like庫的影響。像inferno/preact/anu/react-lite,他們都在v16上面臨了一個重大拐點。

正如很多文章提到的,這次換Fiber Reconciler帶來了全新的特性,比如render函數可以返回數組、字元串、數字。這在Stack Reconciler上是很難做到的,而上述的react-like庫,無一例外都是使用的Stack Reconciler。

現在大家剛開始從v15遷移,無論是React@16 還是上述React-like庫們都保持了對v15的高度兼容,因此暫時沒什麼問題。

等到Fiber的特性慢慢推廣,甚至開放出更多可能性(比如非同步模式)的時候,上述React-like們未來如何跟進就成了問題。

可能有人要說,React-like們重寫成fiber架構就行了唄。

這麼說可能忽視了Fiber這東西的複雜度,以前StackReconciler其實只是簡單的遞歸函數調用,不到300行代碼就能演示個大概,而現在的Fiber,React團隊號稱搞了一年,雖然已經正式發布,但目前還是只開放了同步模式,非同步渲染的調度到現在也還沒完全搞定,複雜度完全不在一個數量級

個人開發者自己從頭擼React-like的時代,從v16開始可能要划上句號了

不是說不可能,我相信還是有很多牛人"能"自己擼的,但是這種複雜度高度集中,重寫一遍很難帶來好處的東西,重複造輪子是不划算的。

寫React-like庫,最大的優化點其實是renderer——即ReactDOM的部分,無論是去掉瀏覽器兼容代碼還是事件系統,說白了都是和DOM相關。把整個React都重寫一遍,既有歷史原因(以前ReactDOM只是個空殼子),也因為Stack Reconciler並不複雜且無從復用,只能一鍋端。

而在Fiber中,不僅React和Renderer高度解耦,Reconciler也是純調度、和宿主環境無關、可復用的。

在Fiber中一個Renderer是這樣初始化的:

ReactDOMRenderer = ReactFiberReconciler({
getRootHostContext() {},
createTextInstance() {},
//...
appendChild: appendChild,
appendChildToContainer: appendChild,
insertBefore: insertBefore,
insertInContainerBefore: insertBefore,
removeChild: removeChild,
removeChildFromContainer: removeChild,
//...
});

把宿主環境相關的操作函數以配置的形式注入到FiberReconciler中即可。具體可參考官方的ReactNoopEntry 或 ReactDOMFiberEntry

也就是說,第三方完全可以使用Fiber的官方Reconciler來構建自己的Renderer——不僅是優化DOM Renderer,也可以用到其它宿主環境,比如渲染一個docx。

這裡推薦:nitin42/Making-a-custom-React-renderer ,感興趣的可以看下。

簡單的總結,以後寫react-like會非常困難,但只寫Renderer的話,卻是大大簡化了,第三方的Renderer生態,會變得更加繁榮。

這也應該會是react-like們下一步的發展方向。

最後:目前v16正式發布版沒有提供ReactFiberReconciler(之前的alpha版可以通過react-dom/lib訪問到),有相應的issue和討論,最終可能會以單獨的create-react-renderer的方式出現,感興趣的可以先用alpha練手。


1. 解決了一些開發時比較惱火,但又不是無法解決的困擾

  • 可以return array了,不用非得戴套
  • Modal和tooltip 的實現有了Portals後可以實現得更優雅,特別是 Portals是按React組件樹上的層級來冒泡事件的,就是即使Portals產生的Dom是當前容器組件之外,但事件(比如onclick)還是可以冒泡到容器組件。不過這個特性可能也會帶來一些困擾
  • Error Boundaries 可以讓錯誤處理更優雅些,而且最關鍵的是,一旦你利用Error Boundaries 處理好了漏網的運行時錯誤,React就不會處於一個異常狀態,你就無需通過刷新頁面的方式來恢復你的整個程序。這其實更讓React 在容錯性方面的體驗能接近原始的JS開發(就是不管有多少錯誤,錯誤有多嚴重,僅僅出錯的部分受影響,其它無關功能運行照舊。就拿我的實際情況說,北極的房子炸了,地球沒事,我還可以住南極的房子 :PPP)
  • 支持自定義屬性

2. 性能相關的

  • 文件尺寸的減小,主要應該得益於rollup的使用,去除屬性白名單也有一點幫助(支持自定義屬性附帶的一個好處)
  • 伺服器端渲染的全面提升(等下詳細講講)

3. 伺服器端渲染邏輯的重寫

  • 整個邏輯的徹底重寫,之前伺服器端渲染和客戶端渲染基本是走同一套邏輯的。讓我們閉上眼睛想一想,有什麼明顯的問題 ------------- &>VIRTUAL DOM 。。。。伺服器端生成virtual dom幹嘛,原來以前就是為了生成而生成,然後在renderToString return後就把它拋棄了。R16 React Core Team 徹底重寫了伺服器端的渲染邏輯,這些都不是問題了
  • 支持node 的stream, 這個也是一個期待已久的支持了,不過會有些限制
  • 重寫的時候把所有針對process.env.NODE_ENV的檢測都去掉了,其實也不是真的去掉,只是在一開始時檢測一下(估計可能是把結果賦給一個內部變數,讓後所有地方都直接檢測這個內部變數吧),為什麼要這麼干,因為調用process.env.NODE_ENV在node.js裡面是個很耗時的操作(針對一般的變數讀取),沒事不要大量調用。理論上,以前做服務端渲染時,要把react用webpack之類的工具再打包一下(去掉process.env.NODE_ENV),再用於服務端才行,現在這一步就免了
  • 生成的HTML更乾淨了(:P 就像v15的拿到洗衣機洗過一樣),data-reactid屬性去掉了,那些不知道什麼時候出現的comment 也去掉了(e.g &This is some &),一句話,現在的生成的html, 應該就像你沒用過react 一樣(為什麼應該,因為我還沒試過)
  • 客戶端基於服務端生成HTML做渲染的邏輯也變的更有效率了,為了兼容,加了個方法hydrate。現在也不要求服務端生成的和客戶端生成的HTML100%匹配了(你也不會看到那個checksum warning了)
  • 性能的提高,如果拿現在最差的情況 [node4.8.4+v15+沒其它優化] 和 [node8.4.0 + v16] ,用一組偽數據來測試,性能差了225 / 13 = 17倍。實際情況肯定沒那麼誇張,這也和你之前做得有多差有關 :P,估計1~3倍左右吧,用上stream 的話用戶體驗會進一步加成,嗯,而且這個測試不知有沒算上客戶端渲染的

4. 了解的越多,越覺得重要的

  • MIT license, 這個沸沸揚揚了好一陣,如果再不改,可能React 會給更多的大公司放棄,也帶了一個壞榜樣給open source 。其實facebook做得比不是太差,只是可能沒想到React 那麼成功。如果再去掉原來的BSD+Patents, 很多人的態度恐怕就是 「我能理解你,但我還是得放棄你「,現在facebook終於開了個好頭,大家也多支持支持吧。不過不知道reactnative有沒希望
  • 新的核心架構 Fiber,這個之前了解不多,不過怎麼都感覺是個很厲害的東東。

個人意見,覺得R16最重要的是 MIT license (非技術層面),然後就是 伺服器渲染,感覺以前的整個伺服器端渲染的實現在react里的地位,就是個小婢女,R16後應該可以是個小妾了,可以正式用用了(不過事實上不少大的網站已經在用了,網易嚴選,pinterest.com,知乎應該也是)。其它一些開發友好的改進也是不錯地, 而Fiber的出現讓我們有更多的期待。


從15.5升級到了16,基本上不用改動多少東西。16里最喜歡的是Portals,使得寫諸如&之類組件可以變得更放肆了。原本需要自己處理props update,現在直接交給react來做就行了。</P><P>另外,升級後addon不能用了(ref:Plan for Addons in React 16 · Issue #9207 · facebook/react)。</P><P>影響比較大的一點是,如果你使用了同構技術。之前的版本檢測到不匹配會將服務端渲染捨棄,用客戶端渲染。而16中會最大化利用服務端渲染,從而導致頁面詭異的布局。如果你使用了同構+熱部署作為開發環境,該現象尤為明顯。(ref:React 16 SSR - hydration on client not working if server html !== client html · Issue #10591 · facebook/react)</P><P>a不過好在,這不影響生產環境(如果你寫的同構代碼邏輯正確的話)。現在對開發環境的熱更新代碼做了一些調整,頁面做兩次hydrate。第一次用於client對server的同構檢測,檢測完畢後直接捨棄這次render。將容器清空,從新做一次render:</P></p> <p><center> <script src="/336-4.js"></script></center></p> <p><P><code class="language-js">function doRender() {<br /> hydrate(<br /> ...,<br /> $root,<br /> );<br /> }</p> <p>doRender();</p> <p>if (isDev) {<br /> // React 16 isomorphic logic changed: https://github.com/facebook/react/issues/10591<br /> unmountComponentAtNode($root);</p> <p> Promise.resolve().then(() =&> {<br /> $root.innerHTML = "";<br /> console.log("============ Client Render ============");<br /> doRender();<br /> });<br /> }<br /> </code></pre> <p><P>(PS:用的redux,所以不用擔心狀態非同步的問題。)</P><P>還不快點歡樂的升級一下~~</P></P></p> <hr /><P>好棒啊!終於正式發布了!</P><P>還沒來得及玩玩正式版,但從以往的beta到rc版看來,開發體驗最大的不同是出錯提示改進了很多,以往經常在console里大段大段紅色的不知所云的什麼React Composite Component看不見了,現在渲染出錯就默認全unmount。</P>我開了一個Live《深入理解React v16新功能》https://www.zhihu.com/lives/896398188230103040 過完十一假期之後一起來感受一下React新版的應用體驗吧。</P></p> <hr /><P>React v16 裡面還是有非常多不錯的特性,部分是解決了長久的痛點,比如 Error Boundary、Render 多個節點,部分是對原有功能做了增強,比如 Portal、DOM 屬性,不管怎麼說,學會如何使用它改進我們的開發方式、代碼質量才是王道,我專門錄製了《React 16 新特性嘗鮮》視頻教程,共6小節,約 20 分鐘,用實際代碼演示下面幾個話題:</P></p> <ol> <li>用 Error Boundary 優雅的處理錯誤</li> <li>在 render 中返回沒有容器元素的多個元素</li> <li>用 Text Only Component 減少 DOM 層級</li> <li>用 createPortal 把組件渲染到當前組件樹之外</li> <li>更加自由的 DOM 屬性</li> <li>在 setState 時用 null 避免不必要的渲染</li> </ol> <p><P>已經發布在我的專欄《前端周刊》上供大家觀看,送給期望與時俱進的同學:React 16 新特性嘗鮮實戰教程</P></P></p> <hr /><P>曾經有個人對我說,那些第三方3KB就能寫個react出來怎麼可能功能完整,你當Faceboom是SB嗎把react寫這麼大</P></p> <p><center> <script src="/336-5.js"></script></center></p> <p>當時我就想對他說:是的</P></p> <hr /><P>在前段時間沸沸揚揚的的證書風波終以 facebook 的妥協告終之後,Facebook團隊馬不停蹄的推出了 Recat 的v16.0版本,先不說證書這個事件怎麼看,關注 React 的同學們都知道,v16.0絕對是一次大變化,底層源碼重構,發布新的 Fiber 核心演算法,加入一系列呼喚許久的新特性,渲染性能優化,文件大小減少等等。那麼下面就詳細說說。</P></p> <h2><b>新的核心演算法 Fiber</b></h2> <p><P>早在2015年的時候,fb官方就放出消息和文章,要著手開發新的底層演算法框架去支持 React 的更新換代。終於在經歷2年多的開發之後,這次新版本最重要的更新,就是這個名為 Fiber的核心底層演算法,全新演算法將改變 React 組件原有的渲染方式,不僅如此,Fiber 幾乎是重寫了整個 React 框架,以支持一系列的新特性和性能提高。在新的基礎演算法的支持下,React 變得更為強大也更有潛力。那麼在 React 中是怎麼做到的呢?那就要先說一說這個 Fiber。</P><P>fb團隊從開始設計 Fiber 時,就把它當做是 React 框架在未來發展的基礎結構。Fiber 對框架的渲染和任務調度都有新的設計和實現,以實現在正確時的時間裡做正確的事的目標,並對框架的積極響應能力做了很大的提升。一個有很多組件和複雜結構的SPA渲染起來有可能會變得很慢,Fiber 能夠優先處理重要元素的渲染(加入了優先順序制度),並且顯著提高了渲染的速度,通過這種任務調度的能力使應用程序變得更好。</P><P>而 Fiber 最為激動人心的地方就是實現了<b>非同步渲染</b>。一種通過對瀏覽器周期性地執行調度的方法來協調渲染工作的策略,在這一新特性的支持下,React 的渲染能夠避免阻塞主線程,應用也可以更快的響應交互(然而在v16中,默認並沒有開啟非同步渲染 …)。</P><P>要理解 Fiber 的工作過程,首先來看一下 React 實現渲染的過程。在 React 中主要有兩個部分去執行他們對應的過程:Reconciler 和 Renderer,可以理解為前者任務是渲染之前的 diff 演算法等一系列過程,而後者的任務就是 React 對於不用平台(DOM,Canvas,Native…)的渲染執行過程。這兩部分的分離意味著不同的渲染引擎能共享 React核心提供的協調演算法,而使用它們各自的渲染器,這樣可以更好做到演算法優化的最大化。</P></p> <figure><P style="text-align:center;"><img src="//i1.wp.com/pic4.zhimg.com/50/v2-bff5ebc77ea50ac8f6079716541835ce_hd.jpg" dw="581" dh="294" w="581" data-original="https://pic4.zhimg.com/v2-bff5ebc77ea50ac8f6079716541835ce_r.jpg"></P><figcaption>Reconciler 和 Renderer。圖為React Conf 2017上,Lin Clark做的A Cartoon Intro to Fiber分享的ppt視頻。</figcaption></figure> <p><P>每當組件觸發更新或者渲染、卸載時,Reconciler 會對組件的狀態與新狀態進行 diff 演算法,比如執行 setState() 時,Reconciler在一系列相關邏輯執行完畢之後,將會在組建中觸發 render() 去渲染或者更新 React 節點的元素。在老版本中,Reconciler 的整體調度方式叫做 Stack Reconciler ,他會獨立維護一個所有組件組成的「實例」樹,它包含所有用戶和框架所定義的組件,而用戶無法訪問到這個實例樹並且不會對外暴露。而在v16中,這個 stack 換成了這次的主角 Fiber,叫做 Fiber Reconciler。新的 Fiber Reconciler 演算法意在解決原來有一些積壓已久的問題:</P></p> <ul> <li>可以中斷且分離任務為多個「幀」</li> <li>可以在執行中制定優先權,重新制定和重新執行任務</li> <li>可以在父節點與子節點之間來回切換以支持react的渲染</li> <li>可以讓 `render()` 返回多元素</li> <li>最好支持 `error boundaries`(下文會介紹到)</li> </ul> <p><P>在原來的調度演算法實現中,會遞歸調用組件實例樹,每一個更新都是立即執行,由於瀏覽器JS單線程的原因,整個更新渲染過程會同步進行,這樣其實也是比較合理的方案,因為渲染本來就不需要等待IO,只是單純去執行瀏覽器計算即可,但是當組件的複雜程度越來越大之後,瀏覽器的計算不足以應付繁重的任務,於是就開始排隊,而且這個排隊過程中會佔住瀏覽器主線程,使用戶的操作完全被阻塞,沒有響應,造成很差的用戶體驗。這個問題在複雜組件的場景會很常見,那麼首先要明白問題的關鍵,在與用戶的交互中,界面是沒有必要每一個更新的都立即執行的,這樣的做法中無用的開銷會很高;另外,不同類型的組件之間有相對的重要程度,有的需要馬上與用戶交互,有的則不會被重視到,那麼就要引入優先順序的概念來安排渲染順序。</P></p> <figure><P style="text-align:center;"><img src="//i1.wp.com/pic1.zhimg.com/50/v2-2d6f76460c4b1a47889899f09977f130_hd.jpg" dw="600" dh="195" w="600" data-original="https://pic1.zhimg.com/v2-2d6f76460c4b1a47889899f09977f130_r.jpg"></P></p> <p><center> <script src="/336-5.js"></script></center></p><figcaption>圖為「一調到底」的調用棧執行方式,主線程一直被佔用,來自Lin Clark做的A Cartoon Intro to Fiber分享的ppt視頻</figcaption></figure> <p><P>首先,我們都知道 React 組件實質是一個函數,只要是一樣的輸入,就會得到同樣的輸出組件。而瀏覽器跟蹤程序執行的方式是調用棧,不斷的入棧與出棧來實現組件的渲染過程。但是問題就是,如果我們只依賴調用棧去執行程序,我們就只能老老實實等待它變空,無法提升什麼效率了。那麼我們要做的工作就是去自定義調用棧,或者操作調用棧。而這就是 Fiber 的設計目的,實現 React 組件的堆棧操作,按照你想要的方式和時機去執行,一個虛擬的堆棧結構。</P><P>從 Fiber 的設計思想來說,Fiber 是一個基於組件的執行單元,多個 Fiber 可能對應於同一個組件,你可以對它暫停,繼續,修改優先順序等操作。在對 Fiber 的調度過程中,為了實現及時的 「打斷」 和 「繼續」,我們把調度過程分為兩個階段,第一階段叫做 Render /Reconcilication Phase 過程,用於建立Fiber樹,任務樹,並且得到需要改變的節點表,但是此時並不會開始執行改變,第二階段叫做 Commit Phase 過程,用於執行DOM上的上述改變。在第一階段中,執行過程是可以被隨時打斷,所以我們可以要求主線程在正確的時機放下手中的任務去執行更優先的任務,但是第二階段的渲染過程不可以被打斷,一旦啟動就會執行到結束。</P><P>新的結構中,更新執行的過程被被切分成了很多「幀」,每一幀的時間很短,在每一幀執行一小段任務,在每一小段任務執行完畢之後 Fiber 就會檢查調度任務的模塊,看是否有別的優先順序更高的任務需要執行,這樣唯一的主線程就不會被長時間佔用,其他任務就有機會被執行。維護每一幀的執行與切換,就是 React Fiber 演算法。</P></p> <figure><P style="text-align:center;"><img src="//i1.wp.com/pic4.zhimg.com/50/v2-2944689de58540873e661adbc35689d2_hd.jpg" dw="600" dh="265" w="600" data-original="https://pic4.zhimg.com/v2-2944689de58540873e661adbc35689d2_r.jpg"></P><figcaption>任務分成多幀之後,中斷任使得主線程可以得到處理另外的任務的機會,圖來自Lin Clark做的A Cartoon Intro to Fiber分享的ppt視頻</figcaption></figure> <p><P>在每一幀的執行過程中,React 會去詢問主線程是否有餘下時間繼續接下來的任務,如果有時間那麼React就會開始執行任務,但是這樣也就會出現一個問題,就是時間不夠用的情況,任務沒有完成,時間就到了,而此時有優先順序更高的任務進來,把之前的任務打斷,這時候優先順序更高的任務會被主線程優先執行,而低優先順序任務則會作廢,在下一次有機會時再次被執行,這個就是第一階段 Render /reconcilication Phase 過程中被打斷的情況。而整個一幀一幀不斷切換、繼續的執行過程,就叫做 work loop,這讓 React 可以在它與主線程的來回切換之間做一些計算來提升交互。</P><P>關於 Fiber 具體實現過程,我會專門寫一篇關於 React Fiber 演算法的源碼分析(大概可能也許吧)</P></p> <h2><b>靈活的 render 返回</b></h2> <p><P>在以往版本的 `render()` 函數返回中,只能以完整的單個DOM返回,例如 `&...&</div&>` 如果只是想返回個簡單的字元或者數組就得套上一層,這樣套下來的結果就是整體結構跟俄羅斯套娃似的,一推無用的DOM結構,只是為了返回語法而存在,十分累贅且無效,更不用說性能消耗和可能潛在造成的css問題。</P><P>那麼在新版本中,render不僅可以返回數組,還可以返回字元串或數字等。例如:</P><P><code class="language-text">render() {<br /> // No need to wrap list items in an extra element!<br /> return [<br /> // Don"t forget the keys <img src="https://www.getit01.com/wp-content/themes/Qu/images/smilies/icon_smile.gif" alt=":)" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br /> &</p> <li key="A">First item&</li&>,<br /> &</p> <li key="B">Second item&</li&>,<br /> &</p> <li key="C">Third item&</li&> ];<br /> }</p> <p>render() {<br /> return "Look ma, no spans!";<br /> }</p> <p></code></pre> <p><P>具體來說,在render函數執行時,可以有以下返回:</P></p> <ul> <li>常規React節點:正常渲染JSX,原生DOM組件或者自定義高階組件</li> <li>字元串或者數字:將會被當作文本節點加入DOM中</li> <li>Portal組件:這是一個react新加入的特性,會在下文詳細介紹</li> <li>null:可以渲染null了</li> <li>布爾值:如果為布爾值則返回null,於是render可以這樣寫:`return flag &`</li> <li>數組:多個DOM以數組形式返回,如上例代碼段。</li> </ul> <p><P>在曾經的版本中如果沒有返回一個符合規範的JSX,就會報錯:「<i>A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object</i>」(或者別的啥)。在原有的演算法結構里並不支持多元素的返回結果,所以在老版本的`react-dom.js` 的 `ReactCompositeComponent.mountComponent`函數中對不合規定的返回結果做了報錯處理。</P></p> <p><center> <script src="/336-5.js"></script></center></p> <p><P>在經歷了多個版本的「聲討」,終於不用在一層套一層的DOM了… 其實在幾年前關於多子元素返回的爭論就有很多,這樣做也的確能讓結構和語法更優美一些,fb的工程師也參與了很多的社區討論,但是因為結構複雜的原因,在沒有改變原有結構的情況下一直沒有合適的解決方案。在「多方」的「討論」之下,fb也終於決定開始著手支持這一特性</P></p> <figure><P style="text-align:center;"><img src="//i1.wp.com/pic4.zhimg.com/50/v2-1c736eff021410839dfe4af73a30b87b_hd.jpg" data-caption="" dw="600" dh="557" w="600" data-original="https://pic4.zhimg.com/v2-1c736eff021410839dfe4af73a30b87b_r.jpg"></P></figure> <p><P>圖為 github 上對新版本要做的feature的討論結果</P></p> <h2><b>更強的錯誤處理</b></h2> <p><P>在以往的 React App 開發中,一遇到 React 的錯誤發生,將會是一片腥紅的報錯屏,頁面停止工作,只能刷新恢復。現在不再會這樣了。新版本使用了更為彈性的錯誤處理策略,如果在組件中存在渲染錯誤,那麼整個組件樹將不會從根節點被渲染出來,這樣避免了錯誤數據的出現。</P><P>除此之外,還引入了一種新的概念「Error Boundaries」,這是一種 React 組件用於捕捉子組件樹中的 JavaScript 的錯誤,並且記錄下來,還可以回調錯誤處理的邏輯,這樣就不會任由React被報錯停止工作了。Error Boundaries 在渲染時開始捕捉錯誤,以 `componentDidCatch()` 的方法調用 ,他將會以生命周期的情形加入組件中,完成 catch JS的報錯,換句話說就是 React 版的 try catch。</P><P><code class="language-text">componentDidCatch(error, info) {<br /> // 處理錯誤<br /> this.setState({ hasError: true });<br /> // 記錄錯誤<br /> logErrorToMyService(error, info);<br /> }</p> <p></code></pre> <p><P>目前,只有Class組件可以使用`componentDidCatch()`,另外他只會 catch 它的樹結構子節點的錯誤,這並不會包括它自己的。並且錯誤會傳遞到上層,這一點與 JavaScript 的 catch{} 類似。</P><P>新的錯誤捕捉機制可以解決一系列問題,那些沒有被捕捉到的錯誤將會使得子組件被停止渲染,這個在實際環境中會很有用,我們都不希望一個有錯誤的頁面直接呈現到用戶面前,這樣可能會暴露很多問題,引起麻煩。那麼使用 error boundaries ,即使出現錯誤,錯誤的組件停止渲染,其他功能依然可以正常交互。這樣使得應用的健全性提升了一個檔次,當然,建立完善的錯誤控制體系也是一個成熟應用應該具備的。</P><P>其實在React v15中,fb團隊寫了一個不太完善的組件可以實現部分 `componentDidCatch()` 的工作,叫做 `unstable_handleError`,(所以叫做 unstable),在新版本中,這個保留的方法將被放棄,轉而使用更為高大上的 `componentDidCatch()` 。</P></p> <h2><b>Portals 組件</b></h2> <p><P>Portals 的作用就跟他的名字一樣形象,(好像也沒找到別的譯名),這是一種新的 ReactDom 組件方法,<b>能夠渲染位於當前結構位置之外的組件</b>,這個需求也許這樣聽著不太明白,但是舉個 就很清晰了,比如我們常用的提示框,會突出顯示在屏幕中間那種,一般出現的時候會居中並且優先於別的操作頁面,有的還會把背景加上陰影啥的,像這樣的組件在常規的組件邏輯里,會直接寫在當前的頁面中,類似於:</P><P><code class="language-text">{ loading ? &<Loading /&> : "" }</p> <p></code></pre> <p><P>再由當前的組件決定是否顯示,先不說組件復用的問題,在這裡 CSS 樣式就有可能出現不穩定的情況,因為每個用到這種提示框的外部組件都可能不一樣,再一個個調 position 啥的實在是太麻煩了,而且從結構上來看,這種交叉的結構實在是談不上優美。那麼,Portals 組件就出現了,其實他的作用就如其名,在組件中放一個「傳送門」,讓復用組件渲染的結果出現在你想要的地方。代碼例子如下:</P></p> <p><center> <script src="/336-5.js"></script></center></p> <p><P><code class="language-text">render() {<br /> return ReactDOM.createPortal(<br /> this.props.children,<br /> domNode,<br /> );<br /> }<br /> </code></pre> <p><P>通過`ReactDOM.createPortal()`,你就可以把children插入到你希望出現的DOM節點處。</P></p> <h2><b>Portals的事件冒泡</b></h2> <p><P>Portals 組件與正常的 React Elements 並沒有什麼差異,包括事件的處理,所以無論邏輯上還是寫法上 Portals 都是與當前組件是「一體」的。在事件冒泡傳遞中,Portal 內的事件依然會傳遞到外層,優美的實現了功能和邏輯。</P><P><code class="language-text">&<br /> &<Modal&> // Modal 是Portals組件<br /> &<Child /&><br /> &</Modal&><br /> // 點機 &<Child /&> 會觸發 this.handleClick()<br /> &</div&> <p></code></pre> <p><P>儘管 Modal 和當前的組件並不在一個 DOMtree 上,但是依然會觸發 `handleClick` 點擊事件。</P></p> <h2><b>更好的服務端渲染</b></h2> <p><P>在新版本中,團隊完全重寫了服務端渲染,他變得更快並且支持位元組流(Stream)傳輸。有關 Node 的 Stream 的請看Node.js v8.8.1 Documentation。Stream 是一種很好的建立於應用和服務之間傳輸的方式,使用它我們可以不用一次性讀或寫一個大文件,轉而以位元組流的形式傳輸文件,這樣對於文件的傳輸時間和空間效率都有重大的作用。(另外,fb團隊將原先大量的`process.env.NODE_ENV`檢查減少到了一個,這是因為`process.env`並不是一個常規的 JavaScript 對象,讀它很昂貴)。因此,開發環境中的性能將與生產環境相差無幾,但錯誤處理和Portals組件(沒有可綁的DOM節點…)暫時還沒有加入 SSR。總體來說速度提升了一個台階,比 v15 的時候快了近3倍(官方數據)。</P><P>我們先來看看在React v15中的SSR是怎麼做的。首先在服務端的框架里(Express,Koa)使用原來的方法</P><P><code class="language-text">ReactDOMServer.renderToString(element)<br /> // 傳入React element,返回為HTML String<br /> // 可以用於請求的快速返回用於載入頁面或者有利於PWA的SEO。服務端或者瀏覽器都可以使用</p> <p></code></pre> <p><P>將React組件轉為String,然後寫入response中。</P><P><code class="language-text">// using Express<br /> import { renderToString } from "react-dom/server"<br /> import App from "./App"<br /> app.get("/", (req, res) =&> {<br /> res.write("&&<html&>&<body&>");<br /> res.write("&");<br /> res.write(renderToString(&<App /&>));<br /> res.write("&</div&>&</body&>&</html&>");<br /> res.end();<br /> });</p> <p></code></pre> <p><P>然後在瀏覽器端,再使用`render`將HTML String渲染出來就完成了SSR。在v16中的服務端渲染過程中,以往的`render`方法換成了`hydrate`:</P><P><code class="language-text">ReactDOM.hydrate(element, container,[callback])</p> <p></code></pre> <p><P>`hydrate` 用於「注入」服務端渲染的HTML內容。因為這個版本是嚴格向後兼容的,所以原有的`render`方式也可以使用,這個保留會在v17去除。同時,在新版本中,對 HTML 的轉換也變得更高效,老版本的 HTML 會對每個元素注入`data-reactid`,對文本注入`react-text`,他們只是單純無聊的遞增id而已。而新版**能夠**去除這些冗餘的東西。大大減少了文件的體積。</P></p> <h2><b>支持 Stream</b></h2> <p><P>這是一個十分有用的新特性,SSR 支持 Node Stream 傳輸。Stream 可以減少內容的第一位元組(TTFB)時間,同時在內容還沒完全生成的時候就開始傳輸,瀏覽器會更快更早的開始渲染頁面,用戶體驗也會更好。Stream 的另一個好處就是在阻塞時,能夠更好的處理數據包堵塞的時候可能會發生的丟包,當阻塞時,渲染機制會得到信號並且觸發 Node Stream 的方法將其轉換到到 paused 模式,直到阻塞被清除,渲染機制又會接到信號再轉換為flowing 模式繼續傳輸。這意味著服務端可以使用更少的內存,保持更積極的響應 I/O。Stream 的渲染方法如下:</P><P><code class="language-text">ReactDOMNodeStream.renderToNodeStream(element)<br /> // 返回 Readable stream 格式的 HTML String<br /> ReactDOMNodeStream.renderToStaticNodeStream(element)<br /> // 與 ReactDOMNodeStream.renderToNodeStream 類似,但是不會注入多餘的DOM標籤,比如`data-reactid`</p> <p></code></pre> <p><P>這兩種方法都只能在服務端使用,默認情況下會返回 utf-8 格式的位元組流。</P></p> <p><center> <script src="/336-5.js"></script></center></p> <p><P>當使用 `renderTo(Static)NodeStream` 去返回一個可讀位元組流時,Stream 會處於 Paused 模式,渲染也會暫停,只有當可讀位元組流對象觸發 `read` 或者 `pipe` 方法時才會開始「flowing」位元組流和渲染,比如:</P><P><code class="language-text">// using Express<br /> const stream = renderToNodeStream(&<MyPage/&>);<br /> // paused 模式,並且停止渲染<br /> stream.pipe(res, { end: false });<br /> // flowing 模式,開始渲染。<br /> // 特別的是,參數`{ end: false }`表示是否在輸入結束時停止,應當為false,否則渲染結束而響應未結束時會發生丟包<br /> stream.on("end", () =&> {<br /> res.end();<br /> // 完成,stream 結束<br /> });<br /> });</p> <p></code></pre> <p><P>然而新版本的 Stream SSR 有一個問題需要注意,不要使用 JSX 嵌入 JavaScript 的寫法,例如:</P><P><code class="language-text">res.write(renderToStaticMarkup(<br /> &<br /> { renderToString(&<App /&>) }<br /> &</div&> );</p> <p></code></pre> <p><P>這樣在老版本 SSR 中沒有什麼錯誤,但是在 Stream 中會報錯,因為`renderToString()`的返回是可讀流格式,並不能當做元素嵌入到JSX之中。</P></p> <h2><b>支持自定義的DOM標籤</b></h2> <p><P>以往React會忽略不認識的DOM標籤,在很多時候就得寫所謂的React標籤白名單去支持邏輯或者第三方庫,實在是辣雞,沒有什麼比一個名如其實的標籤更讓人親切了,不過起名字不是程序員最頭痛的事嗎?使用例子:</P><P><code class="language-text">&</p> <p></code></pre> <p><P>另外,如果遇到使用了DOM原有標準的標籤的情況,React優先原有的標準標籤,也就是說,如果你要是自定義了這類標籤,那會無效的。</P><P><code class="language-text">// 沒問題<br /> &</p> <p>// Warning: Invalid DOM property `tabindex`. Did you mean `tabIndex`?<br /> &</p> <p></code></pre> <p><P>除此之外,`data-`,`aria-` 依然可以繼續使用,這一點沒有做改變。但是,React 也一直明確表示,數據不應該放在標籤里來存放,畢竟有 state 或者 store 去管理數據,而在 v16中,會把傳遞的數據轉為 String 再傳過去,所以這樣也可能會導致潛在的問題。</P></p> <h2><b>setState() 變聰明了</b></h2> <p><P>原來執行 `setState()` 無論如何都會重新渲染頁面,而現在如果參數為 null,將不再觸發渲染,這樣就可以直接使用 setState 來決定是否重新渲染頁面,這個小小的改變會對開發十分有用。</P></p> <h2><b>更加輕薄</b></h2> <p><P>一個程序的優秀性在於其重構的次數 ,重寫了底層演算法之後,大小縮小了這麼多:(官方數據)</P></p> <ul> <li>react is 5.3 kb (2.2 kb gzipped), 原來 20.7 kb (6.9 kb gzipped).</li> <li>react-dom is 103.7 kb (32.6 kb gzipped), 原來 141 kb (42.9 kb gzipped).</li> <li>react + react-dom is 109 kb (34.8 kb gzipped), 原來 161.7 kb (49.8 kb gzipped).</li> </ul> <p><P>整體比之前的版本小了32%(壓縮後小了30%)。體積的改變也部分歸功於打包方式的改變,新版本使用了`Rollup`去打包成更為扁平的 bundles,結果也更讓人欣喜。[Rollup.js](rollup.js)也是一種模塊打包工具,類似於webpack,那麼為啥要換成 Rollup 來打包呢,我們都知道 webpack2.0 加入了`tree-shaking`功能,其實這個功能的提出最早出現在 Rollup 中,Rollup 自稱下一代的 JavaScript 模塊化工具,使用 ES6 模塊編寫,支持很多新特性,可以編譯成 UMD 或者 CommonJS 等格式。具體的特性可以參考官網的介紹,總結下來,應用開發的打包推薦使用Webpack,更全面也更強大,而到框架級別推薦使用Rollup,效率會更好。</P></p> <h2><b>改用 MIT licensed</b></h2> <p><P>你問我支不支持,我當然是支持啦。</P><P>只是心疼那些遷移工程做了一半的同學們 …</P></P></p> <hr />官方文檔寫的更易理解了</P></p> <hr />在保持最大兼容性的前提下,進行內部重寫很不容易。這次16版本最大的變化是使用的Fiber引擎,其能力還沒有完全啟用(例如非同步渲染默認關閉),官方是希望大家先平滑過渡到16版本,後續更新小版本再逐步挖掘16的潛力,開放更多特性。遷移到16基本沒有成本(如果避免了15中的警告),未來的版本更新非常值得期待!</P></p> <hr />很喜歡,只是這次出來後,其他的框架就得研究很一陣子才能抄了,但是我也很希望大家都去抄,然後抄的更牛逼,這樣我們使用者就可以隨意挑選,換著法兒寫業務。</P></p> <hr />啥時候能支持 Custom Fiber Renderer……等了快一年了</P></p> <hr /><P>剛好看完了http://egghead.io 上的一套介紹React16新特性的視頻,在使用上大致添加了以下非常方便的方法:</P></p> <ol> <li>添加了新的生命周期函數 <i>ComponentDidCatch(error,info){},</i>使得程序出錯時能更優雅的處理;</li> <li>支持直接通過[]返回多個同層的節點,支持直接返回字元串(字元串會自動被添加在div標籤內)</li> <li>添加新方法ReactDom.createPortal(),允許添加節點到當前DOM樹外,(視頻中有很好的示例,可查看)</li> <li>支持自定義屬性,自定義屬性在React15及以下版本中會被自動忽略</li> <li>調用this.setState()時,傳入null,可以不再觸發DOM的更新</li> </ol> <blockquote><p>ps:實在沒找到知乎的編輯器如何插入行內代碼。。</p></blockquote> <p></P></p> <hr /><P>目前react-addons-perf用不了了</P></P></p> <hr />本來當天就想升react16的,後來發現有些第三方庫直接用了react16棄用的React.PropTypes,沒有用獨立的prop-types庫,導致錯誤,只好回退了。react/lib也沒有了,以前用的裡面的函數需要找替代。另外發現react-dnd已經是react16了,該庫作者應該是個新技術的狂熱愛好者。</P>總的來說,第三方庫使用PropTypes的方法是現在升級最大的問題。</P></p> <hr /><P>終於不用在包一層了,真是解脫啊</P></P></p> <hr />fiber比較強大,期待了有一陣子了。</P></p> <hr /><span style="color:red"><i class="fa fa-paper-plane"></i></span> 推薦閱讀:</div> <p>※<a target=_blank href=/p20171231565446071/>阿里還會使用react嗎?</a><br />※<a target=_blank href=/p20171230467520727/>沒有安卓和ios開發經驗的前端適合學rn嗎?</a><br />※<a target=_blank href=/p20171225232239901/>Flux todoMVC 為什麼要費那麼多力氣實現一個功能!!!!,這樣寫的好處是神馬?</a><br />※<a target=_blank href=/p20171222559069953/>如何評價 Airbnb 發布的 React Sketch.app 工具?</a><br />※<a target=_blank href=/p20171116944/>如何評價 React Native?</a></p> <p>TAG:<a target=_blank href=/tag/前端開發/>前端開發</a> | <a target=_blank href=/tag/React/>React</a> | </p> <!-- AddThis Advanced Settings above via filter on the_content --><!-- AddThis Advanced Settings below via filter on the_content --><!-- AddThis Advanced Settings generic via filter on the_content --><!-- AddThis Share Buttons above via filter on the_content --><!-- AddThis Share Buttons below via filter on the_content --><div class="at-below-post addthis_tool" data-url="https://www.getit01.com/p20180101165920482/"></div><!-- AddThis Share Buttons generic via filter on the_content --></div> <script src="/ce.js"></script> <div class="clear"></div> </div> </div> <div class="clear"></div> </div> </div> <div class="clear"></div> <div id="footer"> <div class="copyright"> <p> 一點新知 <a href="https://www.getit01.com/"><strong> GetIt01 </strong></a> <div style="display:none"><script src="https://s13.cnzz.com/z_stat.php?id=1270562218&web_id=1270562218" language="JavaScript"></script></div> <br /> </p> </div> </div> </div> <!--gototop--> <div id="tbox"> <a target="_blank" id="fb" href="https://www.facebook.com/sharer.php?u=https://www.getit01.com/p20180101165920482/"></a> </div> <script data-cfasync="false" type="text/javascript">if (window.addthis_product === undefined) { window.addthis_product = "wpp"; } if (window.wp_product_version === undefined) { window.wp_product_version = "wpp-6.1.1"; } if (window.wp_blog_version === undefined) { window.wp_blog_version = "4.8.22"; } if (window.addthis_share === undefined) { window.addthis_share = {}; } if (window.addthis_config === undefined) { window.addthis_config = {"data_track_clickback":true,"ignore_server_config":true,"ui_atversion":"300"}; } if (window.addthis_layers === undefined) { window.addthis_layers = {}; } if (window.addthis_layers_tools === undefined) { window.addthis_layers_tools = [{"share":{"counts":"each","numPreferredServices":5,"mobile":false,"position":"left","theme":"transparent"},"sharedock":{"counts":"each","numPreferredServices":5,"mobileButtonSize":"large","position":"bottom","theme":"transparent"}},{"sharetoolbox":{"numPreferredServices":5,"counts":"each","size":"32px","style":"fixed","shareCountThreshold":0,"elements":".addthis_inline_share_toolbox_vh9e,.at-above-post"}}]; } else { window.addthis_layers_tools.push({"share":{"counts":"each","numPreferredServices":5,"mobile":false,"position":"left","theme":"transparent"},"sharedock":{"counts":"each","numPreferredServices":5,"mobileButtonSize":"large","position":"bottom","theme":"transparent"}}); window.addthis_layers_tools.push({"sharetoolbox":{"numPreferredServices":5,"counts":"each","size":"32px","style":"fixed","shareCountThreshold":0,"elements":".addthis_inline_share_toolbox_vh9e,.at-above-post"}}); } if (window.addthis_plugin_info === undefined) { window.addthis_plugin_info = {"info_status":"enabled","cms_name":"WordPress","plugin_name":"Share Buttons by AddThis","plugin_version":"6.1.1","plugin_mode":"WordPress","anonymous_profile_id":"wp-465109ee2f0e70a26b602727e258dac0","page_info":{"template":"posts","post_type":""},"sharing_enabled_on_post_via_metabox":false}; } (function() { var first_load_interval_id = setInterval(function () { if (typeof window.addthis !== 'undefined') { window.clearInterval(first_load_interval_id); if (typeof window.addthis_layers !== 'undefined' && Object.getOwnPropertyNames(window.addthis_layers).length > 0) { window.addthis.layers(window.addthis_layers); } if (Array.isArray(window.addthis_layers_tools)) { for (i = 0; i < window.addthis_layers_tools.length; i++) { window.addthis.layers(window.addthis_layers_tools[i]); } } } },1000) }()); </script><script type='text/javascript' src='https://s7.addthis.com/js/300/addthis_widget.js?ver=4.8.22#pubid=wp-465109ee2f0e70a26b602727e258dac0'></script> <script type='text/javascript' src='https://www.getit01.com/wp-content/themes/Qu/js/loostrive.js?ver=1.0'></script> <script type='text/javascript' src='https://www.getit01.com/wp-includes/js/wp-embed.min.js?ver=4.8.22'></script> </body></html> <!-- Dynamic page generated in 0.136 seconds. --> <!-- Cached page generated by WP-Super-Cache on 2024-12-27 10:28:02 --> <!-- super cache -->