更新流程

我们知道React的在实际渲染的时候,会将对应的JSX转换为React.createElement方法
通过调用React.createElement方法会得到ReactElement对象,而这个对象就是虚拟DOM对象
随后ReactDOM会根据不同的平台进行渲染,例如在web端,会将虚拟DOM渲染为浏览器原生真实DOM

React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树
React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI
如果一个节点需要和另一个树上的所有节点进行比较,也就是进行完全比较更新
那么即使是最先进的算法,该算法的复杂程度为 O(n^3^),其中 n 是树中元素的数量
如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围
这个开销太过昂贵了,React的更新性能会变得非常低效
所以React对节点比较采用了以下限制,以保证React的diff算法的时间复杂度为O(n)
- 同层节点之间相互比较,不会跨节点比较
- 不同类型的节点,产生不同的树结构
- 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定,也就是不需要重新进行渲染
key
默认情况下,react在比较两个节点的时候,是通过如下方式确定两个节点是同一个节点的
- 两个节点的元素类型是一致的
- 两个节点的元素内容是一致的
这种比较方式在列表末尾插入数据的时候,并没有什么问题
但是如果在列表中间插入或移除对于元素的时候,使用以上的比较方式
react并没有办法很好的识别出节点的移动变化
所以为了避免这种情况,React推荐为列表渲染的每一项设置一个key作为节点的唯一标识
React 使用 key 来匹配原有树上的子元素以及最新树上的子元素是否需要更新
React可以通过key来更好的识别新旧节点,并尽可能的移动复用对应的元素,从而以最小代价去更新dom元素
所以key一般需要满足如下要求:
-
key类似于元素的id,是diff算法中对于元素的唯一标识,所以
key的值应该是唯一的 -
在diff算法过程中,通过key来进行元素识别,所以需要在新旧dom中保持key的稳定
也就是说key应该是保持不变的,因此不应该使用随机数或索引值来作为元素的key
scu
默认情况下,只要父组件中通过setState改变了对应的状态,那么就会重新调用render方法
那么父组件和其对应的所有子孙组件都需要执行对应的render函数,生成对应的vDOM
事实上,很多的组件没有必须要重新render
它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法
React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有 返回值
- 参数一:nextProps 修改之后,最新的props属性
- 参数二:nextState 修改之后,最新的state属性
该方法返回值是一个boolean类型:
- 返回值为true,那么就需要调用render方法
- 返回值为false,那么就不需要调用render方法
- 默认返回的是true,也就是只要state发生改变,就会调用render方法
父组件
export class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
msg: 'Hello Wolrd',
count: 0
}
}
changeMsg() {
this.setState({
msg: 'Hello React'
})
}
changeCount() {
this.setState({
count: this.state.count + 1
})
}
render() {
return (
<div>
<Home msg={this.state.msg} />
<div>{ this.state.count }</div>
<button onClick={ () => this.changeMsg() }>change msg</button>
<button onClick={ () => this.changeCount() }>change count</button>
</div>
)
}
}
子组件
import React, { Component } from 'react'
export class Home extends Component {
// 虽然Home只依赖了父组件中的msg,但是默认情况下
// 只要父组件的state发生了改变,无论是count还是msg
// 对应的子组件都需要重新进行渲染
// 所以我们需要手动实现shouldComponentUpdate方法
shouldComponentUpdate(nextProps, nextState) {
if (this.props.msg === nextProps.msg) {
return false
}
return true
}
render() {
console.log('Home Render')
return (
<div>
{ this.props.msg }
</div>
)
}
}
export default Home
PureComponent
如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量
我们所需要做的仅仅只是通过判断props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false
事实上React已经考虑到了这一点, 当我们一个类组件继承自PureComponent的时候,就默认已经实现了对应的PureComponent
import React, { PureComponent } from 'react'
export class Home extends PureComponent {
render() {
console.log('Home Render')
return (
<div>
{ this.props.msg }
</div>
)
}
}
export default Home
memo
针对类组件可以使用PureComponent,那么函数式组件呢
事实上函数式组件我们在props没有改变时,也是不希望其重新渲染其DOM树结构的
这个时候,就可以使用高阶函数memo
import React, { memo } from 'react'
// memo是一个高阶组件,也就是接受组件作为组件的参数并返回一个新的组件
const Home = memo((props) => {
console.log('Home Render')
return (
<div>{props.msg}</div>
)
})
export default Home
数据不可变性
可变数据(mutable)即一个数据被创建之后,可以随时进行修改,修改之后会影响到原值,JavaScript中默认的数据都是可变数据
const user = {
name: 'Klaus'
}
const user1 = user
user.name = 'Alex'
console.log(user.name) // => Alex
console.log(user1.name) // => Alex
不可变数据(Immutable) 就是一旦创建,就不能再被更改的数据。对Immutable对象的任何修改或添加删除操作都会返回一个新的 Immutable对象
const user = {
name: 'Klaus'
}
const user1 = { ...user }
user.name = 'Alex'
console.log(user.name) // => Klaus
console.log(user1.name) // => Alex
不可变数据的特点
-
相比可以随意修改的数据而言,不可变数据更容易追踪,调试和回溯
-
每次对于不可变数据的修改都是新产生了一个对象,并不会修改原始对象
此时就可以将各个阶段的数据存储起来,从而使不可变数据更容易追踪,调试和回溯
-
-
不可变数据可以让复杂的变化检测机制得以简单快速的实现,从而确保代价高昂的DOM更新过程只在真正需要的时候进行
-
比较两个可变引用数据类型是否相同,需要逐个遍历其中的元素进行比较
-
而比较两个不可变引用类型是否相同,直接对比两个对象对应的引用值是否相同即可
-
所以为了提升react在更新组件时候的性能,我们需要将组件继承自PureComponent或使用memo函数进行包裹
而他们在判断shouldComponentUpdate方法的返回值的时候,是对新旧props和新旧state进行了浅层比较
所以如果我们要修改某一个值为对象的state或props的时候,需要对原始值进行拷贝后,修改拷贝后的值,而不是直接修改原始值
如果直接修改原始值,那么在PureComponent或memo进行scu的时候,因为对应值的引用地址没有发生改变,所以SCU方法会返回false,对应的界面也就不会再次重新渲染(re-render)
ref
获取DOM
在React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素,但是某些特殊的情况,确实需要获取到DOM进行某些操作
方式一: 传入字符串
export class App extends PureComponent {
render() {
return (
<div>
<div ref="divEl">div 元素</div>
<button onClick={() => this.getEl()}>click me</button>
</div>
)
}
getEl() {
// 使用字符串获取对应的元素 已经过时
console.log(this.refs.divEl)
}
}
方式二: 传入函数
import React, { PureComponent } from 'react'
export class App extends PureComponent {
constructor(props) {
super(props)
// 因为divEl 并不参与页面的渲染过程
// 在divEl对应的值发生改变的过程中,页面并不需要发生对应的改变
// 所以并不需要定义在state中
this.divEl = null
}
render() {
return (
<div>
{/*
如果ref被绑定到了元素上,且对应值是一个回调函数的时候
会将原生dom对象以参数形式传递进来
*/}
<div ref={ el => this.divEl = el}>div 元素</div>
<button onClick={() => this.getEl()}>click me</button>
</div>
)
}
getEl() {
console.log(this.divEl)
}
}
export default App
方式三:传入一个对象 - 推荐使用的方式
import React, { PureComponent, createRef } from 'react'
export class App extends PureComponent {
constructor(props) {
super(props)
// 创建ref对象
this.divEl = createRef()
}
render() {
return (
<div>
<div ref={this.divEl}>div 元素</div>
<button onClick={() => this.getEl()}>click me</button>
</div>
)
}
getEl() {
// 对应的dom元素会存放在ref对象的current属性中
console.log(this.divEl.current)
}
}
export default App
获取组件
类组件
import React, { PureComponent, createRef } from 'react'
import Foo from './components/Foo'
export class App extends PureComponent {
constructor(props) {
super(props)
// 在获取类组件实例的时候,其绑定方式和获取普通dom的绑定方式是一致的
this.fooEl = createRef()
}
render() {
return (
<div>
<Foo ref={ this.fooEl } />
<button onClick={() => this.getEl()}>click me</button>
</div>
)
}
getEl() {
console.log(this.fooEl)
}
}
export default App
函数组件
- 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
- 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
- 你不能在函数组件上使用 ref 属性,因为他们没有实例
但是某些时候,我们可能想要获取函数式组件中的某个DOM元素,那么就可以使用React.forwardRef这个高阶组件进行ref的转发
父组件
import React, { PureComponent, createRef } from 'react'
import Home from './components/Home'
export class App extends PureComponent {
constructor(props) {
super(props)
this.homeEl = createRef()
}
render() {
return (
<div>
{/*
对于函数式组件而言,函数组件没有对应的实例
所以并不可以和类组件一样获取对应的组件实例
此时我们只能通过forwardRef进行ref的转发
让其绑定到函数组件中的某一个HTML元素上
*/}
<Home ref={ this.homeEl } />
<button onClick={() => this.getEl()}>click me</button>
</div>
)
}
getEl() {
console.log(this.homeEl.current)
}
}
export default App
子组件
import React, { forwardRef, memo } from 'react'
// forwardRef接收一个组件作为参数,并返回一个新的组件
// 在新的组件中,可以接收第二个参数ref
// 对应的值就是函数组件上,外部传入的ref对象
// 我们可以将传入的ref绑定到任意一个HTML元素上
// ps: 如果同时存在memo组件和forwardRef组件的时候
// 请使用memo组件包裹forwardRef组件
const Home = memo(forwardRef((props, ref) => {
return (
<div>
<h1>{ props.msg }</h1>
<p ref={ref}>p in homt compo </p>
</div>
)
}))
export default Home