前言
本次系列分上下兩篇文章,上主要介紹從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包住。
function MyComponent() {
return (
<div>
hello world
<div>
);
}
複製程式碼
React 16: 支援返回這五類:React elements, 陣列和Fragments,Portal,String/numbers,boolean/null。
class Example extends React.Component {
render() {
return [
<div key="1">first element</div>,
<div key="2">second element</div>,
];
}
}
複製程式碼
注意:無論返回的形式是怎麼樣的,都要保持render是一個純函式。所以要求我們不要改state的狀態,同時不要直接跟瀏覽器直接互動,讓它每次呼叫生成的結果都是一致的。
二、Error boundary(錯誤邊界)
React 15:渲染過程中有出錯,直接crash整個頁面,並且錯誤資訊不明確,可讀性差
class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
throw new Error('I am crash');
}
handleClick() {
this.setState(({counter}) => ({
counter: counter + 1
}));
}
render() {
if (this.state.counter === 5) {
// Simulate a JS error
throw new Error('I crashed!');
}
return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
}
}
function App() {
return (
<div>
<p>
<b>
This is an example of error boundaries in React 16.
<br /><br />
Click on the numbers to increase the counters.
<br />
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
</b>
</p>
<hr />
<p>These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.</p>
<BuggyCounter />
<hr />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
複製程式碼
比如上面這個App,可以看到子元件BuggyCounter出了點問題,在沒有Error Boundary的時候,整個App都會crash掉,所以顯示白屏。
React 16:用於捕獲子元件樹的JS異常(即錯誤邊界只可以捕獲元件在樹中比他低的元件錯誤。),記錄錯誤並展示一個回退的UI。
捕獲範圍:
- 渲染期間
- 生命週期內
- 整個元件樹建構函式內
如何使用:
// 先定一個元件ErrorBoundary
class ErrorBoundary extends React.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 message
this.setState({
error: error,
errorInfo: errorInfo
})
// You can also log error messages to an error reporting service here
}
render() {
// 有錯誤的時候展示回退
if (this.state.errorInfo) {
// Error path
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
// 正常的話,直接展示元件
return this.props.children;
}
}
class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
throw new Error('I am crash');
}
handleClick() {
this.setState(({counter}) => ({
counter: counter + 1
}));
}
render() {
if (this.state.counter === 5) {
// Simulate a JS error
throw new Error('I crashed!');
}
return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
}
}
function App() {
return (
<div>
<p>
<b>
This is an example of error boundaries in React 16.
<br /><br />
Click on the numbers to increase the counters.
<br />
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
</b>
</p>
<hr />
<ErrorBoundary>
<p>These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.</p>
<BuggyCounter />
</ErrorBoundary>
<hr />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
複製程式碼
可以看到加上Error Boundary之後,除了出錯的元件,其他的地方都不受影響。
而且它很清晰的告訴我們是哪個元件發生了錯誤。
注意事項:
Error Boundary無法捕獲下面的錯誤:
1、事件函式裡的錯誤
class MyComponent extends React.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>Caught an error.</h1>
}
return <div onClick={this.handleClick}>Click Me</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元件,我們會這樣寫。
<div class="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:
<div id="app-root"></div>
// 這裡為我們定義Dialog想要放入的位置
<div id="modal-root"></div>
複製程式碼
JS:
// These two containers are siblings in the DOM
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
// Let's create a Modal component that is an abstraction around
// the portal API.
class Modal extends React.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 unmount
modalRoot.removeChild(this.el);
}
render() {
// Use a portal to render the children into the element
return ReactDOM.createPortal(
// Any valid React child: JSX, strings, arrays, etc.
this.props.children,
// A DOM element
this.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.
class App extends React.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!)
const modal = this.state.showModal ? (
<div className="modal">
<div>
With a portal, we can render content into a different
part of the DOM, as if it were any other React child.
</div>
This is being rendered inside the #modal-container div.
<button onClick={this.handleHide}>Hide modal</button>
</div>
) : null;
return (
<div className="app">
This div has overflow: hidden.
<button onClick={this.handleShow}>Show modal</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>
This is some <span>server-generated</span> <span>HTML.</span>
</div> );
複製程式碼
react15:
有data-reactid, text noded ,react-text各種屬性。
<div data-reactroot="" data-reactid="1"
data-react-checksum="122239856">
<!-- react-text: 2 -->This is some <!-- /react-text -->
<span data-reactid="3">server-generated</span>
<!-- react-text: 4--> <!-- /react-text -->
<span data-reactid="5">HTML.</span>
</div>
複製程式碼
react 16:
<div data-reactroot="">
This is some <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、支援流式渲染
使用流式渲染會提升首個位元組到(TTFB)的速度。但是什麼是流式渲染呢?
可以理解為內容以一種流的形式傳給前端。所以在下一部分的內容被生成之前,開頭的內容就已經被髮到瀏覽器端了。這樣瀏覽器就可以更早的編譯渲染檔案內容。
// using Express
import { renderToNodeStream } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
res.write("<div id='content'>");
const stream = 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 client
import { renderToString } from "react-dom/server"
import MyPage from "./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();
});
// client
import { render } from "react-dom"
import MyPage from "./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得視訊。
https://www.youtube.com/watch?v=VLAqywvHpD0v16.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.",
<h2 key="heading-1">A heading</h2>,
"More text.",
<h2 key="heading-2">Another heading</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>
);
}
複製程式碼
好了,相信看到這裡,大家都很想睡覺了。堅持一下,還有五個點就講完了~。
前言
本次系列分上下兩篇文章,上主要介紹從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包住。
function MyComponent() {
return (
<div>
hello world
<div>
);
}
複製程式碼
React 16: 支援返回這五類:React elements, 陣列和Fragments,Portal,String/numbers,boolean/null。
class Example extends React.Component {
render() {
return [
<div key="1">first element</div>,
<div key="2">second element</div>,
];
}
}
複製程式碼
注意:無論返回的形式是怎麼樣的,都要保持render是一個純函式。所以要求我們不要改state的狀態,同時不要直接跟瀏覽器直接互動,讓它每次呼叫生成的結果都是一致的。
二、Error boundary(錯誤邊界)
React 15:渲染過程中有出錯,直接crash整個頁面,並且錯誤資訊不明確,可讀性差
class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
throw new Error('I am crash');
}
handleClick() {
this.setState(({counter}) => ({
counter: counter + 1
}));
}
render() {
if (this.state.counter === 5) {
// Simulate a JS error
throw new Error('I crashed!');
}
return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
}
}
function App() {
return (
<div>
<p>
<b>
This is an example of error boundaries in React 16.
<br /><br />
Click on the numbers to increase the counters.
<br />
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
</b>
</p>
<hr />
<p>These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.</p>
<BuggyCounter />
<hr />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
複製程式碼
比如上面這個App,可以看到子元件BuggyCounter出了點問題,在沒有Error Boundary的時候,整個App都會crash掉,所以顯示白屏。
React 16:用於捕獲子元件樹的JS異常(即錯誤邊界只可以捕獲元件在樹中比他低的元件錯誤。),記錄錯誤並展示一個回退的UI。
捕獲範圍:
- 渲染期間
- 生命週期內
- 整個元件樹建構函式內
如何使用:
// 先定一個元件ErrorBoundary
class ErrorBoundary extends React.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 message
this.setState({
error: error,
errorInfo: errorInfo
})
// You can also log error messages to an error reporting service here
}
render() {
// 有錯誤的時候展示回退
if (this.state.errorInfo) {
// Error path
return (
<div>
<h2>Something went wrong.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
// 正常的話,直接展示元件
return this.props.children;
}
}
class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
componentWillMount() {
throw new Error('I am crash');
}
handleClick() {
this.setState(({counter}) => ({
counter: counter + 1
}));
}
render() {
if (this.state.counter === 5) {
// Simulate a JS error
throw new Error('I crashed!');
}
return <h1 onClick={this.handleClick}>{this.state.counter}</h1>;
}
}
function App() {
return (
<div>
<p>
<b>
This is an example of error boundaries in React 16.
<br /><br />
Click on the numbers to increase the counters.
<br />
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
</b>
</p>
<hr />
<ErrorBoundary>
<p>These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.</p>
<BuggyCounter />
</ErrorBoundary>
<hr />
</div>
);
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
複製程式碼
可以看到加上Error Boundary之後,除了出錯的元件,其他的地方都不受影響。
而且它很清晰的告訴我們是哪個元件發生了錯誤。
注意事項:
Error Boundary無法捕獲下面的錯誤:
1、事件函式裡的錯誤
class MyComponent extends React.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>Caught an error.</h1>
}
return <div onClick={this.handleClick}>Click Me</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元件,我們會這樣寫。
<div class="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:
<div id="app-root"></div>
// 這裡為我們定義Dialog想要放入的位置
<div id="modal-root"></div>
複製程式碼
JS:
// These two containers are siblings in the DOM
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
// Let's create a Modal component that is an abstraction around
// the portal API.
class Modal extends React.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 unmount
modalRoot.removeChild(this.el);
}
render() {
// Use a portal to render the children into the element
return ReactDOM.createPortal(
// Any valid React child: JSX, strings, arrays, etc.
this.props.children,
// A DOM element
this.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.
class App extends React.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!)
const modal = this.state.showModal ? (
//注意~~~~~~~~~~~~~這裡可以自行加上這個除錯
// <Modal>
<div className="modal">
<div>
With a portal, we can render content into a different
part of the DOM, as if it were any other React child.
</div>
This is being rendered inside the #modal-container div.
<button onClick={this.handleHide}>Hide modal</button>
</div>
//</Modal>
) : null;
return (
<div className="app">
This div has overflow: hidden.
<button onClick={this.handleShow}>Show modal</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>
This is some <span>server-generated</span> <span>HTML.</span>
</div> );
複製程式碼
react15:
有data-reactid, text noded ,react-text各種屬性。
<div data-reactroot="" data-reactid="1"
data-react-checksum="122239856">
<!-- react-text: 2 -->This is some <!-- /react-text -->
<span data-reactid="3">server-generated</span>
<!-- react-text: 4--> <!-- /react-text -->
<span data-reactid="5">HTML.</span>
</div>
複製程式碼
react 16:
<div data-reactroot="">
This is some <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、支援流式渲染
會提升首個位元組到的速度,不過我試了一下,會閃屏,所以我不太推薦使用/除非我們的頁面做到一個空的框架先來,內容在填充。
新的API server: renderyToNodeStream, renderToStaticNodeStream (renderToString, renderToStaticMarkup) client: hydyate
如何使用:
React 15:
// server:
// using Express client
import { renderToString } from "react-dom/server"
import MyPage from "./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();
});
// client
import { render } from "react-dom"
import MyPage from "./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.",
<h2 key="heading-1">A heading</h2>,
"More text.",
<h2 key="heading-2">Another heading</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).
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>
);
}
class ThemedButton extends React.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".
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
複製程式碼
但是需要謹慎使用,因為這會讓你的元件複用性變差。 一般來說,如果你只是想避免需要傳很多次props的話,可以直接使用component composition(就是通過props自己傳給指定的)會更好。 例如:
function Page(props) {
const user = props.user;
// 簡單來說就是直接父元件將props傳下去
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
// Now, we have:
<Page user={user} />
// ... which renders ...
<PageLayout userLink={...} />
// ... which renders ...
<NavigationBar userLink={...} />
// ... which renders ...
{props.userLink}
複製程式碼
什麼場景下需要用context? 一些相同的data需要被大多的component用到,並且還是在不同的層級裡。 一般用於主題,儲存資料等。
三、createRef API
react15 的時候提供了兩種refs的方法: string 跟 callback string:
class MyComponent extends React.Component {
constructor(props) {
super(props);
}
// 通過this.refs.textInput 來獲取
render() {
return <input type="text" ref='textInput' />;
}
}
callback:
class MyComponent extends React.Component {
constructor(props) {
super(props);
}
// 通過this.textInput 來獲取
render() {
return <input type="text" ref={element => this.textInput = element} />;
}
}
複製程式碼
由於用string的方式會導致一些潛在的問題,所以之前推薦使用callback。但是用string的方法明顯方便一點啊喂~
所以react 團隊get到了大家的需求,又出了一個新的api 可以用string的方式而且還沒有缺點, 真是可喜可賀,可口可樂。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
return <input type="text" ref={this.inputRef} />;
}
componentDidMount() {
this.inputRef.current.focus();
}
}
複製程式碼
使用場景:
- 用於操作focus, text 選擇,media playback
- 觸發即時動畫
- 與第三方元件結合
注意事項:
1、functional component 是不能傳ref屬性的,因為他們沒有instance
function MyFunctionComponent() {
return <input />;
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// 這個不能工作
return (
<MyFunctionComponent ref={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
使用場景: 父元件需要將自己的引用傳給子元件
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>
)
}
}
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
複製程式碼
這樣我們就可以直接用this.ref 拿到對button的引用。 如果你寫的是一個高階元件,那麼推薦使用forwardAPI 將ref傳給下面的component。
五、strictMode component
嚴格模式用來幫助開發者發現潛在問題的工具。就像Fragment 一樣,它不會render任何的DOM 元素。注意:只有在development模式下才能用。
它可以幫助我們:
- 識別出使用不安全生命週期的元件
- 對使用string ref進行告警
- 對使用findDOMNode進行告警
- 探測某些產生副作用的方法
- 對使用棄用context API進行警告
還會有更多的功能在後續版本加進來。
使用:
function ExampleApplication() {
return (
<div>
<Header />
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
<Footer />
</div>
);
}
複製程式碼
參考文件:
2、官方文件