使用 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
欢迎你们转发交流分享
微信交流群
微信号
微信交流群
钉钉交流群