第五节:setState详解、react更新机制、render函数优化、数据不可变、ref获取DOM和组件

一. setState详解

1. 使用背景

 开发中我们并不能直接通过修改state的值来让界面发生更新:

 (1).因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;

 (2).React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;

 (3).我们必须通过setState来告知React数据已经发生了变化

补充:

  在组件中并没有实现setState的方法,为什么可以调用呢?  原因:setState方法是从Component中继承过来的。  详见图1

2. 三种写法

 (1). 基本写法

   this.setState({ msg: "hello ypf1" });  内部执行是一个合并操作, object.assign()

 (2). 传入一个回调函数

    this.setState((state, props) => {      return {msg: "hello ypf2",};   });

用途:

   A.在回调函数中对新的state逻辑进行编写

   PS:这里的state拿到的是最新的state,即上面如果执行了setState一次,这里的state是更新后的state

   B.回调函数中可以将之前的state和props传递进来,进行使用

 (3). setState是一个异步调用

   如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码,那么可以在setState中传入第二个参数: callback

   this.setState({ msg: "hello ypf3" }, () => {});

查看代码
export class APP extends Component {
	constructor() {
		super();
		this.state = { msg: "hello ypf", counter: 100 };
	}
	//setState的三种写法
	// 用法1--基本使用
	changeText1() {
		// 内部执行是一个合并操作, object.assign()
		this.setState({ msg: "hello ypf1" });
	}

	//用法2--传入一个回调函数
	/* 
      用途:
        1.在回调函数中对新的state逻辑进行编写
		PS:这里的state拿到的是最新的state,即上面如果执行了setState一次,这里的state是更细后的state
        2.回调函数中可以将之前的state和props传递进来,进行使用
    */
	changeText2() {
		this.setState((state, props) => {
			// 1. 获取之前的state和props
			console.log(state, props);

			// 2. 在return中设置state中的值
			return {
				msg: "hello ypf2",
			};
		});
	}

	//用法3--setState是一个异步调用
	/* 
      如果希望在数据更新之后(数据合并), 获取到对应的结果执行一些逻辑代码
      那么可以在setState中传入第二个参数: callback
    */
	changeText3() {
		this.setState({ msg: "hello ypf3" }, () => {
			console.log("----回调中获取:", this.state.msg); //hello ypf3
		});
		console.log("----执行后获取:", this.state.msg); //hello ypf
	}

	render() {
		const { msg, counter } = this.state;
		return (
			<div>
				<h3>msg:{msg}</h3>
				<h3>counter:{counter}</h3>
				<p>
					<button onClick={() => this.changeText1()}>修改文本1</button>
				</p>
				<p>
					<button onClick={() => this.changeText2()}>修改文本2</button>
				</p>
				<p>
					<button onClick={() => this.changeText3()}>修改文本3</button>
				</p>
			</div>
		);
	}
}

 

