大約一年前,React 團隊發佈了 React 16.0。時至今日,已更新到 16.5 。這其中有很多激動人心的特性(如 Fiber 架構的引入、新的周期函數、全新 Context API、Fragment、Error Boundary、Portal 等)都值得開發者跟進學習。本文就以 React 更新日誌 爲引,選取幾個重要且用於工做的更新,和你們一塊兒學習。全部示例代碼在 react-upgrade-examples, 配合文章一塊兒食用更佳~ 😆
隨着 React 16.0 發佈, React 採用了新的內核架構 Fiber,在新的架構中它將更新分爲兩個階段:Render Parse 和 Commit Parse, 也由此引入了 getDerivedStateFromProps
、 getSnapshotBeforeUpdate
及 componentDidCatch
等三個生命週期函數。同時,也將 componentWillMount
、componentWillReceiveProps
和 componentWillUpdate
標記爲不安全的方法。javascript
new lifecyclecss
新增html
static getDerivedStateFromProps(nextProps, prevState)
getSnapshotBeforeUpdate(prevProps, prevState)
componentDidCatch(error, info)
標記爲不安全java
componentWillMount(nextProps, nextState)
componentWillReceiveProps(nextProps)
componentWillUpdate(nextProps, nextState)
static getDerivedStateFromProps(nextProps, prevState)
根據 getDerivedStateFromProps(nextProps, prevState)
的函數簽名可知: 其做用是根據傳遞的 props
來更新 state
。它的一大特色是 無反作用 : 因爲處在 Render Phase 階段,因此在每次的更新都要觸發, 故在設計 API 時採用了靜態方法,其好處是單純 —— 沒法訪問實例、沒法經過 ref
訪問到 DOM 對象等,保證了單純且高效。值得注意的是,其仍能夠經過 props
的操做來產生反作用,這時應該將操做 props
的方法移到 componentDidUpdate
中,減小觸發次數。react
例:git
state = { isLogin: false } static getDerivedStateFromProps(nextProps, prevState) { if(nextProps.isLogin !== prevState.isLogin){ return { isLogin: nextProps.isLogin } } return null } componentDidUpdate(prevProps, prevState){ if(!prevState.isLogin && prevProps.isLogin) this.handleClose() }
但在使用時要很是當心,由於它不像 componentWillReceiveProps
同樣,只在父組件從新渲染時才觸發,自己調用 setState
也會觸發。官方提供了 3 條 checklist, 這裏搬運一下:github
props
的同時,有反作用的產生(如異步請求數據,動畫效果),這時應該使用 componentDidUpdate
props
計算屬性,應該考慮將結果 memoization 化,參見 memoization props
變化來重置某些狀態,應該考慮使用受控組件配合 componentDidUpdate
周期函數,getDerivedStateFromProps
是爲了替代 componentWillReceiveProps
而出現的。它將本來 componentWillReceiveProps
功能進行劃分 —— 更新 state
和 操做/調用 props
,很大程度避免了職責不清而致使過多的渲染, 從而影響應該性能。web
getSnapshotBeforeUpdate(prevProps, prevState)
根據 getSnapshotBeforeUpdate(prevProps, prevState)
的函數簽名可知,其在組件更新以前獲取一個 snapshot —— 能夠將計算得的值或從 DOM 獲得的信息傳遞到 componentDidUpdate(prevProps, prevState, snapshot)
周期函數的第三個參數,經常用於 scroll 位置的定位。摘自官方的示例:npm
class ScrollingList extends React.Component { constructor(props) { super(props) // 取得dom 節點 this.listRef = React.createRef() } getSnapshotBeforeUpdate(prevProps, prevState) { // 根據新添加的元素來計算獲得所須要滾動的位置 if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current return list.scrollHeight - list.scrollTop } return null } componentDidUpdate(prevProps, prevState, snapshot) { // 根據 snapshot 計算獲得偏移量,獲得最終滾動位置 if (snapshot !== null) { const list = this.listRef.current list.scrollTop = list.scrollHeight - snapshot } } render() { return <div ref={this.listRef}>{/* ...contents... */}</div> } }
componentDidCatch(error, info)
在 16.0 之前,錯誤捕獲使用 unstable_handleError
或者採用第三方庫如 react-error-overlay 來捕獲,前者捕獲的信息十分有限,後者爲非官方支持。而在 16.0 中,增長了 componentDidCatch
周期函數來讓開發者能夠自主處理錯誤信息,諸如展現,上報錯誤等,用戶能夠建立本身的Error Boundary
來捕獲錯誤。例:api
··· componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service logErrorToMyService(error, info); } ···
此外,用戶還能夠採用第三方錯誤追蹤服務,如 Sentry、Bugsnag 等,保證了錯誤處理效率的同時也極大降級了中小型項目錯誤追蹤的成本。
圖片bugsnag
componentWillMount
、componentWillReceiveProps
、componentWillUpdate
componentWillMount
componentWillMount
可被開發者用於獲取首屏數據或事務訂閱。
開發者爲了快速獲得數據,將首屏請求放在 componentWillMount
中。實際上在執行 componentWillMount
時第一次渲染已開始。把首屏請求放在componentWillMount
的與否都不能解決首屏渲染無異步數據的問題。而官方的建議是將首屏放在 constructor
或 componentDidMount
中。
此外事件訂閱也被常在 componentWillMount
用到,並在 componentWillUnmount
中取消掉相應的事件訂閱。但事實上 React 並不可以保證在 componentWillMount
被調用後,同一組件的 componentWillUnmount
也必定會被調用。另外一方面,在將來 React 開啓異步渲染模式後,在 · 被調用以後,組件的渲染也頗有可能會被其餘的事務所打斷,致使 componentWillUnmount
不會被調用。而 componentDidMount
就不存在這個問題,在 componentDidMount
被調用後,componentWillUnmount
必定會隨後被調用到,並根據具體代碼清除掉組件中存在的事件訂閱。
對此的升級方案是把 componentWillMount
改成 componentDidMount
便可。
componentWillReceiveProps
、componentWillUpdate
componentWillReceiveProps
被標記爲不安全的緣由見前文所述,其主要緣由是操做 props 引發的 re-render。與之相似的 componentWillUpdate
被標記爲不安全也是一樣的緣由。除此以外,對 DOM 的更新操做也可能致使從新渲染。
對於 componentWillReceiveProps
的升級方案是使用 getDerivedStateFromProps
和 componentDidUpdate
來代替。
對於 componentWillUpdate
的升級方案是使用 componentDidUpdate
代替。如涉及大量的計算,可在 getSnapshotBeforeUpdate
完成計算,再在 componentDidUpdate
一次完成更新。
經過框架級別的 API 來約束甚至限制開發者寫出更易維護的 Javascript 代碼,最大限度的避免了反模式的開發方式。
在 React 16.3 以前,Context API 一直被官方置爲不推薦使用(don’t use context),究其緣由是由於老的 Context API 做爲一個實驗性的產品,破壞了 React 的分形結構。同時在使用的過程當中,若是在穿透組件的過程當中,某個組件的 shouldComponentUpdate
返回了 false
, 則 Context API 就不能穿透了。其帶來的不肯定性也就致使被不推薦使用。隨着 React 16.3 的發佈,全新 Context API 成了一等 API,能夠很容易穿透組件而無反作用,官方示例代碼:
// Context lets us pass a value deep into the component tree // without explicitly threading it through every component. // Create a context for the current theme (with "light" as the default). const ThemeContext = React.createContext('light') class App extends React.Component { render() { // Use a Provider to pass the current theme to the tree below. // Any component can read it, no matter how deep it is. // In this example, we're passing "dark" as the current value. return ( <ThemeContext.Provider value="dark"> <Toolbar /> </ThemeContext.Provider> ) } } // A component in the middle doesn't have to // pass the theme down explicitly anymore. function Toolbar(props) { return ( <div> <ThemedButton /> </div> ) } function ThemedButton(props) { // Use a Consumer to read the current theme context. // React will find the closest theme Provider above and use its value. // In this example, the current theme is "dark". return ( <ThemeContext.Consumer>{theme => <Button {...props} theme={theme} />}</ThemeContext.Consumer> ) }
其過程大概以下:
React.createContext
建立 Context 對象<ThemeContext.Provider/>
來提供 Provider<ThemeContext.Consumer/>
以函數調用的方式{theme => <Button {...props} theme={theme} />}
得到 Context 對象的值。在狀態的管理上,全新的 Context API 徹底能夠取代部分 Redux 應用,示例代碼:
const initialState = { theme: 'dark', color: 'blue', } const GlobalStore = React.createContext() class GlobalStoreProvider extends React.Component { render() { return ( <GlobalStore.Provider value={{ ...initialState }}>{this.props.children}</GlobalStore.Provider> ) } } class App extends React.Component { render() { return ( <GlobalStoreProvider> <GlobalStore.Consumer> {context => ( <div> <div>{context.theme}</div> <div>{context.color}</div> </div> )} </GlobalStore.Consumer> </GlobalStoreProvider> ) } }
全新的 Context API 帶來的穿透組件的能力對於須要全局狀態共享的場景十分有用,無需進入額外的依賴就能對狀態進行管理,代碼簡潔明瞭。
React StrictMode 能夠在開發階段發現應用存在的潛在問題,提醒開發者解決相關問題,提供應用的健壯性。其主要能檢測到 4 個問題:
使用起來也很簡單,只要在須要被檢測的組件上包裹一層 React StrictMode
,示例代碼 React-StictMode:
class App extends React.Component { render() { return ( <div> <React.StrictMode> <ComponentA /> </React.StrictMode> </div> ) } }
若出現錯誤,則在控制檯輸出具體錯誤信息:
React Strict Mode
由 ReactDOM 提供的 createPortal
方法,容許將組件渲染到其餘 DOM 節點上。這對大型應用或者獨立於應用自己的渲染頗有幫助。其函數簽名爲ReactDOM.createPortal(child, container)
, child
參數爲任意的可渲染的 React Component,如 element
、sting
、fragment
等,container
則爲要掛載的 DOM 節點.
以一個簡單的 Modal 爲例, 代碼見 Portal Modal :
import React from 'react' import ReactDOM from 'react-dom' const modalRoot = document.querySelector('#modal') export default class Modal extends React.Component { constructor(props) { super(props) this.el = document.createElement('div') } componentDidMount() { modalRoot.appendChild(this.el) } componentWillUnmount() { modalRoot.removeChild(this.el) } handleClose = () => [this.props.onClose && this.props.onClose()] render() { const { visible } = this.props if (!visible) return null return ReactDOM.createPortal( <div> {this.props.children} <span onClick={this.handleClose}>[x]</span> </div>, this.el ) } }
具體過程就是使用了 props
傳遞 children
後, 使用 ReactDOM.createPortal
, 將 container 渲染在其餘 DOM 節點上的過程。
雖然 React 使用 Virtual DOM 來更新視圖,但某些時刻咱們還要操做真正的 DOM ,這時 ref
屬性就派上用場了。
React.createRef
React 16 使用了 React.createRef
取得 Ref 對象,這和以前的方式仍是有不小的差異,例:
// before React 16 ··· componentDidMount() { // the refs object container the myRef const el = this.refs.myRef // you can also using ReactDOM.findDOMNode // const el = ReactDOM.findDOMNode(this.refs.myRef) } render() { return <div ref="myRef" /> } ··· ··· // React 16+ constructor(props) { super(props) this.myRef = React.createRef() } render() { return <div ref={this.myRef} /> } ···
React.forwardRef
另一個新特性是 Ref 的轉發, 它的目的是讓父組件能夠訪問到子組件的 Ref,從而操做子組件的 DOM。React.forwardRef
接收一個函數,函數參數有 props
和 ref
。看一個簡單的例子,代碼見 Refs:
const TextInput = React.forwardRef((props, ref) => ( <input type="text" placeholder="Hello forwardRef" ref={ref} /> )) const inputRef = React.createRef() class App extends Component { constructor(props) { super(props) this.myRef = React.createRef() } handleSubmit = event => { event.preventDefault() alert('input value is:' + inputRef.current.value) } render() { return ( <form onSubmit={this.handleSubmit}> <TextInput ref={inputRef} /> <button type="submit">Submit</button> </form> ) } }
這個例子使用了 React.forwardRef
將 props
和 ref
傳遞給子組件,直接就能夠在父組件直接調用。
在向 DOM 樹批量添加元素時,一個好的實踐是建立一個document.createDocumentFragment
,先將元素批量添加到 DocumentFragment
上,再把 DocumentFragment
添加到 DOM 樹,減小了 DOM 操做次數的同時也不會建立一個新元素。
和 DocumentFragment
相似,React 也存在 Fragment
的概念,用途很相似。在 React 16 以前,Fragment 的建立是經過擴展包 react-addons-create-fragment
建立,而 React 16 中則經過 <React.Fragment></React.Fragment>
直接建立 'Fragment'。例如:
render() { return ( <React.Fragment> <ChildA /> <ChildB /> <ChildC /> </React.Fragment> ) }
如此,咱們不須要單獨包裹一層無用的元素(如使用<div></div>
包裹),減小層級嵌套。
此外,還一種精簡的寫法:
render() { return ( <> <ChildA /> <ChildB /> <ChildC /> </> ) }
ReactDOM
的 render
函數能夠數組形式返回 React Componentrender(){ return [ <ComponentA key='A' />, <ComponentB key='B' />, ] }
react-with-addons.js
, 全部的插件都獨立出來以前經常使用的react-addons-(css-)transition-group
,react-addons-create-fragment
,react-addons-pure-render-mixin
、react-addons-perf
等,除部分被內置,其他所有都獨立爲一個項目,使用時要注意。
窺一斑而見全豹,React 16.0 ~ 16.5 的升級給了開發者一個更爲純粹的開發流程。API 層面的更改、架構的更替、工具類的拆分都在爲構建更易維護的 JavaScript 應用而努力。擁抱變化,順應時勢。
因爲筆者能力有限,文中不免有疏漏,還望讀者不吝賜教。
以上。
Find me on Github