React 16 新特性全解(上)
前言
本次系列分上下兩篇文章,上主要介紹從v16.0~ 16.4的新特性,下主要介紹16.5~16.8。下面就開始吧~
本篇文章較長預計需要15min(當然主要是因為demo太多),大家可以搞點瓜子邊啃邊看。最好能留出一隻手自己在codePen上自己調試一下。
目錄
v16.0
- render支持返回數組和字符串演示
- Error Boundary
- createPortal
- 支持自定義DOM 屬性
- Fiber
- 提升SSR渲染速度
- 減小文件體積
v16.1
react-call-return
v16.2
Fragment
v16.3
- 生命週期函數的更新
- createContext
- createRef
- forwardRef
- strict Mode
下面就開始吧~
v16.0
主要特性:
一、render可以返回字符串,數組,數字
React 15 :只可以返回單一組件,也就是說即使你返回的是一個string,也需要用div包住。
functionMyComponent(){return(<div>helloworld<div>);}
React 16:支持返回這五類:React elements,數組和Fragments,Portal,String/numbers,boolean/null。
classExampleextendsReact.Component{render(){return[<divkey="1">firstelement</div>,<divkey="2">secondelement</div>,];}}
注意:無論返回的形式是怎麼樣的,都要保持render是一個純函數。所以要求我們不要改state的狀態,同時不要直接跟瀏覽器直接交互,讓它每次調用生成的結果都是一致的。
二、Error boundary(錯誤邊界)
React 15 :渲染過程中有出錯,直接crash整個頁面,並且錯誤訊息不明確,可讀性差
classBuggyCounterextendsReact.Component{constructor(props){super(props);this.state={counter:0};this.handleClick=this.handleClick.bind(this);}componentWillMount(){thrownewError('I am crash');}handleClick(){this.setState(({counter})=>({counter:counter+1}));}render(){if(this.state.counter===5){// Simulate a JS errorthrownewError('I crashed!');}return<h1onClick={this.handleClick}>{this.state.counter}</h1>;}}functionApp(){return(<div><p><b>ThisisanexampleoferrorboundariesinReact16.<br/><br/>Clickonthenumberstoincreasethecounters.<br/>Thecounterisprogrammedtothrowwhenitreaches5.ThissimulatesaJavaScripterrorinacomponent.</b></p><hr/><p>Thesetwocountersareinsidethesameerrorboundary.Ifonecrashes,theerrorboundarywillreplacebothofthem.</p><BuggyCounter/><hr/></div>);}ReactDOM.render(<App/>,document.getElementById('root'));
比如上面這個App,可以看到子組件BuggyCounter出了點問題,在沒有Error Boundary的時候,整個App都會crash掉,所以顯示白屏。
React 16 :用於捕獲子組件樹的JS異常(即錯誤邊界只可以捕獲組件在樹中比他低的組件錯誤。),記錄錯誤並展示一個回退的UI。
捕獲範圍:
- 渲染期間
- 生命週期內
- 整個組件樹構造函數內
如何使用:
//先定一個組件ErrorBoundaryclassErrorBoundaryextendsReact.Component{constructor(props){super(props);this.state={error:null,errorInfo:null};}componentDidCatch(error,errorInfo){// Catch errors in any components below and re-render with error messagethis.setState({error:error,errorInfo:errorInfo})// You can also log error messages to an error reporting service here}render(){//有錯誤的時候展示回退if(this.state.errorInfo){// Error pathreturn(<div><h2>Somethingwentwrong.</h2><detailsstyle={{whiteSpace:'pre-wrap'}}>{this.state.error&&this.state.error.toString()}<br/>{this.state.errorInfo.componentStack}</details></div>);}//正常的話,直接展示組件returnthis.props.children;}}classBuggyCounterextendsReact.Component{constructor(props){super(props);this.state={counter:0};this.handleClick=this.handleClick.bind(this);}componentWillMount(){thrownewError('I am crash');}handleClick(){this.setState(({counter})=>({counter:counter+1}));}render(){if(this.state.counter===5){// Simulate a JS errorthrownewError('I crashed!');}return<h1onClick={this.handleClick}>{this.state.counter}</h1>;}}functionApp(){return(<div><p><b>ThisisanexampleoferrorboundariesinReact16.<br/><br/>Clickonthenumberstoincreasethecounters.<br/>Thecounterisprogrammedtothrowwhenitreaches5.ThissimulatesaJavaScripterrorinacomponent.</b></p><hr/><ErrorBoundary><p>Thesetwocountersareinsidethesameerrorboundary.Ifonecrashes,theerrorboundarywillreplacebothofthem.</p><BuggyCounter/></ErrorBoundary><hr/></div>);}ReactDOM.render(<App/>,document.getElementById('root'));
可以看到加上Error Boundary之後,除了出錯的組件,其他的地方都不受影響。

而且它很清晰的告訴我們是哪個組件發生了錯誤。
注意事項:
Error Boundary無法捕獲下面的錯誤:
1、事件函數里的錯誤
classMyComponentextendsReact.Component{constructor(props){super(props);this.state={error:null};this.handleClick=this.handleClick.bind(this);}handleClick(){try{// Do something that could throw}catch(error){this.setState({error});}}render(){if(this.state.error){return<h1>Caughtanerror.</h1>}return<divonClick={this.handleClick}>ClickMe</div>}}
上面的例子中,handleClick方法裡面發生的錯誤,Error Boundary是捕獲不到的。因為它不發生在渲染階段,所以採用try/catch來捕獲。
2、異步代碼(例如setTimeout 或requestAnimationFrame 回調函數)
class A extends React . Component { render () { // 此错误无法被捕获,渲染时组件正常返回`<div></div>` setTimeout (() => { throw new Error ( 'error' ) }, 1000 ) return ( < div >< /div> ) } }
3、服務端渲染
因為服務器渲染不支持Error Boundary
4、Error Boundary自身拋出來的錯誤(而不是其子組件)
那這裡還遺留一個問題?錯誤邊界放在哪裡。一般來說,有兩個地方:
1、可以放在頂層,告訴用戶有東西出錯。但是我個人不建議這樣,這感覺失去了錯誤邊界的意義。因為有一個組件出錯了,其他正常的也沒辦法正常顯示了
2、包在子組件外面,保護其他應用不崩潰。
三、react portal
在介紹這個新特性之前,我們先來看看為什麼需要portal。在沒有portal之前,如果我們需要寫一個Dialog組件,我們會這樣寫。
<divclass="app"><div>...</div>{needDialog?<Dialog/>:null}</div>
問題:
1、最終渲染產生的html存在於JSX產生的HTML在一起,這時候dialog 如果需要position:absolute 控制位置的話,需要保證dialog 往上沒有position:relative 的干擾。
2、層級關係不清晰,dialog實際是獨立在app之外的。
所以這時候Portal降臨。
Portal可以幫助我們在JSX中跟普通組件一樣直接使用dialog, 但是又可以讓dialog內容層級不在父組件內,而是顯示在獨立於原來app在外的同層級組件。
如何使用:
HTML:
<divid="app-root"></div>// 这里为我们定义Dialog想要放入的位置
<divid="modal-root"></div>
JS:
// These two containers are siblings in the DOMconstappRoot=document.getElementById('app-root');constmodalRoot=document.getElementById('modal-root');// Let's create a Modal component that is an abstraction around// the portal API.classModalextendsReact.Component{constructor(props){super(props);// Create a div that we'll render the modal into. Because each// Modal component has its own element, we can render multiple// modal components into the modal container.this.el=document.createElement('div');}componentDidMount(){// Append the element into the DOM on mount. We'll render// into the modal container element (see the HTML tab).//這邊會將我們生成的portal element插入到modal-root裡。modalRoot.appendChild(this.el);}componentWillUnmount(){// Remove the element from the DOM when we unmountmodalRoot.removeChild(this.el);}render(){// Use a portal to render the children into the elementreturnReactDOM.createPortal(// Any valid React child: JSX, strings, arrays, etc.this.props.children,// A DOM elementthis.el,);}}// The Modal component is a normal React component, so we can// render it wherever we like without needing to know that it's// implemented with portals.classAppextendsReact.Component{constructor(props){super(props);this.state={showModal:false};this.handleShow=this.handleShow.bind(this);this.handleHide=this.handleHide.bind(this);}handleShow(){this.setState({showModal:true});}handleHide(){this.setState({showModal:false});}render(){// Show a Modal on click.// (In a real app, don't forget to use ARIA attributes// for accessibility!)constmodal=this.state.showModal?(//注意~~~~~~~~~~~~~這裡可以自行加上這個調試// <Modal><divclassName="modal"><div>Withaportal,wecanrendercontentintoadifferentpartoftheDOM,asifitwereanyotherReactchild.</div>Thisisbeingrenderedinsidethe#modal-containerdiv.<buttononClick={this.handleHide}>Hidemodal</button></div>//</Modal>):null;return(<divclassName="app">Thisdivhasoverflow:hidden.<buttononClick={this.handleShow}>Showmodal</button>{modal}</div>);}}ReactDOM.render(<App/>,appRoot);
沒有portal生成與有portal的時候生成的層級關係如下:


可以很清楚的看到,使用portal之後,modal不在嵌在app-root裡。
四、自定義DOM屬性
React 15:忽略未標準化的html 和svg屬性
React 16:去掉了這個限制
為什麼要做這個改動呢?兩個原因:
- 不能用自定義屬性,對於非標準(proposal階段)新屬性還有其他框架(Angular)很不友好
- React 15之所以可以過濾掉非標準的屬性,是因為他們維護了一個白名單的文件(放在bundle size 裡)。而隨著時間的增加,標準化的屬性越來越多,意味著要一直維護這個文件,同時這個文件也會越來越大,增加bundle的體積。
所以還不如去掉這個限制。

可以看到自定義屬性已經生效了。
五、優化SSR
具體優化了下面五個方面:
- 生成更簡潔的HTML
- 寬鬆的客戶端一致性校驗
- 無需提前編譯
- react 16服務端渲染速度更快
- 支持流式渲染
1、生成更簡潔的HTML
先看下面的HTML,react 15與react 16的服務端分別會生成什麼。
renderToString(<div>Thisissome<span>server-generated</span> <span>HTML.</span> </div> );
react15:
有data-reactid, text noded ,react-text各種屬性。
<divdata-reactroot=""data-reactid="1"data-react-checksum="122239856"><!--react-text:2-->Thisissome<!--/react-text--><spandata-reactid="3">server-generated</span><!--react-text:4--><!--/react-text --><spandata-reactid="5">HTML.</span></div>
react 16:
<divdata-reactroot="">Thisissome<span>server-generated</span> <span>HTML.</span> </div>
可以看到,react 16去掉了很多屬性,它的好處很明顯:增加易讀性,同時很大程度上減少html的文件大小。
2、寬鬆的客戶端一致性校驗
react 15 :會將SSR的結果與客戶端生成的做一個個字節的對比校驗,一點不匹配發出waring同時就替換整個SSR生成的樹。
react 16 :對比校驗會更寬鬆一些,比如,react 16允許屬性順序不一致,而且遇到不匹配的標籤,還會做子樹的修改,不是整個替換。
注意點: react16不會自動fix SSR屬性跟client html屬性的不同,但是仍然會報waring,所以我們需要自己手動去修改。
3、無需提前編譯
react 15:如果你直接使用SSR,會有很多需要檢查procee.env的地方,但是讀取在node中讀取process.env是很消耗時間的。所以在react 15的時候,需要提前編譯,這樣就可以移除process.env的引用。
react 16:只有一次檢查process.env的地方,所以就不需要提前編譯了,可以開箱即用。
4、react 16服務端渲染速度更快
為什麼呢?因為react 15下,server client都需要生成vDOM,但是其實在服務端, 當我們使用renderToString的時候,生成的vDom就會被立即拋棄掉, 所以在server端生成vDom是沒有意義的。

5、支持流式渲染( renderyToNodeStream )
使用流式渲染會提升首個字節到(TTFB)的速度。但是什麼是流式渲染呢?
可以理解為內容以一種流的形式傳給前端。所以在下一部分的內容被生成之前,開頭的內容就已經被發到瀏覽器端了。這樣瀏覽器就可以更早的編譯渲染文件內容。
// using Expressimport{renderToNodeStream}from"react-dom/server"importMyPagefrom"./MyPage"app.get("/",(req,res)=>{res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");res.write("<div id='content'>");conststream=renderToNodeStream(<MyPage/>);stream.pipe(res,{end:false});stream.on('end',()=>{res.write("</div></body></html>");res.end();});});
新的API server: renderyToNodeStream, renderToStaticNodeStream (renderToString, renderToStaticMarkup) client: hydyate
如何使用:
React 15:
// server:// using Express clientimport{renderToString}from"react-dom/server"importMyPagefrom"./MyPage"app.get("/",(req,res)=>{res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");res.write("<div id='content'>");res.write(renderToString(<MyPage/>));res.write("</div></body></html>");res.end();});// clientimport{render}from"react-dom"importMyPagefrom"./MyPage"render(<MyPage/>,document.getElementById("content"));
React 16:
其實就是把client端的render改成hydrate。
// client import { hydrate } from "react-dom" import MyPage from "./MyPage" hydrate ( < MyPage /> , document . getElementById ( "content" ));
當然,現在依然兼容render,但是17之後不再兼容,所以還是直接用hydrate好一點。
注意事項:不支持ErrorBoundary跟Portal,所以需要直出的頁面就不能用了。
五、減小了32%bundle的體積
React 庫大小從20.7kb(壓縮後6.9kb)降低到5.3kb(壓縮後2.2kb)
ReactDOM 庫大小從141kb(壓縮後42.9kb)降低到103.7kb(壓縮後32.6kb)
React + ReactDOM 庫大小從161.7kb(壓縮後49.8kb)降低到109kb(壓縮後43.8kb)
六、Fiber
由於Fiber不是新的API,是react對於對比更新的一種新算法,它影響著生命週期函數的變化跟異步渲染。需要詳細了解的同學可以戳下面的鏈接,這應該是我看過最易懂得解釋Fiber得視頻。
v16.1
react-call-return
這就是一個庫,平時用的比較少,所以暫時不講。
v16.2
主要特性:Fragement
React 15:render函數只能接受一個組件,所以一定要外層包一層<div>。
React16:可以通過Fragement直接返回多個組件。
render(){return(<><ChildA/><ChildB/><ChildC/></>);}
但是這樣看起來,似乎可以用v16.0 return一個數組搞定。
但是返回數組是有缺點的,比如:這段html
Some text. < h2 > A heading </ h2 > More text. < h2 > Another heading </ h2 > Even more text.
用Fragment寫,方便又快捷:
render () { return ( // Extraneous div element :( < Fragement > Some text . < h2 > A heading < /h2> More text . < h2 > Another heading < /h2> Even more text . < /Fragement> ); }
用數組寫.... 一言難盡(什麼,你還沒看出來有什麼區別!下面我來帶你)
render(){return["Some text.",<h2key="heading-1">Aheading</h2>,"More text.",<h2key="heading-2">Anotherheading</h2>,"Even more text."];}
缺點:
- 數組裡的子節點必須要用逗號分離
- 數組裡的子節點必須要帶key防止waring
- string類型要用雙引號括住
所以,Fragement還是很大程度上給我們提供了便利。
注意點:
<> </> 不支持寫入屬性,包括keys。如果你需要keys,你可以直接使用<Fragment> (但是Fragment也只可以接受keys這一個屬性,將來會支持更多)
function Glossary ( props ) { return ( < dl > { props . items . map ( item => ( // Without the `key`, React will fire a key warning < Fragment key = { item . id } > < dt > { item . term } < /dt> < dd > { item . description } < /dd> < /Fragment> ))} < /dl> ); }
好了,相信看到這裡,大家都很想睡覺了。堅持一下,還有五個點就講完了~。
16.3
一、新的生命週期函數
由於異步渲染的改動,有可能會導致componentWillMount, componentWillReceiveProps,componentWillUpdate ,所以需要拋棄三個函數。
由於這是一個很大的改變會影響很多現有的組件,所以需要慢慢的去改。目前react 16 只是會報waring,在react 17你就只能在前面加"UNSAFE_" 的前綴來使用。不能不說react團隊真是太貼心了,他們還寫了一個腳本自動幫你加上這些前綴。瘋狂打call~
同時新加了兩個生命週期函數來替代他們,分別是:
getDerivedStateFromProps :這個方法用於替代componentWillReceiveProps,相關內容可以看這篇文章,但是大多數情況下,都不需要用到這兩種方法。因為你都可以用其他辦法來替代。
而getSnapshotBeforeUpate使用的場景很少,這裡就不介紹了。
二、新的context API
1、context 就是可以使用全局的變量,不需要一層層pass props下去,比如主題顏色
// 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).constThemeContext=React.createContext('light');classAppextendsReact.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.Providervalue="dark"><Toolbar/></ThemeContext.Provider>);}}// A component in the middle doesn't have to// pass the theme down explicitly anymore.functionToolbar(props){return(<div><ThemedButton/></div>);}classThemedButtonextendsReact.Component{// Assign a contextType 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".staticcontextType=ThemeContext;render(){return<Buttontheme={this.context}/>;}}
但是需要謹慎使用,因為這會讓你的組件復用性變差。一般來說,如果你只是想避免需要傳很多次props的話,可以直接使用component composition(就是通過props自己傳給指定的)會更好。例如:
functionPage(props){constuser=props.user;//簡單來說就是直接父組件將props傳下去constuserLink=(<Linkhref={user.permalink}><Avataruser={user}size={props.avatarSize}/></Link>);return<PageLayoutuserLink={userLink}/>;}// Now, we have:<Pageuser={user}/>// ... which renders ...<PageLayoutuserLink={...}/>// ... which renders ...<NavigationBaruserLink={...}/>// ... which renders ...{props.userLink}
什麼場景下需要用context?一些相同的data需要被大多的component用到,並且還是在不同的層級裡。一般用於主題,存儲數據等。
三、createRef API
react15 的時候提供了兩種refs的方法: string 跟callback string:
classMyComponentextendsReact.Component{constructor(props){super(props);}//通過this.refs.textInput來獲取render(){return<inputtype="text"ref='textInput'/>;}}callback:classMyComponentextendsReact.Component{constructor(props){super(props);}//通過this.textInput來獲取render(){return<inputtype="text"ref={element=>this.textInput=element}/>;}}
由於用string的方式會導致一些潛在的問題,所以之前推薦使用callback。但是用string的方法明顯方便一點啊餵~
所以react 團隊get到了大家的需求,又出了一個新的api 可以用string的方式而且還沒有缺點, 真是可喜可賀,可口可樂。
classMyComponentextendsReact.Component{constructor(props){super(props);this.inputRef=React.createRef();}render(){return<inputtype="text"ref={this.inputRef}/>;}componentDidMount(){this.inputRef.current.focus();}}
使用場景:
- 用於操作focus, text 選擇,media playback
- 觸發即時動畫
- 與第三方組件結合
注意事項:
1、functional component 是不能傳ref屬性的,因為他們沒有instance
functionMyFunctionComponent(){return<input/>;}classParentextendsReact.Component{constructor(props){super(props);this.textInput=React.createRef();}render(){// 这个不能工作
return(<MyFunctionComponentref={this.textInput}/>);}}
但是!只要你要引用的對像是DOM元素或者是class component, 那你可以在functional component裡可以使用ref屬性
function CustomTextInput ( props ) { // textInput must be declared here so the ref can refer to it let textInput = React . createRef (); function handleClick () { textInput . current . focus (); } return ( < div > < input type = "text" ref = { textInput } /> < input type = "button" value = "Focus the text input" onClick = { handleClick } /> < /div> ); }
簡而言之:functional component裡可以使用refs但是不能把ref屬性給它本身。
四、forwardRef API
使用場景: 父組件需要將自己的引用傳給子組件
constTextInput=React.forwardRef((props,ref)=>(<inputtype="text"placeholder="Hello forwardRef"ref={ref}/>))constinputRef=React.createRef()classAppextendsComponent{constructor(props){super(props)this.myRef=React.createRef()}handleSubmit=event=>{event.preventDefault()alert('input value is:'+inputRef.current.value)}render(){return(<formonSubmit={this.handleSubmit}><TextInputref={inputRef}/><buttontype="submit">Submit</button></form>)}}constFancyButton=React.forwardRef((props,ref)=>(<buttonref={ref}className="FancyButton">{props.children}</button>));// You can now get a ref directly to the DOM button:constref=React.createRef();<FancyButtonref={ref}>Clickme!</FancyButton>;
這樣我們就可以直接用this.ref 拿到對button的引用。如果你寫的是一個高階組件,那麼推薦使用forwardAPI 將ref傳給下面的component。
五、strictMode component
嚴格模式用來幫助開發者發現潛在問題的工具。就像Fragment 一樣,它不會render任何的DOM 元素。注意:只有在development模式下才能用。
它可以幫助我們:
- 識別出使用不安全生命週期的組件
- 對使用string ref進行告警
- 對使用findDOMNode進行告警
- 探測某些產生副作用的方法
- 對使用棄用context API進行警告
還會有更多的功能在後續版本加進來。
使用:
functionExampleApplication(){return(<div><Header/><React.StrictMode><div><ComponentOne/><ComponentTwo/></div></React.StrictMode><Footer/></div>);}
如果文章對你有用,順手點個贊唄
參考文檔:
1、 React Portal
2、 官方文檔