3. setState异步更新

 (1). 证明setState是异步的?

   详见案例1即可,默认值是0,连续执行三次setState,最终结果是1,间接的证明了setState是异步的,三个log中输出的都是0

   constructor() {
		super();
		this.state = { counter: 0 };
	}

	/* 
      案例1
      默认值是0,连续执行三次setState,最终结果是1,间接的证明了setState是异步的
      三个log中输出的都是0
    */
	Add1() {
		this.setState({ counter: this.state.counter + 1 });
		console.log("-----------", this.state.counter); //0
		this.setState({ counter: this.state.counter + 1 });
		console.log("-----------", this.state.counter); //0
		this.setState({ counter: this.state.counter + 1 });
		console.log("-----------", this.state.counter); //0
	}

 (2). 为什么setState设计为异步呢?

   (React核心成员(Redux的作者)的回复:https://github.com/facebook/react/issues/11527#issuecomment-360199710)

  A. setState设计为异步,可以显著的提升性能;

    如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;最好的办法应该是获取到多个更新,之后进行批量更新

  B.如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步(render中的显示和实际不一致)

    state和props不能保持一致性,会在开发中产生很多的问题

 

4. 如何获取异步结果?

 (1). setState的回调

   setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后会执行;(或者是直接传入一个回调函数)

   格式如下:setState(partialState, callback)     this.setState({ msg: "hello ypf3" }, () => {});

   或:   this.setState((state, props)=>{});

/* 
    案例2
      默认值是0,连续执行三次setState,最终结果是3,证明了回调中拿到是更新后state
      三个log中输出的一次是:0、1、2
    */
	Add2() {
		this.setState((state, props) => {
			console.log("-----------", state.counter); //0
			return { counter: state.counter + 1 };
		});
		this.setState((state, props) => {
			console.log("-----------", state.counter); //1
			return { counter: state.counter + 1 };
		});
		this.setState((state, props) => {
			console.log("-----------", state.counter); //2
			return { counter: state.counter + 1 };
		});
	}

 (2).在生命周期函数componentDidUpdate

  componentDidUpdate() {
    console.log("------------componentDidUpdate:", this.state.counter);
  }

 

5. setState不同版本的异步情况

 (1). 在react18之前

  A. 在组件生命周期或React合成事件中,setState是异步;

  B. 在setTimeout或者原生dom事件(eg: addEventListener)中,setState是同步

 (2). 在react18之后

   默认所有的操作都被放到了批处理中(都是异步处理),如果希望代码可以同步会拿到,则需要执行特殊的flushSync操作。

 

二. react更新机制

1. react更新流程

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

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

   如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n³),其中 n 是树中元素的数量;

   https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf;

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

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

(3). 于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?

   同层节点之间相互比较,不会垮节点比较;

   不同类型的节点,产生不同的树结构;

   开发中,可以通过key来指定哪些节点在不同的渲染下保持稳定;

2. keys的优化

 我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性:

(1). key的几种情况

 ◼ 方式一:在最后位置插入数据

  这种情况,有无key意义并不大

 ◼ 方式二:在前面插入数据

  这种做法,在没有key的情况下,所有的li都需要进行修改;

 ◼ 方式三: 当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素:

   在下面这种场景下,key为111和222的元素仅仅进行位移,不需要进行任何的修改; 将key为333的元素插入到最前面的位置即可;

 (2). key的注意事项

   key应该是唯一的;

   key不要使用随机数(随机数在下一次render时,会重新生成一个数字);

   使用index作为key,对性能是没有优化的;

 

三. render函数优化

1. 背景

  App组件中引用Son1 和 Son2 组件,App中修改一下counter,发现Son1和Son2也重新渲染了,即所有的组件都需要重新render,进行diff算法,性能必然是很低的,也是没必要的,很多的组件没有必须要重新render。

  如:V1版本的案例,在App中修改counter, 发现 App、Son1、Son2 都重新渲染了。

 

2. 解决方案1--shouldComponentUpdate

 (1). 说明

   A. React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值,该方法有两个参数:

    参数一:nextProps 修改之后,最新的props属性

    参数二:nextState 修改之后,最新的state属性

   B. 该方法返回值是一个boolean类型:

    返回值为true,那么就需要调用render方法;

    返回值为false,那么久不需要调用render方法;

    默认返回的是true,也就是只要state发生改变,就会调用render方法;

 (2). 实操

    如:V2版本的案例

    App组件中引用Son1 和 Son2 组件, 这两个Son组件中都在shouldComponentUpdate中判断msg是否修改,修改了则返回true,需要渲染,反之返回false,不渲染。 App中Son1绑定msg1,Son2绑定msg2

    APP中修改counter,App渲染,Son1和Son2都不渲染

    APP中修改msg1,App和Son1渲染,Son2不渲染

核心代码分享:

export class Son1 extends Component {
	constructor() {
		super();
		this.state = { msg: "hello ypf" };
	}
	/**
	 * 组件是否更新的生命周期
	 * @param {*} nextProps   修改之后,最新的props属性
	 * @param {*} nextState  修改之后,最新的state属性
	 */
	shouldComponentUpdate(nextProps, nextState) {
		// 自己编写业务决定是否更新
		if (this.state.msg !== nextProps.msg) {
			return true;
		}
		return false;
	}
	render() {
		console.log("Son1 Render");
		return <div>Son1</div>;
	}
}

