最近看了下React16.3的新文檔,發現官方悄悄地改了不少東西了。其中我最感興趣的天然就是這個全新的Context API了。因此寫了這篇文章來總結分享一下。其餘的變更在這篇文章裏或許會說起。vue
本文你能夠在個人github上面找到,轉載請標註這個地址就好了。node
Context API
是React提供的一種跨節點
數據訪問的方式。衆所周知,React是單向數據流的,Vue
裏面的props
也借鑑了這一思想。react
可是不少時候,這種單向數據流的設定卻變得不是那麼友好。咱們每每須要從更高層的節點獲取一些數據,若是使用傳統的prop
傳遞數據,就須要每一層都手動地向下傳遞。對於層次很高的組件,這種方法十分地煩人,極大地下降了工做效率。git
因而,React使用了Context API
。Context API
存在已久,可是舊的Context API
存在不少問題,而且使用起來也並非特別方便,官方並不建議使用老版本的Context API
。因而不少開發者選擇了Redux
之類的狀態管理工具。程序員
受到Redux
的影響,React
在16.3.0版本中推出了全新的Context API
。github
衆所周知,長期起來JavaScript一直沒有模塊系統。nodejs
使用require
做爲彌補方法。ECMAScript6
以後,引入了全新的import
語法標準。import
語法標準有個尤其重要的不一樣(相比較require
),那就是:import
導入的數據是引用的。這意味着多個文件導入同一個數據,並非導入的拷貝,而是導入的引用。數組
react@16.3的聲明文件(d.ts)貌似沒有更新,意味着若是你如今使用Typescript,那麼可能會報錯。markdown
React如今推薦使用render props
,render props
爲組件渲染的代碼複用以及代碼傳遞提供了新的思路,其實本質上就是經過props
傳遞HOC函數來控制組件的渲染。app
或許你曾經聽過「Context API
是用來替代Redux
」之類的傳聞,然而事實並不是如此。Redux
和Context API
解決的問題並不同,會形成那樣的錯覺多是由於他們的使用方法有點兒同樣。dom
React16.3有幾個新特性,最主要的變化是Context,還有就是廢除了幾個生命週期,好比ComponentWillReceiveProps
(說實話,實際項目中,這個生命週期徹底能夠用ComponentWillUpdate
來替換)
React16.3中的refs再也不推薦直接傳遞一個函數了,而是使用了全新的React.createRef
來替代。固然之前的方法依舊適用,畢竟是爲了兼容。
createContext
用來建立一個Context
,它接受一個參數,這個參數會做爲Context
傳遞的默認值。須要注意的是,若是你傳入的參數是個對象,那麼當你更改Context
的時候,內部會調用Object.is
來比較對象是否相等。這會致使一些性能上的問題。固然,這並不重要,由於大部分狀況下,這點兒性能損失能夠忽略。
咱們看下這個例子,這是一個提供主題(Light/Dark)類型的Context
。
// context.js import * as React from 'react'; // 默認主題是Light export const { Provider, Consumer } = React.createContext("Light"); 複製代碼
接下來咱們只須要在須要的文件裏import
就好了
Provider
是須要使用Context
的全部組件的根組件。它接受一個value
做爲props
,它表示Context
傳遞的值,它會修改你在建立Context
時候設定的默認值。
import { Provider } from './context'; import * as React from 'react'; import { render } from 'react-dom'; import App from './app'; const root = ( <Provider value='Dark'> <App /> </Provider> ); render(root, document.getElementById('root')); 複製代碼
Consumer
表示消費者,它接受一個render props
做爲惟一的children
。其實就是一個函數,這個函數會接收到Context
傳遞的數據做爲參數,而且須要返回一個組件。
// app.jsx import { Consumer } from './context'; import * as React from 'react'; export default class App extends React.Component { render() { return ( <Consumer> { theme => <div>Now, the theme is { theme }</div> } </Consumer> ) } } 複製代碼
Context
爲了確保從新渲染的快速性,React須要保證每一個Consumer
都是獨立的節點。
const ThemeContext = React.createContext('light'); const UserContext = React.createContext(); function Toolbar(props) { return ( <ThemeContext.Consumer> {theme => ( <UserContext.Consumer> {user => ( <ProfilePage user={user} theme={theme} /> )} </UserContext.Consumer> )} </ThemeContext.Consumer> ); } class App extends React.Component { render() { const {signedInUser, theme} = this.props; return ( <ThemeContext.Provider value={theme}> <UserContext.Provider value={signedInUser}> <Toolbar /> </UserContext.Provider> </ThemeContext.Provider> ); } } 複製代碼
當層次更加複雜的時候,會變得很煩人。所以推薦當層次超過兩層以後,建立一個本身的render prop
或許是個不錯的主意。在實際工程中,其實並不建議多層嵌套。更爲適合的時,提供一對Provier
和Consumer
對,傳遞狀態管理工具對應的實例就好了。
在以前的Context API
中,在一些聲明週期中會暴露一個context
的參數,以供開發者更爲方便的訪問。新版API並無這個參數傳遞了,更爲推薦的方式是直接把Context
的值經過props
傳遞給組件。具體來講,就像下面這個官方的例子這樣。
class Button extends React.Component { componentDidMount() { // ThemeContext value is this.props.theme } componentDidUpdate(prevProps, prevState) { // Previous ThemeContext value is prevProps.theme // New ThemeContext value is this.props.theme } render() { const {theme, children} = this.props; return ( <button className={theme ? 'dark' : 'light'}> {children} </button> ); } } export default props => ( <ThemeContext.Consumer> {theme => <Button {...props} theme={theme} />} </ThemeContext.Consumer> ); 複製代碼
不像之前那樣,能夠直接經過this.context
訪問,新版本的Context
只能在render
方法裏面訪問。由於Context
只暴露在Consumer
的render prop
裏面。我的以爲這是這個版本API的一個缺點。因此只有採用上面這種折中的方式,再包裝一個函數組件來封裝到props裏面去。相比較而言,仍是麻煩了一點兒。在組件樹裏面多了一個函數組件,也是一個缺點。
當一個Context
的值多個組件都在使用的時候,你須要手動地每次都寫一次Consumer
和redner prop
。這是很煩的,程序員都是很懶的(至少我是這樣),所以這個時候利用一下React的HOC來封裝一下來簡化這個過程。
const ThemeContext = React.createContext('light'); function ThemedButton(props) { return ( <ThemeContext.Consumer> {theme => <button className={theme} {...props} />} </ThemeContext.Consumer> ); } 複製代碼
接下來,當你須要使用Context
的時候,就不須要在寫什麼Consumer
了
export default props => ( ThemeButton(props) ); 複製代碼
當你封裝完一個Consumer
以後,或許你想要用ref來獲取Consumer
裏面根組件的實例或者對應的DOM。若是直接在Consumer
上使用ref,是得不到想要的結果的。因而在React16.3裏面,使用了一種全新的技術(不肯定是否是16.3才引入的),叫作轉發refs
。不只僅用在Context
裏面,實際上,在任何你想要把ref
傳遞給組件內部的子組件的時候,你均可以使用轉發refs
。
具體來講,你須要使用一個新的API:React.forwardRef((props, ref) => React.ReactElement)
,如下面這個爲例:
class FancyButton extends React.Component {} // Use context to pass the current "theme" to FancyButton. // Use forwardRef to pass refs to FancyButton as well. export default React.forwardRef((props, ref) => ( <ThemeContext.Consumer> { theme => ( <FancyButton {...props} theme={theme} ref={ref} /> ) } </ThemeContext.Consumer> )); 複製代碼
React.forwardRef()
接受一個函數做爲參數。實際上,你能夠將這個函數當作一個函數組件,它的第一個參數和函數組件同樣。不一樣的地方在於,它多了一個ref。這意味着若是你在React.forwardRef
建立的組件上使用ref的話,它並不會直接被組件消化掉,而是向內部進行了轉發,讓須要消化它的組件去消化。
若是你以爲難以理解,其實這種方法徹底能夠用另外一種方法替代。咱們知道,在React中,ref並不會出如今props中,它被特殊對待。可是換個名字不就好了嗎。
須要提一下的是,之前咱們獲取ref是傳遞一個函數(不推薦使用字符串,這是一個歷史遺留的問題,ref會在某些狀況下沒法獲取到正確的值。vuejs
可使用,不要搞混了)。可是這個過程很煩的,咱們只須要把實例或者DOM賦值給對應的變量就好了,每次都寫一下這個同樣模板的代碼,很煩人的好嗎。「千呼萬喚」中,React終於聽到了。如今只須要React.createRef
就能夠簡化這個過程了。
class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } render() { return <div ref={this.myRef} />; } } 複製代碼
使用方法就這麼簡單,沒什麼特別的地方。
回到上面的話題,如今咱們用props來實現轉發refs
的功能。
class Input extends React.Component { reder() { return ( <label>Autofocus Input:</label> <input ref={this.props.forwardRef} type="text" /> ) } } function forwardRef(Component, ref) { return (<Component forwardRef={ref} />); } // 使用forwardRef let input = React.createRef(); forwardRef(Input, input); // 當組件綁定成功以後 input.current.focus(); 複製代碼
React.createRef
返回的值中,current
屬性表示的就是對應的DOM或者組件實例。forwardRef
並無什麼特殊的含義,就是一個簡單的props。這個用法就像是狀態提高
同樣。