从 0 到 1 实现 React 系列 —— 5.PureComponent 实现 && HOC 探幽
本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/...) 项目地址
- 从 0 到 1 实现 React 系列 —— JSX 和 Virtual DOM
- 从 0 到 1 实现 React 系列 —— 组件和 state|props
- 从 0 到 1 实现 React 系列 —— 生命周期和 diff 算法
- 从 0 到 1 实现 React 系列 —— 优化 setState 和 ref 的实现
- 从 0 到 1 实现 React 系列 —— PureComponent 实现 && HOC 探幽
PureComponent 精髓
使用 PureComponent 是优化 React 性能的一种常用手段,相较于 Component, PureComponent 会在 render 之前自动执行一次 shouldComponentUpdate() 函数,根据返回的 bool 值判断是否进行 render。其中有个重点是 PureComponent 在 shouldComponentUpdate() 的时候会进行 shallowEqual(浅比较)。
PureComponent 的浅比较策略如下:
对 prevState/nextState 以及 prevProps/nextProps 这两组数据进行浅比较:
1.对象第一层数据未发生改变,render 方法不会触发;
2.对象第一层数据发生改变(包括第一层数据引用的改变),render 方法会触发;
PureComponent 的实现
照着上述思路我们来实现 PureComponent 的逻辑
function PureComponent(props) {
this.props = props || {}
this.state = {}
isShouldComponentUpdate.call(this) // 为每个 PureComponent 绑定 shouldComponentUpdate 方法
}
PureComponent.prototype.setState = function(updater, cb) {
isShouldComponentUpdate.call(this) // 调用 setState 时,让 this 指向子类的实例,目的取到子类的 this.state
asyncRender(updater, this, cb)
}
function isShouldComponentUpdate() {
const cpState = this.state
const cpProps = this.props
this.shouldComponentUpdate = function (nextProps, nextState) {
if (!shallowEqual(cpState, nextState) || !shallowEqual(cpProps, nextProps)) {
return true // 只要 state 或 props 浅比较不等的话,就进行渲染
} else {
return false // 浅比较相等的话,不渲染
}
}
}
// 浅比较逻辑
const shallowEqual = function(oldState, nextState) {
const oldKeys = Object.keys(oldState)
const newKeys = Object.keys(nextState)
if (oldKeys.length !== newKeys.length) {
return false
}
let flag = true
for (let i = 0; i < oldKeys.length; i++) {
if (!nextState.hasOwnProperty(oldKeys[i])) {
flag = false
break
}
if (nextState[oldKeys[i]] !== oldState[oldKeys[i]]) {
flag = false
break
}
}
return flag
}
测试用例
测试用例用 在 React 上提的一个 issue 中的案例,我们期望点击增加按钮后,页面上显示的值能够加 1。
class B extends PureComponent {
constructor(props) {
super(props)
this.state = {
count: 0
}
this.click = this.click.bind(this)
}
click() {
this.setState({
count: ++this.state.count,
})
}
render() {
return (
<div>
<button onClick={this.click}>增加</button>
<div>{this.state.count}</div>
</div>
)
}
}
然而,我们点击上述代码,页面上显示的 0 分毫不动!!!
揭秘如下:
click() {
const t = ++this.state.count
console.log(t === this.state.count) // true
this.setState({
count: t,
})
}
当点击增加按钮,控制台显示 t === this.state.count 为 true, 也就说明了 setState 前后的状态是统一的,所以 shallowEqual(浅比较) 返回的是 true,致使 shouldComponentUpdate 返回了 false,页面因此没有渲染。
类似的,如下写法也是达不到目标的,留给读者思考了。
click() {
this.setState({
count: this.state.count++,
})
}
那么如何达到我们期望的目标呢。揭秘如下:
click() {
this.setState({
count: this.state.count + 1
})
}
感悟:小小的一行代码里蕴藏着无数的 bug。
HOC 实践
高阶组件(Higher Order Component) 不属于 React API 范畴,但是它在 React 中也是一种实用的技术,它可以将常见任务抽象成一个可重用的部分。这个小节算是番外篇,会结合 cpreact(前文实现的类 react 轮子) 与 HOC 进行相关的实践。
它可以用如下公式表示:
y = f(x),
// x:原有组件
// y:高阶组件
// f():
f() 的实现有两种方法,下面进行实践。
属性代理(Props Proxy)
这类实现也是装饰器模式的一种运用,通过装饰器函数给原来函数赋能。下面例子在装饰器函数中给被装饰的组件传递了额外的属性 { a: 1, b: 2 }。
声明:下文所展示的 demo 均已在 cpreact 测试通过
function ppHOC(WrappedComponent) {
return class extends Component {
render() {
const obj = { a: 1, b: 2 }
return (
<WrappedComponent { ...this.props } { ...obj } />
)
}
}
}
@ppHOC
class B extends Component {
render() {
return (
<div>
{ this.props.a + this.props.b } { /* 输出 3 */ }
</div>
)
}
}
要是将 { a: 1, b: 2 } 替换成全局共享对象,那么不就是 react-redux 中的 Connect 了么?
改进上述 demo,我们就可以实现可插拔的受控组件,代码示意如下:
function ppDecorate(WrappedComponent) {
return class extends Component {
constructor() {
super()
this.state = {
value: ''
}
this.onChange = this.onChange.bind(this)
}
onChange(e) {
this.setState({
value: e.target.value
})
}
render() {
const obj = {
onChange: this.onChange,
value: this.state.value,
}
return (
<WrappedComponent { ...this.props } { ...obj } />
)
}
}
}
@ppDecorate
class B extends Component {
render() {
return (
<div>
<input { ...this.props } />
<div>{ this.props.value }</div>
</div>
)
}
}
效果如下图:

