React 16 新特性全解(上)

blank

React 16 新特性全解(上)

前言

本次系列分上下兩篇文章,上主要介紹從v16.0~ 16.4的新特性,下主要介紹16.5~16.8。下面就開始吧~

本篇文章較長預計需要15min(當然主要是因為demo太多),大家可以搞點瓜子邊啃邊看。最好能留出一隻手自己在codePen上自己調試一下。

目錄

v16.0

  1. render支持返回數組和字符串演示
  2. Error Boundary
  3. createPortal
  4. 支持自定義DOM 屬性
  5. Fiber
  6. 提升SSR渲染速度
  7. 減小文件體積

v16.1

react-call-return

v16.2

Fragment

v16.3

  1. 生命週期函數的更新
  2. createContext
  3. createRef
  4. forwardRef
  5. 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'));

demo地址

比如上面這個App,可以看到子組件BuggyCounter出了點問題,在沒有Error Boundary的時候,整個App都會crash掉,所以顯示白屏。

React 16 :用於捕獲子組件樹的JS異常(即錯誤邊界只可以捕獲組件在樹中比他低的組件錯誤。),記錄錯誤並展示一個回退的UI。

捕獲範圍:

  1. 渲染期間
  2. 生命週期內
  3. 整個組件樹構造函數內

如何使用:

//先定一個組件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'));

demo演示:

可以看到加上Error Boundary之後,除了出錯的組件,其他的地方都不受影響。

blank

而且它很清晰的告訴我們是哪個組件發生了錯誤。

注意事項:

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的時候生成的層級關係如下:

blank
blank

可以很清楚的看到,使用portal之後,modal不在嵌在app-root裡。

四、自定義DOM屬性

React 15:忽略未標準化的html 和svg屬性

React 16:去掉了這個限制

為什麼要做這個改動呢?兩個原因:

  1. 不能用自定義屬性,對於非標準(proposal階段)新屬性還有其他框架(Angular)很不友好
  2. React 15之所以可以過濾掉非標準的屬性,是因為他們維護了一個白名單的文件(放在bundle size 裡)。而隨著時間的增加,標準化的屬性越來越多,意味著要一直維護這個文件,同時這個文件也會越來越大,增加bundle的體積。

所以還不如去掉這個限制。

演示

blank

可以看到自定義屬性已經生效了。

五、優化SSR

具體優化了下面五個方面:

  1. 生成更簡潔的HTML
  2. 寬鬆的客戶端一致性校驗
  3. 無需提前編譯
  4. react 16服務端渲染速度更快
  5. 支持流式渲染

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是沒有意義的。

blank
圖片來源(https://hackernoon.com/whats-new-with-server-side-rendering-in-react-16-9b0d78585d67)

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();}}

使用場景:

  1. 用於操作focus, text 選擇,media playback
  2. 觸發即時動畫
  3. 與第三方組件結合

注意事項:

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模式下才能用。

它可以幫助我們:

  1. 識別出使用不安全生命週期的組件
  2. 對使用string ref進行告警
  3. 對使用findDOMNode進行告警
  4. 探測某些產生副作用的方法
  5. 對使用棄用context API進行警告

還會有更多的功能在後續版本加進來。

使用:

functionExampleApplication(){return(<div><Header/><React.StrictMode><div><ComponentOne/><ComponentTwo/></div></React.StrictMode><Footer/></div>);}

如果文章對你有用,順手點個贊唄

參考文檔:

1、 React Portal

2、 官方文檔

3、 ayqy.net/blog/react-16/

4、 React 16 SSR新特性

5、 Refs and the Dom

What do you think?

Written by marketer

blank

RIS,創建React 應用的新選擇

blank

在React 中處理數據流問題的一些思考