React小技巧汇总

30,660 阅读41分钟

image.png

引言

使用 React.js 一段时间了,把使用过程遇到的小坑和小技巧记录下来,希望能够帮助到其他人。此文章是长篇大论你只有耐得住寂寞,禁得住诱惑才会有所成长。

image.png

一、工具篇

1、显示 html

<div dangerouslySetInnerHTML={{ __html: LANG.auth_register_tips1 }}/>

2、常用组件

  • 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

3、react-devtools 调试工具

工具地址:github.com/facebook/re…

全局安装:

yarn global add react-devtools

配置:在package.json中配置上去:

"scripts": {
"devtools": "react-devtools"
}

然后就可以启动了:yarn run devtools

image.png

二、组件通讯篇

需要组件之进行通信的几种情况

  • 父组件向子组件通信
  • 子组件向父组件通信
  • 跨级组件通信
  • 没有嵌套关系组件之间的通信
  1. redux 架构
  2. 父组件向子组件 —— props
  3. 子组件向父组件 —— props.funciton 接收参数
  4. 利用事件机制

1. 父组件向子组件通信

React数据流动是单向的,父组件向子组件通信也是最常见的;父组件通过props向子组件传递需要的信息

2. 子组件向父组件通信

  • 利用回调函数
  • 利用自定义事件机制

子组件改变父组件的state

// 一般改变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 };
});

3. 跨级组件通信

  • 层层组件传递props
例如A组件和B组件之间要进行通信,先找到A和B公共的父组件,A先向C组件通信,C组件通过props和B组件通信,此时C组件起的就是中间件的作用
  • 使用context
context是一个全局变量,像是一个大容器,在任何地方都可以访问到,我们可以把要通信的信息放在context上,然后在其他组件中可以随意取到;但是React官方不建议使用大量context,尽管他可以减少逐层传递,但是当组件结构复杂的时候,我们并不知道context是从哪里传过来的;而且context是一个全局变量,全局变量正是导致应用走向混乱的罪魁祸首.

使用context

下面例子中的组件关系: ListItem是List的子组件,List是app的子组件

ListItem.js

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.js

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.js

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

4. 没有嵌套关系的组件通信

  • 使用自定义事件机制
在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>
        );
    }
}

自定义事件是典型的发布订阅模式,通过向事件对象上添加监听器和触发事件来实现组件之间的通信

组件间通信之onRef方法

组件间通信除了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进行说明。

image.png

1、React Router中有三类组件:

  • router 组件(BrowserRouter,HashRouter)
  • route matching 组件(Route,Switch)
  • navigation 组件(Link)

基于React Router的web应用,根组件应该是一个router组件(BrowserRouterHashRouter)。 项目中,react-router-dom提供了和两种路由。两种路由都会创建一个history对象。如果我们的应用有服务器响应web的请求,我们通常使用<BrowserRouter>组件; 如果使用静态文件服务器,则我们应该使用<HashRouter>组件

2、HashRouter和BrowserRouter

其实就是路由的hash和history两种模式(要是不了解这两种模式之间的区别那就需要去恶补下啦)

并且这两个组件是路由的容器,必须在最外层

// hash模式
ReactDom.render(
  <HashRouter>
        <Route path="/" component={Home}/>
    </HashRouter>
)
// history模式
ReactDom.render(
  <BrowserRouter>
        <Route path="/" component={Home}/>
    </BrowserRouter>
)

下面说说HashRouter和BrowserRouter上的参数

  • basename 路由的基础链接,用来部署到非根目录下,比如你需要将项目部署到 www.xxxx.com/web 下,则设置basename="/web"
  • getUserConfirmation 用来拦截Prompt组件,并且决定是否跳转
  • forceRefresh 用来设置是否强制浏览器整体刷新,默认值为false
  • keLength 用来设置location.key的长度,默认是6,可以自定义

3、Route

Route是路由的一个原材料,它是控制路径对应显示的组件

Route的参数

  • path 跳转的路径
  • component 对应路径显示的组件
  • render 可以自己写render函数返回具体的dom,而不需要去设置component
  • location 传递route对象,和当前的route对象对比,如果匹配则跳转
  • exact 匹配规则,true的时候则精确匹配。
pathurl是否开启匹配结果
/a/a/bfalseyes
/a/a/btrueno
  • sensitive 是否区分path的大小写
pathurl是否开启匹配结果
/a/atrueyes
/a/Atrueyes
  • strict 是否匹配后面的/
pathurl是否开启匹配结果
/a/a/trueyes
/a/a/ctrueyes
/a/atrueno

4、Router