3. 解决方案3--PureComponent

 (1). 背景

  如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量,React已经考虑到了这一点,使用 PureComponent 解决了这个问题

 (2). 实操

  如:V3版本的案例

  App组件中引用Son1 和 Son2 组件,且该三个组件都继承PureComponent,App中Son1绑定msg1,Son2绑定msg2

  APP中修改counter,App渲染,Son1和Son2都不渲染

  APP中修改msg1,App和Son1渲染,Son2不渲染

核心代码分享:

export class Son1 extends PureComponent {
	constructor() {
		super();
		this.state = { msg: "hello ypf" };
	}
	render() {
		console.log("Son1 Render");
		return <div>Son1</div>;
	}
}

 

4. 高阶组件memo

 (1). 说明

  函数式组件没有PureComponent,则需要使用memo高阶组件,函数式组件我们在props没有改变时,也是不希望其重新渲染其DOM树结构的。

 (2). 实操

   如:V4版本的案例, Son2组件中使用memo

核心代码分享:

import { memo } from "react";

const Son2 = memo(function (props) {
	console.log("Son2 Render");
	return <div>Son2</div>;
});

export default Son2;

 

5. 原理剖析--shallowEqual方法

   PureComponent内部使用shallowEqual方法进行浅层比较,详见图1

 

 

四. 数据不可变 

1. 背景

  修改一个引用类型的数据(eg:this.state.books 这个数据),我们能想到两种修改方式

  方式1: 【不要用这种!!】

     直接修改这个books, 然后调用setState进行赋值

  方式2:【推荐】

      先把这个books进行浅拷贝一下,然后修改浅拷贝后的数据,最后调用setState进行赋值

上述两种方式,如果类组件继承的是 Component,两种方式都能生效。如果继承的是PureComponent,那么只有方式2生效。

import React, { PureComponent, Component } from "react";

export class App extends PureComponent {
	constructor() {
		super();
		this.state = {
			books: [
				{ name: "你不知道JS", price: 99, count: 1 },
				{ name: "JS高级程序设计", price: 88, count: 1 },
				{ name: "React高级设计", price: 78, count: 2 },
				{ name: "Vue高级设计", price: 95, count: 3 },
			],
			friend: { name: "ypf", age: 18 },
			msg: "Hello World",
		};
	}

	addNewBook() {
		const newBook = { name: "Angular高级设计", price: 88, count: 1 };
		
           //写法1(不推荐!)
           //在PureComponent是不能引入重新渲染(re-render),因为 pureComponent认为this.state 和 修改后后nextState是一样的,所以不渲染
  
		// this.state.books.push(newBook);
		// this.setState({ books: this.state.books });

		//写法2
		let tempBooks = [...this.state.books];
		tempBooks.push(newBook);
		this.setState({ books: tempBooks });
	}
	addBookCount(index) {
		// 写法1(不推荐!):
		// this.state.books[index].count++; //不能直接修改
		// this.setState({ books: this.state.books });
		// 写法2:
		let tempBooks = [...this.state.books];
		tempBooks[index].count++;
		this.setState({ books: tempBooks });
	}

	render() {
		const { books } = this.state;

		return (
			<div>
				<h3>数据列表</h3>
				<ul>
					{books.map((item, index) => (
						<li key={index}>
							<span>--name--:{item.name}--</span>
							<span>--price--:{item.price}--</span>
							<span>--count--:{item.count}--</span>
							<button onClick={() => this.addBookCount(index)}>+1</button>
						</li>
					))}
				</ul>
				<p>
					<button onClick={() => this.addNewBook()}>AddNewBook</button>
				</p>
			</div>
		);
	}
}

export default App;

 

2. 剖析

  方式1 在PureComponent中为什么不生效呢?

  在PureComponent是不能引入重新渲染(re-render),因为 pureComponent认为this.state 和 修改后nextState是一样的,所以不渲染。

  浅拷贝一下,虽然堆的数据还是一致的,  但是栈中却是不能的两个对象了,所以PureComponent认为是不一样的,就能生效,重新渲染。

 

