React18 - 组件化开发 (三)

339 阅读9分钟

更新流程

image.png

我们知道React的在实际渲染的时候,会将对应的JSX转换为React.createElement方法

通过调用React.createElement方法会得到ReactElement对象,而这个对象就是虚拟DOM对象

随后ReactDOM会根据不同的平台进行渲染,例如在web端,会将虚拟DOM渲染为浏览器原生真实DOM

image.png

React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树

React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI

如果一个节点需要和另一个树上的所有节点进行比较,也就是进行完全比较更新

那么即使是最先进的算法,该算法的复杂程度为 O(n^3^),其中 n 是树中元素的数量

如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围

这个开销太过昂贵了,React的更新性能会变得非常低效

所以React对节点比较采用了以下限制,以保证React的diff算法的时间复杂度为O(n)

  • 同层节点之间相互比较,不会跨节点比较
  • 不同类型的节点,产生不同的树结构
  • 开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定,也就是不需要重新进行渲染

key

默认情况下,react在比较两个节点的时候,是通过如下方式确定两个节点是同一个节点的

  1. 两个节点的元素类型是一致的
  2. 两个节点的元素内容是一致的

这种比较方式在列表末尾插入数据的时候,并没有什么问题

但是如果在列表中间插入或移除对于元素的时候,使用以上的比较方式

react并没有办法很好的识别出节点的移动变化

所以为了避免这种情况,React推荐为列表渲染的每一项设置一个key作为节点的唯一标识

React 使用 key 来匹配原有树上的子元素以及最新树上的子元素是否需要更新

React可以通过key来更好的识别新旧节点,并尽可能的移动复用对应的元素,从而以最小代价去更新dom元素

所以key一般需要满足如下要求:

  1. key类似于元素的id,是diff算法中对于元素的唯一标识,所以key的值应该是唯一的

  2. 在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

不可变数据的特点

  1. 相比可以随意修改的数据而言,不可变数据更容易追踪,调试和回溯

    • 每次对于不可变数据的修改都是新产生了一个对象,并不会修改原始对象

      此时就可以将各个阶段的数据存储起来,从而使不可变数据更容易追踪,调试和回溯

  2. 不可变数据可以让复杂的变化检测机制得以简单快速的实现,从而确保代价高昂的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

函数组件

  1. 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
  2. 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
  3. 你不能在函数组件上使用 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