低级路由,适用于任何路由组件,主要和redux深度集成,使用必须配合history对象

使用Router路由的目的是和状态管理库如redux中的history同步对接

<Router history={history}>
    ...
</Router>
复制代码

5、Link和NavLink

两者都是跳转路由,NavLink的参数更多些

Link的api

  • to 有两种写法,表示跳转到哪个路由
  • 字符串写法
<Link to="/a"/>
复制代码
  • 对象写法
<Link to={{
  pathname: '/courses',
  search: '?sort=name',
  hash: '#the-hash',
  state: { fromDashboard: true }
}}/>
复制代码
  • replace 就是将push改成replace
  • innerRef 访问Link标签的dom

NavLink的api

  • Link的所有api
  • activeClassName 路由激活的时候设置的类名
  • activeStyle 路由激活设置的样式
  • exact 参考Route,符合这个条件才会激活active类
  • strict 参考Route,符合这个条件才会激活active类
  • isActive 接收一个回调函数,active状态变化的时候回触发,返回false则中断跳转
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>
复制代码
  • location 接收一个location对象,当url满足这个对象的条件才会跳转
<NavLink to="/a/123" location={{ key:"mb5wu3", pathname:"/a/123" }}/>
复制代码

6、Redirect

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>
复制代码

7、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>
复制代码

8、withRouter

当一个非路由组件也想访问到当前路由的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);
复制代码

9、history对象

用过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>

10、路由鉴权

四、性能篇

写了一段时间的react之后,渐渐的喜欢上了使用react来写应用。

我们知道,Facebook在推出react时打出的旗号之一就是高性能。

今天我们还一起来聊一聊react的性能优化,思考还能通过哪些手段来提升React的性能,使我们的react更快,性能更好。

1、react组件的性能优化(渲染角度优化)

1、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() 查看所有设计到的组件render
  • Perf.printWasted() 查看不需要的浪费组件render

大家可以在chorme中先安装React Perf扩展,然后在入口文件或者reduxstore.js中加入相应的代码即可:

image.png

再来了解一下,在最新的React16版本中,在url后加上?react_pref,就可以在chrome浏览器的performance,我们可以查看User Timeing来查看组件的加载时间。点击record开始记录,注意记录时长不要超过20s,否则可能导致chrome挂起。

image.png

使用此工具的具体操作大家可以看下图:

image.png

2、单个react组件性能优化

1、render里面尽量减少新建变量和bind函数,传递参数是尽量减少传递参数的数量。

首先我们先思考一个问题,比如我要实现一个点击按钮使相应的num增加1,我们有哪一些方法。

大家应该都能想到,无非就是三种,如下图:

image.png


第一种是在构造函数中绑定this,第二种是在render()函数里面绑定this,第三种就是使用箭头函数,都能实现上述方法;

但是哪一种方法的性能最好,是我们要考虑的问题。应该大家都知道答案:第一种的性能最好

因为第一种,构造函数每一次渲染的时候只会执行一遍;

而第二种方法,在每次render()的时候都会重新执行一遍函数;

第三种方法的话,每一次render()的时候,都会生成一个新的箭头函数,即使两个箭头函数的内容是一样的。

第三种方法我们可以举一个例子,因为react判断是否需要进行render浅层比较,简单来说就是通过===来判断的,如果state或者prop的类型是字符串或者数字,只要值相同,那么浅层比较就会认为其相同;

但是如果前者的类型是复杂的对象的时候,我们知道对象是引用类型,浅层比较只会认为这两个prop是不是同一个引用,如果不是,哪怕这两个对象中的内容完全一样,也会被认为是两个不同的prop

举个例子:

当我们给组件Foo给名为styleprop赋值;

<Foo style={{ color:"red" }}

使用这种方法,每一次渲染都会被认为是一个style这个prop发生了变化,因为每一次都会产生一个对象给style

那么我们应该如何改进,如果想要让react渲染的时候认为前后对象类型prop相同,则必须要保证prop指向同一个javascript对象,如下:

const fooStyle = { color: "red" }; //取保这个初始化只执行一次,不要放在render中,可以放在构造函数中

<Foo style={fooStyle} />

2、定制shouldComponentUpdate函数

shouldComponentUpdate是决定react组件什么时候能够不重新渲染的函数,但是这个函数默认的实现方式就是简单的返回一个true。也就是说,默认每次更新的时候都要调用所用的生命周期函数,包括render函数,重新渲染。

我们来看一下下面的一个例子

image.png

我们写两个组件,AppDemo组件,并写两个方法,一个改变App中的num的值,一个是改变title,我们在Demo的render中打印render函数。我们可以看到以下的效果:

image.png

我们可以清晰的看到虽然demo组件里的title值没有改变,但是还是render了。

为了解决这个问题,我们可以对demo组件进行如下的修改:

image.png

只有当demo的title值发生改变的时候,我们才去render,我们可以看一下效果:

image.png

以上只是一个特别简单的一个对于shouldComponentUpdate的定制。

在最新的react中,react给我们提供了React.PureComponent,官方也在早期提供了名为react-addons-pure-render-mixin插件来重新实现shouldComponentUpdate生命周期方法。

image.png

通过上述的方法的效果也是和我们定制shouldComponentUpdate的效果是一致的。

但是我们要注意的是,这里的PureRender是浅比较的,因为深比较的场景是相当昂贵的。所以我们要注意我们在1.1中说到的一些注意点:不要直接为props设置对象或者数组不要将方法直接绑定在元素上,因为其实函数也是对象

3、多个react组件性能优化,key的优化

react组件在装载过程中,react通过在render方法在内存中产生一个树形结构,树上的节点代表一个react组件或者原生的Dom元素,这个树形结构就是我们所谓的Vitural Dom,react根据这个来渲染产生浏览器的Dom树。

react在更新阶段对比原有的Vitural Dom和新生成的Vitural Dom,找出不同之处,在根据不同来渲染Dom树。

react为了追求高性能,采用了时间复杂度为O(N)来比较两个属性结构的区别,因为要确切比较两个树形结构,需要通过O(N^3),这会降低性能。

简单来说,react利用key来识别组件,它是一种身份标识标识,就像我们的身份证用来辨识一个人一样。

列表的 key

  • key 不会传给组件,如果需要使用 key 的值,另外命名为其他属性传进去
  • 当一个列表顺序会重排时,不适合使用数组的索引作为 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会执行),然后重新创建该组件(有状态组件的constructorcomponentWillUnmount都会执行)

我们举几个情况,大家就会马上理解:

  • 节点类型不同
// 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组件的区别是文字、classNamestyle中的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,如果没有加,在浏览器中会报错。

image.png关于key的使用我们要注意的是,这个key值要稳定不变的,就如同身份证号之于我们是稳定不变的一样。

一个常见的错误就是,拿数组的的下标值去当做key,这个是很危险的,代码如下,我们一定要避免。

<ul>
  {
        todos.map((item, index) => {
            <TodoItem
              key={index}
              text={item.text}
              completed={item.completed}
        })
  }
</ul>

2、redux性能优化:reselect(数据获取时优化)

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的结果),函数将会再次执行.

在前面的优化过程中,我们都是优化渲染来提高性能的,既然reactredux都是通过数据驱动的的方式驱动渲染过程,那么处理优化渲染过程,获取数据的过程也是需要考虑的一个优化点。

//下面是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中获取数据的重要一环,当我们根据filtertodos来显示相应的待办事项的时候,我们都要遍历todos字段上的数组。

当数组比较大的时候,则会降低性能。

这个时候,reselect就应运而生了,它的动作原理:只要相关的状态没有改变,那么就直接使用上一次的缓存结果。

具体的用法我就不在这里过多介绍了,已经有很多的牛人写了相关的文章,我也不重复写了,大家如果想深入了解的话,可以参考reselect的giuhubRedux的中间件-Reselect

3、分隔代码

1、动态加载

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]。

2、动态加载React组件

当前最为流行的一种方法是使用 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>
    )
}

在上面的例子中,假设 LazyComponetDemoComponent 渲染时我们并不展示。但是因为我们使用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属性,以设置在加载组件时将展示的组件。

3、lazy和suspense的使用

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-loadableloadable-components [3]。

由于我是对原有项目进行的升级,因此,本文以下内容主要针对于老项目升级React最新版所做的工作。

1、代码升级React最新版

如果你的代码是Reactv16,那么可以直接升级到最新版本,当然React16.6已经提供了lazy和suspense方法。如果是v16之前,则按照官方操作迁移。

2、确定原有代码的懒加载组件

首先,按照需求,将非首屏加载的组件确定为懒加载组件,我的项目共确定五个组件可以进行懒加载。修改方式很简单,原有引入组件的方法为:

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已经加载到我们的页面中了,这也许并不是我们想要的,我们想要的是当我们需要的时候再加载。接下来我们就控制一下,当我们需要的时候,再加载该文件。

3、通过变量控制加载

原本我选择的五个懒加载组件均属于弹层性质的组件,因此必然会设置一个state来控制该组件的显示与隐藏,因此我们将代码改为:

return (
        <div>
            <MainComponet />
            {this.state.showLazyComponent && (
                <React.Suspense fallback={<div>正在加载中</div>}>
                    <LazyComponent />
                </React.Suspense>
            )}
        </div>
    )

由此,在首屏加载时,并未加载我们的懒加载组件 LazyComponent 所对应的bundle包。等到我们点击需要该组件显示时,页面才去加载该js。这便达到了我们代码分离并懒加载的目的。那么我们这么操作,到底主bundle包的体积减少了吗?接下来我们打包文件看一下。

4、打包文件

优化前打包出来的文件:

image.png

优化后打包出来的文件:

image.png

app.js 文件变小,随之增加 lazyComponent.js 。当懒加载组件多时,我们便可一定程度上减少首屏加载文件的大小,提高首屏的渲染速度。本实验仅仅抽取一个组件作为示例,如果懒加载的数量较多,足以明显减小app.js的体积。

总结——>验证优化的有效性

5、[验证优化的时效性] 利用Puppeteer和Performance API做对比

为了验证前面我所做的优化的有效性,我做了一组对比实验。实验内容为使用puppeteer分别访问优化前和优化后的页面1000次,使用Performance API分别统计五项数据在这1000次访问时的平均值。 实验结果如下图所示,其中:

  • A为request请求平均耗时
  • B为解析dom树耗时平均耗时
  • C为请求完毕至DOM加载平均耗时
  • D为请求开始到domContentLoadedEvent结束平均耗时
  • E为请求开始至load平均耗时

image.png


折线图无法准确展示数据,因此,附表格数据如下(均为平均耗时):

类别优化后优化前

A(request请求)

7.017.04

B(解析dom树平均)

30.2832.59

C(请求完毕至DOM加载)

552.86582.0

D(请求开始到domContentLoadedEvent结束)

569.13589.0

E(请求开始至load结束)

1055.591126.94

从数据中我们可以看出,优化前后请求时间并没有什么影响,但是总体load的时间明显缩短并马上进入1000ms大关,可见优化后对于首屏的加载速度还是有明显提升。

注:因puppeteer运行1000次的过程中,会出现网络波动的情况,导致有些请求的数据偏大,因此平均值并不能完全体现正常的请求时间。但1000次的平均值足以进行优化前后的请求时间对比。

6、[验证优化的时效性] 利用Chorme Performance 参数做对比

因为Performance API提供的参数有限,因此我从Chrome浏览器的performance summary中拿到了单次页面请求时的参数。因为是单次数据,因此我们不进行详细的对比。在此列出,只为说明优化前后浏览器渲染时间上哪些部分有提升。 优化前: 优化后:

image.png image.png

  • 蓝色:加载(Loading)时间降低
  • 黄色:脚本运算(Scripting)时间降低
  • 紫色:渲染(Rendering)时间降低
  • 绿色:绘制(Painting)时间持平
  • 灰色:其他(Other)时间降低
  • 闲置:浏览器空闲时间降低

另外,我从Network中发现,优化后因为页面解析的相对之前较快,因此主接口的请求时间也相应的提前了一些。

4、总结

从多项数据表明, React.lazyReact.Suspense 的使用一定程度上加快了首屏的渲染速度,使得我们的页面加载更快。 另外,当我们想添加一个新功能而引入一个新依赖时,我们往往会评估该依赖的大小以及引入该依赖会对原有bundle造成多大影响。假如该功能很少被用到,那么我们可以痛快地使用 React.lazyReact.Suspense 来按需加载该功能,而无需牺牲用户体验了。

4、使用React.memo()来优化函数组件的性能

eact16.6加入的另外一个专门用来优化函数组件(Functional Component)性能的方法: React.memo

1、无用的渲染

组件是构成React视图的一个基本单元。有些组件会有自己本地的状态(state), 当它们的值由于用户的操作而发生改变时,组件就会重新渲染。在一个React应用中,一个组件可能会被频繁地进行渲染。这些渲染虽然有一小部分是必须的,不过大多数都是无用的,它们的存在会大大降低我们应用的性能。
看下面这个例子:

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按钮,你可以看到以下输出:


我们可以看到'componentWillUpdate'和'componentWillUpdate'在每次我们点击完按钮后,都会在控制台输出来。所以即使count被设置相同的值,TestC组件还是会被重新渲染,这些就是所谓的无用渲染。

2、函数组件

上面我们探讨了如何使用PureComponentshouldComponentUpdate的方法优化类组件的性能。虽然类组件是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, 这时候浏览器输出


由于count的值改变了,所以该组件也被重新渲染了,控制台输出Object{count: 45},让我们重复设置count的值为45, 然后再看一下控制台的输出结果:

由输出结果可以看出,即使count的值保持不变,还是45, 该组件还是被重渲染了。

既然函数组件也有无用渲染的问题,我们如何对其进行优化呢?

3、解决方案: 使用React.memo()

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后,该组件在传入的值不变的前提下是不会被重新渲染的。

高阶组件(HOC)

高阶函数,可以传入函数作为参数的函数,如 map,sort,reduce。高阶组件包装了另一个组件的组件。

  1. 属性代理 (Props Proxy)
  2. 反向继承 (Inheritance Inversion)

Immutable.js

因为数据是不可变的,可以避免引用传值的问题,但也麻烦

无状态组件

使用无状态组件,只从父组件接收 props,可以提高组件的渲染性能

const HelloWorld = (props) => <div>{props.name}</div>ReactDOM.render(<HelloWorld name="HelloWorld" />,App)

componentWillReceiveProps 中取 props 的值

注意应该取 nextProps,而不是 this.props

bind 绑定函数

利用 bind 绑定函数,是默认有 event 这个参数的,只是这个参数在给定参数之后

handleClockClick (id, e) {console.log(id,e)}<button onClick={this.handleClockClick.bind(this, 2)}>Clock</button>

ES6 类中,函数 this 不默认指向 对象

获取元素

  • this.getDomNode 已经在低版本被移除了,现在设置 ref=xxx,然后使用 this.refs.xxx 访问 DOM 元素
  • ref 可以赋值两种类型,一种是字符串,一种是函数, 字符串只能用在类组件,DOM 元素使用函数,纯函数组件不能使用 ref。旧版本 DOM 元素虽然可以使用 ref,但是 React 已不推荐。

ref="test" // this.refs.test 访问ref={test => this.test = test} // this.test 访问

组合 VS 继承

React 推荐使用组合,而不是继承,组合在 UI 来的更加直观,代码看起来也比较容易,更符合我们的认知,也符合 React component-base 的特性。

当只写属性名时,默认值为 true

<MyComponent isStock/>// isStock 默认为 true

createPortal

  • 将子节点挂载到父组件以外组件的方法,在 render 方法中使用,不能挂载到父组件,因为此时父组件都没有渲染好,无法获取到 DOM
  • 行为上跟其他子节点没有区别,因为 React DOM 树上依然在这个组件上,包括事件冒泡等东西
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)}
  • 无法捕获的错误
    事件处理
    异步代码 (例如 setTimeout 或 requestAnimationFrame 回调函数)
    服务端渲染
    错误边界自身抛出来的错误 (而不是其子组件)

高级组件就是函数

  • 不应该在高阶组件修改原组件的属性
  • 利用函数包裹组件,返回一个新的组件
  • 为组件切换不同的数据源
  1. Showcase 组件,利用 getData (Showcase, data)函数包裹,获取不同数据
  • 不要在 render 中使用高阶组件
    因为每一次挂载组件,都会重新获取一个高阶组件的实例
  • hoistNonReactStatic
    将原始组件的静态方法拷贝到包裹组件中

容器组件

  • 处理数据订阅和状态管理
  • 高阶组件是参数化的容器组件

rander prop

标题相同,利用高阶组件把标题渲染到不同的组件

React 中使用 Web component

这个时候有一点要注意的是,对于 Web component 应该使用 class,而不是 className

lable 的 for

for 是 JS 的保留字,所以使用 htmlFor 替代 for

style属性

浏览器后缀除了ms以外,都应该以大写字母开头。这就是为什么WebkitTransition有一个大写字母W。

const divStyle = {WebkitTransition: 'all', // note the capital 'W' heremsTransition: 'all' // 'ms' is the only lowercase vendor prefix};

在 IE11 以下使用 React16

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 请求服务器数据

在 componentDidMount 请求服务器数据并利用 setState 时应注意,在组件卸载 componentWillUnmount 应该把去求去掉

使用es6新特性传递组件props

const {data, type} = this.state;
// 一般方法
<Demo data={data} type={type}/>
// es6方法
<Demo {...{data, type}}/>

3、 利用es6 rest 参数(形式为...变量名)传递可变数量的props

// 定义子组件
const Demo = ({ prop1, prop2, ...restProps }) => (
<div>{ restProps.text}</div>
)
// 父组件使用Demo
<Demo prop1={xxx} prop2={xxx} text={xxx}/>

4、setState的其他用法

// 一般改变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 };
});

5、React 性能优化

// 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 进阶提高 - 技巧篇

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项目

image.png

devhints.io/react

image.png

js.coach 找js包的网站

image.png

视频

基础的免费,高级的收费 egghead.io