使用 React.js
一段時間了,把使用過程遇到的小坑和小技巧記錄下來,但願可以幫助到其餘人。此文章是長篇大論你只有耐得住寂寞,禁得住誘惑纔會有所成長。javascript
<div dangerouslySetInnerHTML={{ __html: LANG.auth_register_tips1 }}/>複製代碼
axios
(http
請求模塊,可用於前端任何場景,很強大)echarts-for-react
(可視化圖表,別人基於react對echarts的封裝,足夠用了)recharts
(另外一個基於react
封裝的圖表)nprogress
(頂部加載條,蠻好用)react-draft-wysiwyg(
別人基於react
的富文本封裝,若是找到其餘更好的能夠替換)react-draggable
(拖拽模塊,找了個簡單版的)screenfull
(全屏插件)photoswipe
(圖片彈層查看插件,不依賴jQuery,仍是蠻好用)animate.css
(css動畫庫)redux Web
應用是一個狀態機,視圖與狀態是一一對應的.全部的狀態,保存在一個對象裏面redux-logger
日誌Reselect
記憶組件redux-thunk
爲了解決異步action的問題redux-saga
爲了解決異步action的問題react-router-redux
保持路由與應用狀態(state)同步react-router-dom
工具地址:github.com/facebook/re…css
全局安裝:html
yarn global add react-devtools複製代碼
配置:在package.json中配置上去:前端
"scripts": {
"devtools": "react-devtools"
}複製代碼
而後就能夠啓動了:yarn run devtoolsvue
- redux 架構
- 父組件向子組件 —— props
- 子組件向父組件 —— props.funciton 接收參數
- 利用事件機制
React數據流動是單向的,父組件向子組件通訊也是最多見的;父組件經過props向子組件傳遞須要的信息java
// 通常改變state值的一種方式
const { data } = this.state;
this.setState({ data: {...data, key: 1 } });
// 另一種能夠經過callback的方式改變state的值
this.setState(({ data }) => ({ data: {...data, key: 1 } }));
// 還能夠
this.setState((state, props) => {
return { counter: state.counter + props.step };
});複製代碼
例如A組件和B組件之間要進行通訊,先找到A和B公共的父組件,A先向C組件通訊,C組件經過props和B組件通訊,此時C組件起的就是中間件的做用
context是一個全局變量,像是一個大容器,在任何地方均可以訪問到,咱們能夠把要通訊的信息放在context上,而後在其餘組件中能夠隨意取到;可是React官方不建議使用大量context,儘管他能夠減小逐層傳遞,可是當組件結構複雜的時候,咱們並不知道context是從哪裏傳過來的;並且context是一個全局變量,全局變量正是致使應用走向混亂的罪魁禍首.
下面例子中的組件關係: ListItem是List的子組件,List是app的子組件react
ListItem.jswebpack
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ListItem extends Component {
// 子組件聲明本身要使用context
static contextTypes = {
color: PropTypes.string,
}
static propTypes = {
value: PropTypes.string,
}
render() {
const { value } = this.props;
return (
<li style={{ background: this.context.color }}>
<span>{value}</span>
</li>
);
}
}
export default ListItem;複製代碼
List.jsios
import ListItem from './ListItem';
class List extends Component {
// 父組件聲明本身支持context
static childContextTypes = {
color: PropTypes.string,
}
static propTypes = {
list: PropTypes.array,
}
// 提供一個函數,用來返回相應的context對象
getChildContext() {
return {
color: 'red',
};
}
render() {
const { list } = this.props;
return (
<div>
<ul>
{
list.map((entry, index) =>
<ListItem key={`list-${index}`} value={entry.text} />,
)
}
</ul>
</div>
);
}
}
export default List;複製代碼
App.jsgit
import React, { Component } from 'react';
import List from './components/List';
const list = [
{
text: '題目一',
},
{
text: '題目二',
},
];
export default class App extends Component {
render() {
return (
<div>
<List
list={list}
/>
</div>
);
}
}複製代碼
在componentDidMount事件中,若是組件掛載完成,再訂閱事件;在組件卸載的時候,在componentWillUnmount事件中取消事件的訂閱;以經常使用的發佈/訂閱模式舉例,借用Node.js Events模塊的瀏覽器版實現
下面例子中的組件關係: List1和List2沒有任何嵌套關係,App是他們的父組件;
實現這樣一個功能: 點擊List2中的一個按鈕,改變List1中的信息顯示
首先須要項目中安裝events 包:
npm install events --save複製代碼
在src下新建一個util目錄裏面建一個events.js
import { EventEmitter } from 'events';
export default new EventEmitter();複製代碼
list1.js
import React, { Component } from 'react';
import emitter from '../util/events';
class List extends Component {
constructor(props) {
super(props);
this.state = {
message: 'List1',
};
}
componentDidMount() {
// 組件裝載完成之後聲明一個自定義事件
this.eventEmitter = emitter.addListener('changeMessage', (message) => {
this.setState({
message,
});
});
}
componentWillUnmount() {
emitter.removeListener(this.eventEmitter);
}
render() {
return (
<div>
{this.state.message}
</div>
);
}
}
export default List;複製代碼
List2.js
import React, { Component } from 'react';
import emitter from '../util/events';
class List2 extends Component {
handleClick = (message) => {
emitter.emit('changeMessage', message);
};
render() {
return (
<div>
<button onClick={this.handleClick.bind(this, 'List2')}>點擊我改變List1組件中顯示信息</button>
</div>
);
}
}複製代碼
APP.js
import React, { Component } from 'react';
import List1 from './components/List1';
import List2 from './components/List2';
export default class App extends Component {
render() {
return (
<div>
<List1 />
<List2 />
</div>
);
}
}複製代碼
自定義事件是典型的發佈訂閱模式,經過向事件對象上添加監聽器和觸發事件來實現組件之間的通訊
組件間通訊除了
props
外還有onRef
方法,不過React官方文檔建議不要過分依賴ref。本文使用onRef
語境爲在表單錄入時提取公共組件,在提交時分別獲取表單信息。
下面demo中點擊父組件按鈕能夠獲取子組件所有信息,包括狀態和方法,能夠看下demo中控制檯打印。
// 父組件
class Parent extends React.Component {
testRef=(ref)=>{
this.child = ref
console.log(ref) // -> 獲取整個Child元素
}
handleClick=()=>{
alert(this.child.state.info) // -> 經過this.child能夠拿到child全部狀態和方法
}
render() {
return <div>
<Child onRef={this.testRef} />
<button onClick={this.handleClick}>父組件按鈕</button>
</div>
}
}
// 子組件
class Child extends React.Component {
constructor(props) {
super(props)
this.state = {
info:'快點擊子組件按鈕哈哈哈'
}
}
componentDidMount(){
this.props.onRef(this)
console.log(this) // ->將child傳遞給this.props.onRef()方法
}
handleChildClick=()=>{
this.setState({info:'經過父組件按鈕獲取到子組件信息啦啦啦'})
}
render(){
return <button onClick={this.handleChildClick}>子組件按鈕</button>
}
}複製代碼
當在子組件中調用onRef函數時,正在調用從父組件傳遞的函數。this.props.onRef(this)
這裏的參數指向子組件自己,父組件接收該引用做爲第一個參數:onRef = {ref =>(this.child = ref)}
而後它使用this.child
保存引用。以後,能夠在父組件內訪問整個子組件實例,而且能夠調用子組件函數。
使用React構建的單頁面應用,要想實現頁面間的跳轉,首先想到的就是使用路由。在React中,經常使用的有兩個包能夠實現這個需求,那就是react-router和react-router-dom。本文主要針對react-router-dom進行說明。
基於React Router的web應用,根組件應該是一個router組件(
BrowserRouter
,HashRouter
)。 項目中,react-router-dom
提供了和兩種路由。兩種路由都會建立一個history
對象。若是咱們的應用有服務器響應web的請求,咱們一般使用<BrowserRouter>
組件; 若是使用靜態文件服務器,則咱們應該使用<HashRouter>
組件
其實就是路由的hash和history兩種模式(要是不瞭解這兩種模式之間的區別那就須要去惡補下啦)
而且這兩個組件是路由的容器,必須在最外層
// hash模式
ReactDom.render(
<HashRouter>
<Route path="/" component={Home}/>
</HashRouter>
)
// history模式
ReactDom.render(
<BrowserRouter>
<Route path="/" component={Home}/>
</BrowserRouter>
)複製代碼
下面說說HashRouter和BrowserRouter上的參數
Route是路由的一個原材料,它是控制路徑對應顯示的組件
Route的參數
path | url | 是否開啓 | 匹配結果 |
/a | /a/b | false | yes |
/a | /a/b | true | no |
path | url | 是否開啓 | 匹配結果 |
/a | /a | true | yes |
/a | /A | true | yes |
path | url | 是否開啓 | 匹配結果 |
/a | /a/ | true | yes |
/a | /a/c | true | yes |
/a | /a | true | no |
低級路由,適用於任何路由組件,主要和redux深度集成,使用必須配合history對象
使用Router路由的目的是和狀態管理庫如redux中的history同步對接
<Router history={history}>
...
</Router>
複製代碼複製代碼
二者都是跳轉路由,NavLink的參數更多些
<Link to="/a"/>
複製代碼複製代碼
<Link to={{
pathname: '/courses',
search: '?sort=name',
hash: '#the-hash',
state: { fromDashboard: true }
}}/>
複製代碼複製代碼
const oddEvent = (match, location) => {
console.log(match,location)
if (!match) {
return false
}
console.log(match.id)
return true
}
<NavLink isActive={oddEvent} to="/a/123">組件一</NavLink>
複製代碼複製代碼
<NavLink to="/a/123" location={{ key:"mb5wu3", pathname:"/a/123" }}/>
複製代碼複製代碼
Redirect重定向很簡單,咱們直接看代碼便可
// 基本的重定向
<Redirect to="/somewhere/else" />
// 對象形式
<Redirect
to={{
pathname: "/login",
search: "?utm=your+face",
state: { referrer: currentLocation }
}}
/>
// 採用push生成新的記錄
<Redirect push to="/somewhere/else" />
// 配合Switch組件使用,form表示重定向以前的路徑,若是匹配則重定向,不匹配則不重定向
<Switch>
<Redirect from='/old-path' to='/new-path'/>
<Route path='/new-path' component={Place}/>
</Switch>
複製代碼複製代碼
路由切換,只會匹配第一個路由,能夠想象成tab欄
Switch內部只能包含Route、Redirect、Router
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
</Switch>
複製代碼複製代碼
當一個非路由組件也想訪問到當前路由的match,location,history對象,那麼withRouter將是一個很是好的選擇,能夠理解爲將一個組件包裹成路由組件
import { withRouter } from 'react-router-dom'
const MyComponent = (props) => {
const { match, location, history } = this.props
return (
<div>{props.location.pathname}</div>
)
}
const FirstTest = withRouter(MyComponent);
複製代碼複製代碼
用過vue的都知道,vue-router有組件形式的導航,也有編程式導航,那react-router怎麼使用api來控制前進後退和刷新呢?這就須要咱們來講明下history對象的做用了
其實在每一個路由組件中咱們可使用this.props.history獲取到history對象,也可使用withRouter包裹組件獲取,
在history中封裝了push,replace,go等方法,具體內容以下
History {
length: number;
action: Action;
location: Location;
push(path: Path, state?: LocationState): void; // 調用push前進到一個地址,能夠接受一個state對象,就是自定義的路由數據
push(location: LocationDescriptorObject): void; // 接受一個location的描述對象
replace(path: Path, state?: LocationState): void; // 用頁面替換當前的路徑,不可再goBack
replace(location: LocationDescriptorObject): void; // 同上
go(n: number): void; // 往前走多少也頁面
goBack(): void; // 返回一個頁面
goForward(): void; // 前進一個頁面
block(prompt?: boolean | string | TransitionPromptHook): UnregisterCallback;
listen(listener: LocationListener): UnregisterCallback;
createHref(location: LocationDescriptorObject): Href;
}
複製代碼複製代碼
這樣咱們想使用api來操做前進後退就能夠調用history中的方法啦
其次也可經過暗轉history
庫來實現,具體案例以下
import { BrowserRouter } from 'react-router-dom';
const history = require('history').createBrowserHistory();
/**
* forceRefresh: bool
* 做用:當瀏覽器不支持 HTML5 的 history API 時強制刷新頁面。
*/
const supportsHistory = 'pushState' in window.history;
<BrowserRouter
history={history}
basename="/"
forceRefresh={!supportsHistory}
>
{/* 路由入口 */}
......
</BrowserRouter>複製代碼
寫了一段時間的
react
以後,漸漸的喜歡上了使用react
來寫應用。咱們知道,
react
時打出的旗號之一就是高性能。今天咱們還一塊兒來聊一聊
react
的性能優化,思考還能經過哪些手段來提高React的性能,使咱們的react
更快,性能更好。
再講性能優化以前,咱們須要先來了解一下如何查看
react
加載組件時所耗費的時間的工具,在react 16版本以前咱們可使用React Perf
來查看。
react16
版本以前,咱們可使用react-addons-perf
工具來查看,而在最新的16版本,咱們只須要在url後加上?react_pref。
首先來了解一下react-addons-perf。
react-addons-perf
這是 React 官方推出的一個性能工具包,能夠打印出組件渲染的時間、次數、浪費時間等。
簡單說幾個api,具體用法可參考官網:
Perf.start()
開始記錄Perf.stop()
結束記錄Perf.printInclusive()
查看全部設計到的組件renderPerf.printWasted()
查看不須要的浪費組件render你們能夠在chorme中先安裝React Perf擴展,而後在入口文件或者redux
的store.js
中加入相應的代碼便可:
再來了解一下,在最新的React16版本中,在url後加上?react_pref
,就能夠在chrome瀏覽器的performance
,咱們能夠查看User Timeing
來查看組件的加載時間。點擊record開始記錄,注意記錄時長不要超過20s,不然可能致使chrome掛起。
使用此工具的具體操做你們能夠看下圖:
首先咱們先思考一個問題,好比我要實現一個點擊按鈕使相應的
你們應該都能想到,無非就是三種,以下圖:num
增長1,咱們有哪一些方法。
第一種是在構造函數中綁定this
,第二種是在render()
函數裏面綁定this
,第三種就是使用箭頭函數,都能實現上述方法;
可是哪種方法的性能最好,是咱們要考慮的問題。應該你們都知道答案:第一種的性能最好。
由於第一種,構造函數每一次渲染的時候只會執行一遍;
而第二種方法,在每次render()
的時候都會從新執行一遍函數;
第三種方法的話,每一次render()
的時候,都會生成一個新的箭頭函數,即便兩個箭頭函數的內容是同樣的。
第三種方法咱們能夠舉一個例子,由於react
判斷是否須要進行render
是淺層比較,簡單來講就是經過===
來判斷的,若是state
或者prop
的類型是字符串或者數字,只要值相同,那麼淺層比較就會認爲其相同;
可是若是前者的類型是複雜的對象的時候,咱們知道對象是引用類型,淺層比較只會認爲這兩個prop
是否是同一個引用,若是不是,哪怕這兩個對象中的內容徹底同樣,也會被認爲是兩個不一樣的prop
。
舉個例子:
當咱們給組件Foo
給名爲style
的prop
賦值;
<Foo style={{ color:"red" }}複製代碼
使用這種方法,每一次渲染都會被認爲是一個style
這個prop
發生了變化,由於每一次都會產生一個對象給style
。
那麼咱們應該如何改進,若是想要讓react
渲染的時候認爲先後對象類型prop
相同,則必需要保證prop
指向同一個javascript
對象,以下:
const fooStyle = { color: "red" }; //取保這個初始化只執行一次,不要放在render中,能夠放在構造函數中
<Foo style={fooStyle} />複製代碼
shouldComponentUpdate
是決定react
組件何時可以不從新渲染的函數,可是這個函數默認的實現方式就是簡單的返回一個true
。也就是說,默認每次更新的時候都要調用所用的生命週期函數,包括render
函數,從新渲染。
咱們來看一下下面的一個例子
咱們寫兩個組件,App
和Demo
組件,並寫兩個方法,一個改變App
中的num
的值,一個是改變title
,咱們在Demo的render中打印render函數。咱們能夠看到如下的效果:
咱們能夠清晰的看到雖然demo
組件裏的title
值沒有改變,可是仍是render
了。
爲了解決這個問題,咱們能夠對demo組件進行以下的修改:
只有當demo的title值發生改變的時候,咱們纔去render,咱們能夠看一下效果:
以上只是一個特別簡單的一個對於shouldComponentUpdate
的定製。
在最新的react
中,react給咱們提供了React.PureComponent
,官方也在早期提供了名爲react-addons-pure-render-mixin
插件來從新實現shouldComponentUpdate
生命週期方法。
經過上述的方法的效果也是和咱們定製shouldComponentUpdate
的效果是一致的。
可是咱們要注意的是,這裏的PureRender
是淺比較的,由於深比較的場景是至關昂貴的。因此咱們要注意咱們在1.1
中說到的一些注意點:不要直接爲props設置對象或者數組、不要將方法直接綁定在元素上,由於其實函數也是對象
react
組件在裝載過程當中,react
經過在render
方法在內存中產生一個樹形結構,樹上的節點表明一個react
組件或者原生的Dom
元素,這個樹形結構就是咱們所謂的Vitural Dom
,react根據這個來渲染產生瀏覽器的Dom
樹。
react
在更新階段對比原有的Vitural Dom
和新生成的Vitural Dom
,找出不一樣之處,在根據不一樣來渲染Dom樹。react爲了追求高性能,採用了時間複雜度爲
O(N)
來比較兩個屬性結構的區別,由於要確切比較兩個樹形結構,須要經過O(N^3)
,這會下降性能。簡單來講,react利用key來識別組件,它是一種身份標識標識,就像咱們的身份證用來辨識一我的同樣。
*注意:另外有個方式:推薦使用shortid生成惟一key的數組,和數據數組一塊兒使用,省去提交數據時再重組數組。
案例:
import React from 'react';
import shortid from 'shortid';
class Demo extends React.Component {
constructor(props) {
super(props);
this.state = {
data: ['a', 'b', 'c']
}
this.dataKeys = this.state.data.map(v => shortid.generate());
}
deleteOne = index => { // 刪除操做
const { data } = this.state;
this.setState({ data: data.filter((v, i) => i !== index) });
this.dataKyes.splice(index, 1);
}
render() {
return (
<ul>
{
data.map((v, i) =>
<li
onClick={i => this.deleteOne(i)}
key={this.dataKeys[i]}
>
{v}
</li>
)
}
</ul>
)
}
}
// 稍微抽取,能夠封裝一個通用的組件複製代碼
另外須要指明的是:
key不是用來提高react的性能的,不過用好key對性能是有幫組的。
不能使用random來使用key
key相同,若組件屬性有所變化,則react只更新組件對應的屬性;沒有變化則不更新。
key值不一樣,則react先銷燬該組件(有狀態組件的
componentWillUnmount會執行
),而後從新建立該組件(有狀態組件的constructor
和componentWillUnmount
都會執行)
咱們舉幾個狀況,你們就會立刻理解:
// A組件
<div>
<Todos />
</div>
// B組件
<span>
<Todos />
</span>複製代碼
咱們想把A
組件更新成B
組件,react
在作比較的時候,發現最外面的根結點不同,直接就廢掉了以前的<div>
節點,包括裏面的子節點,這是一個巨大的浪費,可是爲了不O(N^3)
的時間複雜度,只能採用這種方式
因此在開發過程當中,咱們應該儘可能避免上面的狀況,不要將包裹節點的類型隨意改變。
這裏包括兩種狀況,一種是節點是Dom
類型,還有一種react
組件。
對於dom
類型,咱們舉個例子:
// A組件
<div style={{color: 'red',fontSize:15}} className="welcome">
Hello World!!!
</div>
// B組件
<div style={{color: 'green',fontSize:15}} className="react">
Good Bye!!!
</div>複製代碼
上述A和B組件的區別是文字、className
、style
中的color
發生改變,由於Dom
元素沒變,React
只會修改他變化的部分。
針對react
組件類型,渲染無非就是在走一遍組件實例的更新過程,最主要的就是定製shouldComponentUpdate
,咱們上面也有講到,就不細講了。
咱們看兩個例子就能明白
例子一:
// A
<ul>
<TodoItem text="First" complete={false} />
<TodoItem text="Second" complete={false} />
</ul>
// B
<ul>
<TodoItem text="First" complete={false} />
<TodoItem text="Second" complete={false} />
<TodoItem text="Third" complete={false} />
</ul>複製代碼
從A變到B,若是shouldComponentUpdate
處理得當,咱們只須要更新裝載third
的那一次就行。
咱們來看看下一個例子:
// A
<ul>
<TodoItem text="First" complete={false} />
<TodoItem text="Second" complete={false} />
</ul>
// B
<ul>
<TodoItem text="Zero" complete={false} />
<TodoItem text="First" complete={false} />
<TodoItem text="Second" complete={false} />
</ul>複製代碼
這裏由於react是採用O(n)的時間複雜度,因此會依次將text爲First的改成Zero,text爲Second改成First,在最後再加上一個組件,text爲Second。現存的兩個的text的屬性都被改變了,因此會依次渲染。
若是咱們這裏有1000個實例,那麼就會發生1000次更新。
這裏咱們就要用到Key
了
簡單來講,其實這一個Key就是react組件的身份證號。
咱們將上一個例子改爲以下,就能夠避免上面的問題了,react就可以知道其實B裏面的第二個和第三個組件其實就是A中的第一個和第二個實例。
// A
<ul>
<TodoItem key={1} text="First" complete={false} />
<TodoItem key={2} text="Second" complete={false} />
</ul>
// B
<ul>
<TodoItem key={0} text="Zero" complete={false} />
<TodoItem key={1} text="First" complete={false} />
<TodoItem key={2} text="Second" complete={false} />
</ul>複製代碼
不過如今,react也會提醒咱們不要忘記使用key
,若是沒有加,在瀏覽器中會報錯。
關於key
的使用咱們要注意的是,這個key值要穩定不變的,就如同身份證號之於咱們是穩定不變的同樣。
一個常見的錯誤就是,拿數組的的下標值去當作key,這個是很危險的,代碼以下,咱們必定要避免。
<ul>
{
todos.map((item, index) => {
<TodoItem
key={index}
text={item.text}
completed={item.completed}
})
}
</ul>複製代碼
mapStateToProps也被叫作selector,在store發生變化的時候就會被調用,而無論是否是selector關心的數據發生改變它都會被調用,因此若是selector計算量很是大,每次更新都從新計算可能會帶來性能問題。
Reselect能幫你省去這些不必的從新計算。
Reselect 提供 createSelector 函數來建立可記憶的 selector。
createSelector 接收一個 input-selectors 數組和一個轉換函數做爲參數。
若是 state tree 的改變會引發 input-selector 值變化,那麼 selector 會調用轉換函數,傳入 input-selectors 做爲參數,並返回結果。
若是 input-selectors 的值和前一次的同樣,它將會直接返回前一次計算的數據,而不會再調用一次轉換函數。
這樣就能夠避免沒必要要的計算,爲性能帶來提高。
例子:
import {} from 'reselect';
export const getItems = (state) => state.cart.get('items');
export const getItemsWithTotals = createSelector(
[ getItems ],
(item) => {
return items.map(i =>{
return i.set('total', i.get('price', 0) * i.get('quantity');
});
}
)複製代碼
建立一個記憶性的selector.這個意思是getItemWithTotals在第一次函數運行的時候將會進行運算.
若是同一個函數再次被調用,可是輸入值(getItems的值)沒有變化,函數將會返回一個緩存(cached)的計算結果.
若是items被修改了(例如:item添加,數量的變化,任何事情操做了getItems的結果),函數將會再次執行.
在前面的優化過程當中,咱們都是優化渲染來提升性能的,既然react
和redux
都是經過數據驅動的的方式驅動渲染過程,那麼處理優化渲染過程,獲取數據的過程也是須要考慮的一個優化點。
//下面是redux中簡單的一個篩選功能
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}複製代碼
mapStateToProps
函數做爲redux store
中獲取數據的重要一環,當咱們根據filter
和todos
來顯示相應的待辦事項的時候,咱們都要遍歷todos
字段上的數組。
當數組比較大的時候,則會下降性能。
這個時候,reselect就應運而生了,它的動做原理:只要相關的狀態沒有改變,那麼就直接使用上一次的緩存結果。
具體的用法我就不在這裏過多介紹了,已經有不少的牛人寫了相關的文章,我也不重複寫了,你們若是想深刻了解的話,能夠參考reselect的giuhub、Redux的中間件-Reselect。
ES6標準引入了import以方便咱們靜態加載模塊。形式如:
import xxx from xxx.js複製代碼
儘管import對於咱們加載模塊頗有幫助,可是靜態加載模塊的方式必定程度上限制了咱們來實現異步模塊加載。不過,目前動態加載模塊的import()語法已處於提案階段,而且webpack已將他引入並使用。import()提供了基於Promise的API,所以,import()的返回值是一個完成狀態或拒絕狀態的Promise對象。形式如:
import(/* webpackChunkName: 'module'*/ "module")
.then(() => {
//todo
})
.catch(_ => console.log('It is an error'))複製代碼
webpack在編譯時,識別到動態加載的import語法,則webpack會爲當前動態加載的模塊建立一個單獨的bundle。若是你使用的是官方的Create-react-app腳手架或React的服務端渲染框架Next.js,那麼能夠直接使用動態import語法。若是你的腳手架是你本身配置的webpack,那麼你須要按照官方指南來設置,請移步[1]。
當前最爲流行的一種方法是使用 React-loadable
[2]庫提供的懶加載React組件。它利用import()語法,使用Promise語法加載React組件。同時,React-loadable支持React的服務端渲染。 一般,咱們以以下方式實現組件:
import LazyComponet from 'LazyComponent';
export default function DemoComponent() {
return (
<div>
<p>demo component</p>
<AComponent />
</div>
)
}複製代碼
在上面的例子中,假設 LazyComponet
在 DemoComponent
渲染時咱們並不展現。可是由於咱們使用import語法將 LazyComponet
導入,因此在編譯時會將 LazyComponet
的代碼與 DemoComponent
的代碼打包到同一個bundle裏面。 可是,這並非咱們想要的。因此咱們能夠經過使用 React-loadable
來懶加載 LazyComponet
,同時將 LazyComponet
的代碼單獨打包到一個bundle裏面。咱們能夠看一下官網提供的例子:
import Loadable from 'react-loadable';
import Loading from './my-loading-component';
const LoadableComponent = Loadable({
loader: () => import('./my-component'),
loading: Loading,
});
export default class App extends React.Component {
render() {
return <LoadableComponent/>;
}
}複製代碼
從例子中咱們能夠看到,react-loadable使用動態import()方法,並將導入的組件分配給loader屬性。同時,react-loadable提供了一個loading屬性,以設置在加載組件時將展現的組件。
React.lazy and Suspense is not yet available for server-side rendering. If you want to do code-splitting in a server rendered app, we recommend Loadable Components. It has a nice guide for bundle splitting with server-side rendering.
在使用以前,咱們須要特別注意的一點是,React官方明確支持,React.lazy和Suspense並不支持服務端渲染。所以,使用服務端渲染的同窗,請繞行至 react-loadable
和 loadable-components
[3]。
因爲我是對原有項目進行的升級,所以,本文如下內容主要針對於老項目升級React最新版所作的工做。
若是你的代碼是Reactv16,那麼能夠直接升級到最新版本,固然React16.6已經提供了lazy和suspense方法。若是是v16以前,則按照官方操做遷移。
首先,按照需求,將非首屏加載的組件肯定爲懶加載組件,個人項目共肯定五個組件能夠進行懶加載。修改方式很簡單,原有引入組件的方法爲:
import LazyComponent from "../components/LazyComponent ";複製代碼
修改成:
const LazyComponent = React.lazy(() =>
import(/* webpackChunkName: 'lazyComponent'*/ "../components/LazyComponent")
);複製代碼
如代碼所示:將靜態引用組件的代碼替換爲調用React.lazy(),在lazy()傳入一個匿名函數做爲參數,在函數中動態引入 lazyComponent
組件。這樣在咱們渲染這個組件前,瀏覽器將不會下載 lazyComponent.bundle.js
文件和它的依賴。 其中,import內的webpackChunkName爲咱們定義的bundle文件名。
若是React要渲染組件時,組件依賴的代碼還沒下載好,會出現什麼狀況? <React.Suspense/>
的出現幫咱們解決問題。在代碼未下載好前,它將會渲染fallback屬性傳入的值。所以咱們的原有代碼爲:
return (
<div>
<MainComponet />
<LazyComponent />
</div>
)複製代碼
修改成:
return (
<div>
<MainComponet />
<React.Suspense fallback={<div>正在加載中</div>}>
<LazyComponent />
</React.Suspense>
</div>
)複製代碼
fallback中能夠修改成任意的spinner,本次不作過多優化。假如你不使用React.suspense,則React會給出你錯誤提示,所以記得React.lazy和React.Suspense搭配使用。 此時咱們能夠從網絡請求中看到,動態加載的lazyComponet組件被單獨打包到一個bundle裏面,然而,在首屏加載的時候,該bundle已經加載到咱們的頁面中了,這也許並非咱們想要的,咱們想要的是當咱們須要的時候再加載。接下來咱們就控制一下,當咱們須要的時候,再加載該文件。
本來我選擇的五個懶加載組件均屬於彈層性質的組件,所以必然會設置一個state來控制該組件的顯示與隱藏,所以咱們將代碼改成:
return (
<div>
<MainComponet />
{this.state.showLazyComponent && (
<React.Suspense fallback={<div>正在加載中</div>}>
<LazyComponent />
</React.Suspense>
)}
</div>
)複製代碼
由此,在首屏加載時,並未加載咱們的懶加載組件 LazyComponent
所對應的bundle包。等到咱們點擊須要該組件顯示時,頁面纔去加載該js。這便達到了咱們代碼分離並懶加載的目的。那麼咱們這麼操做,到底主bundle包的體積減小了嗎?接下來咱們打包文件看一下。
優化前打包出來的文件:
優化後打包出來的文件:
app.js
文件變小,隨之增長 lazyComponent.js
。當懶加載組件多時,咱們即可必定程度上減小首屏加載文件的大小,提升首屏的渲染速度。本實驗僅僅抽取一個組件做爲示例,若是懶加載的數量較多,足以明顯減少app.js的體積。
總結——>驗證優化的有效性
爲了驗證前面我所作的優化的有效性,我作了一組對比實驗。實驗內容爲使用puppeteer分別訪問優化前和優化後的頁面1000次,使用Performance API分別統計五項數據在這1000次訪問時的平均值。 實驗結果以下圖所示,其中:
折線圖沒法準確展現數據,所以,附表格數據以下(均爲平均耗時):
類別 | 優化後 | 優化前 |
A(request請求) |
7.01 | 7.04 |
B(解析dom樹平均) |
30.28 | 32.59 |
C(請求完畢至DOM加載) |
552.86 | 582.0 |
D(請求開始到domContentLoadedEvent結束) |
569.13 | 589.0 |
E(請求開始至load結束) |
1055.59 | 1126.94 |
從數據中咱們能夠看出,優化先後請求時間並無什麼影響,可是整體load的時間明顯縮短並立刻進入1000ms大關,可見優化後對於首屏的加載速度仍是有明顯提高。
注:因puppeteer運行1000次的過程當中,會出現網絡波動的狀況,致使有些請求的數據偏大,所以平均值並不能徹底體現正常的請求時間。但1000次的平均值足以進行優化先後的請求時間對比。
由於Performance API提供的參數有限,所以我從Chrome瀏覽器的performance summary中拿到了單次頁面請求時的參數。由於是單次數據,所以咱們不進行詳細的對比。在此列出,只爲說明優化先後瀏覽器渲染時間上哪些部分有提高。 優化前: 優化後:
另外,我從Network中發現,優化後由於頁面解析的相對以前較快,所以主接口的請求時間也相應的提早了一些。
從多項數據代表, React.lazy
和 React.Suspense
的使用必定程度上加快了首屏的渲染速度,使得咱們的頁面加載更快。 另外,當咱們想添加一個新功能而引入一個新依賴時,咱們每每會評估該依賴的大小以及引入該依賴會對原有bundle形成多大影響。假如該功能不多被用到,那麼咱們能夠痛快地使用 React.lazy
和 React.Suspense
來按需加載該功能,而無需犧牲用戶體驗了。
eact16.6加入的另一個專門用來優化 函數組件(Functional Component)性能的方法: React.memo。
import React from 'react';
class TestC extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentWillUpdate(nextProps, nextState) {
console.log('componentWillUpdate')
}
componentDidUpdate(prevProps, prevState) {
console.log('componentDidUpdate')
}
render() {
return (
<div >
{this.state.count}
<button onClick={()=>this.setState({count: 1})}>Click Me</button>
</div>
);
}
}
export default TestC;
複製代碼
TestC組件有一個本地狀態count,它的初始值是0(state = {count: 0})。當咱們點擊Click Me按鈕時,count的值被設置爲1。這時候屏幕的數字將會由0變成1。當咱們再次點擊該按鈕時,count的值仍是1, 這時候TestC組件不該該被從新渲染,但是現實是這樣的嗎?
爲了測試count重複設置相同的值組件會不會被從新渲染, 我爲TestC組件添加了兩個生命週期函數: componentWillUpdate和componentDidUpdate。componentWillUpdate方法在組件將要被從新渲染時被調用,而componentDidUpdate方法會在組件成功重渲染後被調用。
在瀏覽器中運行咱們的代碼,而後屢次點擊Click Me按鈕,你能夠看到如下輸出:
上面咱們探討了如何使用PureComponent
和shouldComponentUpdate
的方法優化類組件的性能。雖然類組件是React應用的主要組成部分,不過函數組件(Functional Component)一樣能夠被做爲React組件使用。
function TestC(props) {
return (
<div>
I am a functional component
</div>
)
}
複製代碼
對於函數組件,它們沒有諸如state的東西去保存它們本地的狀態(雖然在React Hooks中函數組件可使用useState去使用狀態), 因此咱們不能像在類組件中使用shouldComponentUpdate等生命函數去控制函數組件的重渲染。固然,咱們也不能使用extends React.PureComponent了,由於它壓根就不是一個類。
要探討解決方案,讓咱們先驗證一下函數組件是否是也有和類組件同樣的無用渲染的問題。
首先咱們先將ES6的TestC類轉換爲一個函數組件:
import React from 'react';
const TestC = (props) => {
console.log(`Rendering TestC :` props)
return (
<div>
{props.count}
</div>
)
}
export default TestC;
// App.js
<TestC count={5} />
複製代碼
當上面的代碼初次加載時,控制檯的輸出是:
一樣,咱們能夠打開Chrome的調試工具,點擊React標籤而後選中TestC組件:
咱們能夠看到這個組件的參數值是5,讓咱們將這個值改成45, 這時候瀏覽器輸出
既然函數組件也有無用渲染的問題,咱們如何對其進行優化呢?
React.memo(...)
是React v16.6引進來的新屬性。它的做用和React.PureComponent
相似,是用來控制函數組件的從新渲染的。React.memo(...)
其實就是函數組件的React.PureComponent
。
如何使用React.memo(...)?
React.memo使用起來很是簡單,假設你有如下的函數組件:
const Funcomponent = ()=> {
return (
<div>
Hiya!! I am a Funtional component
</div>
)
}
複製代碼
咱們只需將上面的Funcomponent做爲參數傳入React.memo中:
const Funcomponent = ()=> {
return (
<div>
Hiya!! I am a Funtional component
</div>
)
}
const MemodFuncComponent = React.memo(FunComponent)
複製代碼
React.memo會返回一個純化(purified)的組件MemoFuncComponent,這個組件將會在JSX標記中渲染出來。當組件的參數props和狀態state發生改變時,React將會檢查前一個狀態和參數是否和下一個狀態和參數是否相同,若是相同,組件將不會被渲染,若是不一樣,組件將會被從新渲染。
如今讓咱們在TestC組件上使用React.memo進行優化:
let TestC = (props) => {
console.log('Rendering TestC :', props)
return (
<div>
{ props.count }
</>
)
}
TestC = React.memo(TestC);
複製代碼
打開瀏覽器從新加載咱們的應用。而後打開Chrome調試工具,點擊React標籤,而後選中<Memo(TestC)>
組件。
接着編輯一下props的值,將count改成89,咱們將會看到咱們的應用被從新渲染了:
而後重複設置count的值爲89:
這就是React.memo(...)這個函數牛X的地方!
在咱們以前那個沒用到React.memo(...)
的例子中,count的重複設置會使組件進行從新渲染。但是咱們用了React.memo後,該組件在傳入的值不變的前提下是不會被從新渲染的。
高階函數,能夠傳入函數做爲參數的函數,如 map,sort,reduce。高階組件包裝了另外一個組件的組件。
由於數據是不可變的,能夠避免引用傳值的問題,但也麻煩
使用無狀態組件,只從父組件接收 props,能夠提升組件的渲染性能
const HelloWorld = (props) => <div>{props.name}</div>ReactDOM.render(<HelloWorld name="HelloWorld" />,App)複製代碼
注意應該取 nextProps,而不是 this.props
利用 bind 綁定函數,是默認有 event 這個參數的,只是這個參數在給定參數以後
handleClockClick (id, e) {console.log(id,e)}<button onClick={this.handleClockClick.bind(this, 2)}>Clock</button>複製代碼
ref="test" // this.refs.test 訪問ref={test => this.test = test} // this.test 訪問複製代碼
React 推薦使用組合,而不是繼承,組合在 UI 來的更加直觀,代碼看起來也比較容易,更符合咱們的認知,也符合 React component-base 的特性。
<MyComponent isStock/>// isStock 默認爲 true複製代碼
import { createPortal } from 'react-dom'createPortal(this.props.children, document.getElementById('portal-root'))複製代碼 |
componentDidCatch 錯誤邊界,爲組件定義一個父組件,父組件捕獲錯誤,並提供回退的 UI
componentDidCatch(error, info) {this.setState({ hasError: true });console.log(error, info)}複製代碼 |
標題相同,利用高階組件把標題渲染到不一樣的組件
這個時候有一點要注意的是,對於 Web component 應該使用 class,而不是 className
for 是 JS 的保留字,因此使用 htmlFor 替代 for
瀏覽器後綴除了ms之外,都應該以大寫字母開頭。這就是爲何WebkitTransition有一個大寫字母W。
const divStyle = {WebkitTransition: 'all', // note the capital 'W' heremsTransition: 'all' // 'ms' is the only lowercase vendor prefix};複製代碼 |
React16 依賴集合類型 Map 和 Set,在未提供原生支持的瀏覽器,須要使用一個 polyfill,例如 core-js 和 babel-polyfill
使用 core-js 支持
import 'core-js/es6/map';import 'core-js/es6/set';import React from 'react';import ReactDOM from 'react-dom';ReactDOM.render(<h1>Hello, world!</h1>,document.getElementById('root'));複製代碼 |
在 componentDidMount 請求服務器數據並利用 setState 時應注意,在組件卸載 componentWillUnmount 應該把去求去掉
const {data, type} = this.state;
// 通常方法
<Demo data={data} type={type}/>
// es6方法
<Demo {...{data, type}}/>複製代碼
// 定義子組件
const Demo = ({ prop1, prop2, ...restProps }) => (
<div>{ restProps.text}</div>
)
// 父組件使用Demo
<Demo prop1={xxx} prop2={xxx} text={xxx}/>複製代碼
// 通常改變state值的一種方式
const { data } = this.state;
this.setState({ data: {...data, key: 1 } });
// 另一種能夠經過callback的方式改變state的值
this.setState(({ data }) => ({ data: {...data, key: 1 } }));
// 還能夠
this.setState((state, props) => {
return { counter: state.counter + props.step };
});複製代碼
// React 性能優化有不少種方式,
// 那常見的一種就是在生命週期函數shouldComponentUpdate裏面判斷
// 某些值或屬性來控制組件是否從新再次渲染。
// 判斷通常的字符串,數字或者基礎的對象,數組都仍是比較好處理
// 那嵌套的對象或者數組就比較麻煩了,對於這種
// 推薦使用lodash(或者其餘的相似庫)的isEqual對嵌套數組或對象進行判斷
shouldComponentUpdate(nextProps, nextState) {
if (_.isEqual(nextState.columns, this.state.columns)) return false;
return true;
}複製代碼
React 進階提升 - 技巧篇(28 個視頻)連接
介紹 React 的一些進階知識點,一些開發上的實踐技巧,一些工具庫等。
視頻更新地址:www.qiuzhi99.com/
react 技巧 #1 如何用 netlify 雲服務部署 react 應用 65「07:14」
react 技巧 #2 把 react 應用部署到 GitHub Pages 18「05:34」
react 技巧 #3 react-router 教程 part 1 51「10:29」
react 技巧 #4 react-router 教程 part 2 11「07:39」
React 進階提升 #5 無狀態組件的最佳寫法 44「Pro」「04:52」
React 進階提升 #6 Fragment 14「Pro」「02:36」
React 進階提升 #7 context(上下文) 9「03:58」
React 進階提升 #8 高階組件 14「Pro」「02:51」
React 進階提升 #9 強大酷炫好玩的 web IDE 工具(鼠標點擊生成代碼,縮減 N 倍開發時間) 12「Pro」「08:20」
React 進階提升 #10 用高階組件來重構代碼 11「05:58」
React 進階提升 #11 我最愛的 React 庫 - 功能強大的可插入組件 (簡化代碼) 1「Pro」「04:30」
React 進階提升 #12 返回多個組件的正確方式 5「Pro」「03:07」
React 進階提升 #13 netlifyctl 一鍵部署前端應用 2「06:49」
React 進階提升 #14 defaultProps 和 類型檢查 PropTypes part 1 4「06:37」
React 進階提升 #15 類型檢查 PropTypes part 2「Pro」「09:57」
React 進階提升 #16 用 Render Props 代替 HOC(高階組件) 5「Pro」「」
React 進階提升 #17 錯誤邊界和生命週期函數 componentDidCatch 9「Pro」「11:45」
React 進階提升 #18 升級到 16.3「02:37」
React 進階提升 #19 探索 bind (this) 的寫法 9「03:50」
React 進階提升 #20 React 16.3 全新的 Context API 1「06:50」
React 進階提升 #21 React 16.3 全新的 Context API - 實踐 3「Pro」「09:19」
React 進階提升 #22 從 Redux 遷移到 React 16.3 的 Context API 之實踐 1「Pro」「11:37」
React 進階提升 #23 對象,數組,可變數據 9「Pro」「06:10」
React 進階提升 #24 React.Children API 和 props.children 講解 4「Pro」「06:06」
React 進階提升 #25 如何使用 styled-components 5「Pro」「04:56」
React 進階提升 #26 如何使用 styled-components(實踐篇)「Pro」「07:29」
React 進階提升 #27 你應該使用 redux-form(介紹) 12「Pro」「06:40」
React 進階提升 #28 你應該使用 redux-form(實踐篇) 7「Pro」「10:34」
doc.react-china.org/ 翻譯後的官方文檔,學技術必定要多看幾遍文檔
React小書 強烈推薦,由淺入深,按部就班
reactpatterns.com/ 因爲react自己 API 比較簡單,貼近原生。經過組件變化產生一系列模式
github.com/CompuIves/c… react在線編輯器,方便的分享你的react項目
js.coach 找js包的網站
基礎的免費,高級的收費 egghead.io
歡迎你們轉發交流分享
微信交流羣
微信號
微信交流羣
釘釘交流羣