五. ref获取Dom和组件 

1. 获取DOM

(1) 方式一:传入字符串

 使用时通过 this.refs.传入的字符串格式获取对应的元素;

(2) 方式二:传入一个对象 【推荐使用】

 对象是通过 React.createRef() 方式创建出来的;使用时获取到创建的对象其中有一个current属性就是对应的元素;

(3) 方式三:传入一个函数

  该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存;

  使用时,直接拿到之前保存的元素对象即可

export class App extends PureComponent {
	constructor() {
		super();
		this.state = {};

		// 方式2
		this.myRef2 = createRef();

		// 方式3
		this.myRef3 = null;
	}
	getNativeDOM1() {
		// 方式1:在react上绑定一个ref字符串
		console.log(this.refs.myRef1);
	}
	getNativeDOM2() {
		// 方式2:createRef()提前创建好ref对象,  将创建出来的对象绑定到元素 【推荐!】
		// 需要使用 current 属性获取DOM对象
		console.log(this.myRef2.current);
	}
	getNativeDOM3() {
		// 方式3: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素传入
		console.log(this.myRef3);
	}

	render() {
		return (
			<div>
				<h3 ref="myRef1">hello ypf1</h3>
				<h3 ref={this.myRef2}>hello ypf2</h3>
				<h3 ref={el => (this.myRef3 = el)}>hello ypf3</h3>
				<button onClick={() => this.getNativeDOM1()}>获取DOM-方式1</button>
				<button onClick={() => this.getNativeDOM2()}>获取DOM-方式2</button>
				<button onClick={() => this.getNativeDOM3()}>获取DOM-方式3</button>
			</div>
		);
	}
}

 

2. 获取类组件

 采用上述方式二的模式,在在类组件上通过ref绑定一个 createRef() 创建的对象,然后通过.current属性就可以获取对应的组件

export class App extends PureComponent {
	constructor() {
		super();
		// 采用方式2的模式获取
		this.myHelloRef = createRef();
	}
	getChildCpn() {
		let helloCpn = this.myHelloRef.current;
		console.log(helloCpn);
		// 调用子组件的方法
		helloCpn.myTest();
	}

	render() {
		return (
			<div>
				App
				<HelloWorld ref={this.myHelloRef}></HelloWorld>
				<button onClick={() => this.getChildCpn()}>获取类组件</button>
			</div>
		);
	}
}

export class HelloWorld extends PureComponent {
	myTest() {
		console.log("HelloWorld中的myTest方法");
	}
	render() {
		return <div>HelloWorld</div>;
	}
}

 

3. 获取函数式组件的某个标签

 (1). 函数式组件是没有实例的,所以无法通过ref获取他们的实例, 但是可以获取函数组件中的某个标签。

 (2). 如何获取?

    A. 通过上述方式二的模式,给函数组件绑定要给ref对象

    B. 函数式组件要通过forwardRef包裹,接收两个参数:props 和 ref, 然后把这个ref绑定到对应的DOM上即可

export class App extends PureComponent {
	constructor() {
		super();
		// 采用方式2的模式获取
		this.myHelloRef = createRef();
	}
	getChildCpn() {
		// 获取HelloWorldFun组件中的某个标签
		let helloCpn = this.myHelloRef.current;
		console.log(helloCpn);
	}

	render() {
		return (
			<div>
				App
				<HelloWorldFun ref={this.myHelloRef}></HelloWorldFun>
				<button onClick={() => this.getChildCpn()}>获取类组件</button>
			</div>
		);
	}
}

const HelloWorldFun = forwardRef(function (props, ref) {
	return (
		<div>
			<h1 ref={ref}>HelloWorldFun中的h1标签</h1>
			<p>哈哈哈</p>
		</div>
	);
});

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2023-04-16 19:13  Yaopengfei  阅读(1579)  评论(1编辑  收藏  举报