这里有个坑点,当我们在输入框输入字符的时候,并不会立马触发 onChange 事件(我们想要让事件立即触发,然而现在要按下回车键或者点下鼠标才触发),在 react 中有个合成事件 的知识点,下篇文章会进行探究。
顺带一提在这个 demo 中似乎看到了双向绑定的效果,但是实际中 React 并没有双向绑定的概念,但是我们可以运用 HOC 的知识点结合 setState 在 React 表单中实现伪双向绑定的效果。
继承反转(Inheritance Inversion)
继承反转的核心是:传入 HOC 的组件会作为返回类的父类来使用。然后在 render 中调用 super.render() 来调用父类的 render 方法。
在 《ES6 继承与 ES5 继承的差异》中我们提到了作为对象使用的 super 指向父类的实例。
function iiHOC(WrappedComponent) {
return class extends WrappedComponent {
render() {
const parentRender = super.render()
if (parentRender.nodeName === 'span') {
return (
<span>继承反转</span>
)
}
}
}
}
@iiHOC
class B extends Component {
render() {
return (
<span>Inheritance Inversion</span>
)
}
}
在这个 demo 中,在 HOC 内实现了渲染劫持,页面上最终显示如下:

可能会有疑惑,使用
属性代理的方式貌似也能实现渲染劫持呀,但是那样做没有继承反转这种方式纯粹。
鸣谢
Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react
相关链接
- A doubt behaviour using the PureComponent
- React 的性能优化(一)当 PureComponent 遇上 ImmutableJS
- React性能优化方案之PureComponent
- 带着三个问题深入浅出React高阶组件
- 深入理解 React 高阶组件
从 0 到 1 实现 React 系列 —— 5.PureComponent 实现 && HOC 探幽的更多相关文章
- 从 0 到 1 实现 React 系列 —— 1.JSX 和 Virtual DOM
看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...
- 从 0 到 1 实现 React 系列 —— 4.setState优化和ref的实现
看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...
- 从 0 到 1 实现 React 系列 —— 3.生命周期和 diff 算法
看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...
- 从 0 到 1 实现 React 系列 —— 2.组件和 state|props
看源码一个痛处是会陷进理不顺主干的困局中,本系列文章在实现一个 (x)react 的同时理顺 React 框架的主干内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/ref/. ...
- 从0到1用react+antd+redux搭建一个开箱即用的企业级管理后台系列(基础篇)
背景 最近因为要做一个新的管理后台项目,新公司大部分是用vue写的,技术栈这块也是想切到react上面来,所以,这次从0到1重新搭建一个react项目架子,需要考虑的东西的很多,包括目录结构.代码 ...
- React 系列教程 1:实现 Animate.css 官网效果
前言 这是 React 系列教程的第一篇,我们将用 React 实现 Animate.css 官网的效果.对于 Animate.css 官网效果是一个非常简单的例子,原代码使用 jQuery 编写,就 ...
- react系列笔记1 用npx npm命令创建react app
react系列笔记1 用npx npm命令创建react app create-react-app my-app是开始构建新的 React 单页应用程序的最佳方式.它已经为你设置好了开发环境,以便您可 ...
- 用SignalR 2.0开发客服系统[系列2:实现聊天室]
前言 交流群:195866844 上周发表了 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 这篇文章,得到了很多帮助和鼓励,小弟在此真心的感谢大家的支持.. 这周继续系列2,实现聊天室 ...
- 用SignalR 2.0开发客服系统[系列3:实现点对点通讯]
前言 交流群:195866844 目录: 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 用SignalR 2.0开发客服系统[系列2:实现聊天室] 真的很感谢大家的支持,今天发表系列3 ...
随机推荐
- 远程连接MySQL数据库问题总结
远程连接MySQL数据库时,陆陆续续遇到了一些杂七杂八的问题,相信很多人也曾经遇到过这类问题,下面总结归纳在下面,方便以后直接查找. 1:出现ERROR 2003 (HY000): Can't con ...
- mssql 一次向表中插入多条数据的方法分享 (转自:http://www.maomao365.com/?p=6058)
转自:http://www.maomao365.com/?p=6058) <span style="font-size:16px;font-weight:bold;"> ...
- 想起以前写的一个爬虫,然后就用C#WinForm写了一个下载小说的软件,比较简单
本软件本是练习.讨论爬虫技术所用.如果侵犯了您的利益请联系我,我会立即删除! 小工具安装包: 百度网盘链接:https://pan.baidu.com/s/1m_OuEBOEE47kYaXq5fwpI ...
- Win10 - MySQL-zip安装方法
Win10 - MySQL-zip安装方法 安装步骤 1.下载,到MySQL官网:https://dev.mysql.com/downloads/mysql/ 2.解压安装包 解压下载的安装包,放到你 ...
- Linux 小知识翻译 - 「日志」(log)
这次聊聊「日志」. 「日志」主要指系统或者软件留下的「记录」.出自表示「航海日志」的「logbook」. 经常听说「出现问题的时候,或者程序没有安装自己预期的来运行的时候,请看看日志!」. 确实,记录 ...
- CISCO ACL配置
ACL:access(访问)control(控制)list(列表),用来实现防火墙规则. 访问控制列表的原理对路由器接口来说有两个方向出:已经经路由器的处理,正离开路由器接口的数据包入:已经到达路由器 ...
- S/4 HANA中的MATDOC和MATDOC_EXTRACT
最近做了销售和物料管理方面的一些需求,因此对S/4中的MM的数据模型有了一定的了解.目前网络已经有一些介绍物料凭证表MATDOC的文章,内容好像不是很详细,另外也没发现介绍库存变更数据表MATDOC_ ...
- 如何指定一个计划和目标——6W
作为一名IT人员,需要自己指定一个计划和目标,来保证完成进度和高效的完成任务. 参考管理学如何制作计划和目标的,套用过来,也是同样适用的.来先看看管理学的相关知识吧. 计划的概念:计划是为实现组织目标 ...
- keepalived脑裂问题查找
在自己环境做keepalived+redis实验时,当重启了备用redies机器后,发现两台redies主机都拿到了VIP [root@redis2 ~]# ip addr list 1: lo: & ...
- C. Meaningless Operations Codeforces Global Round 1 异或与运算,思维题
C. Meaningless Operations time limit per test 1 second memory limit per test 256 megabytes input sta ...