想了半小时,实在不知道开头应该怎么写,就随便写点内容填充一下,以显得不那么空虚。
话说回来,react是很多前端切图仔的吃饭家伙,更深入的了解其特性有助于我们更好的吃饭。前端时间我在重新学习react的时候,发现了一些比较有趣的行为,总结一波看看有没有引起大家的共鸣。
(全文仅为笔者个人理解和总结,不喜勿喷)
setState
中间状态
有些时候,我们需要在组件更新之后(componentDidUpdate钩子)才能确定要不要更新状态!那么如果在componentDidUpdate钩子里面更新状态,那么中间状态会被显示出来吗,也就是页面会闪一下吗?
class State extends PureComponent {
state = { count: 0 };
componentDidMount() {
this.setState({
count: 1
});
}
componentDidUpdate() {
console.log(document.getElementById('count').innerHTML);
if (this.state.count < 50) {
this.setState({ state: this.state.count + 1 });
}
}
render() {
return (
<span id="count">{ this.state.count }</span>
);
}
}
运行之后可以看到结果,页面直接显示最终状态50,中间状态0-49并没有闪现页面(页面没有出现闪的情况),但是控制台里面按顺序打印了1-49。
得出结论:在componentDidUpdate、componentDidMount等commit阶段的钩子里面进行setState的操作,会改变dom树,浏览器只渲染最终状态,中间状态不会被展示。
猜想原因是js引擎和渲染引擎共用同一线程,中间的一波setState操作实际上是js引擎占据着线程,渲染引擎无法渲染,所以中间状态并未显出来(类似js大计算量任务造成浏览器假死状态)。
同步还是异步setState
看下面例子
class State extends PureComponent {
state = { count: 0 }
componentDidMount() {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 0
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 2
}, 0);
}
render() {
return (
<span id="count">{ this.state.count }</span>
);
}
}
看到最终输出结果发现,第一个setState并没有立即执行,而第二个setState却立即执行了。
首先,第一个setState表现为异步,是React内部做的一个批量更新的优化。当一个react交互事件(如onClick等react合成事件)触发时,内部会将一个flag:isBatchingUpdates置为true,此时React会将收集到的setState存起来,然后统一更新。
其次,根据以上说法,第二个setState是在setTimeout里面调用的,并没有触发react交互事件,所以并不会进行批量更新,也就表现为同步的形式。
当然,如果要在setTimeout等异步回调中进行批量更新的操作,可以使用ReactDom.unstable_batchedUpdates方法:
class State extends PureComponent {
state = { count: 0 }
componentDidMount() {
setTimeout(() => {
ReactDom.unstable_batchedUpdates(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 0
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 0
});
console.log(this.state.count); // 2
}, 0);
}
render() {
return (
<span id="count">{ this.state.count }</span>
);
}
}
连续更新
我们现在知道交互事件回调函数中的setState是异步的,那么在连续更新的时候就会产生一些问题:
class State extends PureComponent {
state = { count: 0 }
handleClick = () => {
this.setState({ count: this.state.count + 1 });
this.updateSomething();
}
updateSomething = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<React.Fragment>
<span id="count">{ this.state.count }</span>
{ /* 实际上点击按钮一次,count += 1 */ }
<button onClick={ this.handleClick }>click me</button>
</React.Fragment>
);
}
}
我们预期的是:点击click me按钮,count += 2。
实际结果:count += 1.
很不幸的是我们setState的时候,setState是异步的,多个setState会被合并掉,最终相当于只发起了一次setState。
像这种需要依赖于上一次setState值的操作,可以使用另一种方法的setState方式:
class State extends PureComponent {
state = { count: 0 }
handleClick = () => {
this.setState(state => ({ count: state.count + 1 }));
this.updateSomething();
}
updateSomething = () => {
this.setState(state => ({ count: state.count + 1 }));
}
render() {
return (
<React.Fragment>
<span id="count">{ this.state.count }</span>
{ /* 实际上点击按钮一次,count += 2 */ }
<button onClick={ this.handleClick }>click me</button>
</React.Fragment>
);
}
}
key
key是帮助React判断哪个元素发生了改变、新增和移除的辅助标识,但是我发现我们基本只在list渲染中使用key,如下:
render() {
return (
<Fragment>{
list.map(item => (
<ListItem key={ item.id } { ...item } />
))
}</Fragment>
);
}
实际上,在处理组件派生状态的时候,使用key可以减少我们状态重置操作:
父组件
class Key extends Component {
state = {
activeItem: {
id: 0,
name: 'listItem0'
}
}
render() {
const { activeItem = {} } = this.state;
return (
<div>
<ItemDetail
key={ activeItem.id }
itemData={ activeItem }
/>
</div>
);
}
}
export default Key;
子组件
class ItemDetail extends PureComponent {
constructor(props) {
super(props);
this.state = {
name: `${ props.itemData.name }_child` // 派生状态,这是不好的行为,应避免
}
}
fetchItemDetail = () => {
// ajax request
}
componentDidMount() {
this.fetchItemDetail();
}
render() {
return (
<div>
{ /* item info */ }
</div>
);
}
}
上面的例子中,每当activeItem变化时,父组件给ItemDetail子组件传入的key会变化,则ItemDetail组件会被React删掉,然后重新创建,故子组件内部会自动生成新的派生状态和获取新的item详情。
此方法的优点是不必在子组件内部监听props的变化以获取新的数据和生成新的派生状态,缺点是key每次渲染都需要删除原有的组件,再生成新的组件。
假如子组件内部的状态比较复杂,且diff成本较高,可以考虑使用以上方法重置状态。
Ref
一直以来我们都被教导:使用ref来调用组件实例的方法会破坏组建的生命周期,我们应该避免使用这个特性,但是某些情况下是非获取不可的。这里总结几种情况下获取组件实例的方法:
获取高阶组件的实例
我们可以使用React提供的forwardRed方法将ref当成一个普通的props属性进行透传
import React, { forwardRef } from 'react';
import Child from './Child';
const ForwardRefChild = forwardRef((props, ref) => {
return (
<Child
{ ...props }
forwardedRef={ ref }
/>
);
});
父组件
class Ref extends PureComponent {
getRef = instance => {
console.log(instance);
}
render() {
return (
<ForwardRefChild ref={ this.getRef }></ForwardRefChild>
);
}
}
子组件
class Child extends PureComponent {
render() {
const { forwardedRef } = this.props;
return (
<div ref={ forwardedRef }></div>
);
}
}
获取react-redux的connect装饰器装饰过的组件实例
connect函数有四个入参,其中第四个入参为可选参数,类型为对象,对象属性包括
- pure: boolean
- withRef: boolean,标识是否保存一个对被被包含的组件实例的引用,改引用通过getWrappedInstance方法获得
没错,只要通过设置withRef为true即可通过getWrappedInstance方法获得实例
class Ref extends PureComponent {
getRef = decoratedInstance => {
console.log(decoratedInstance.getWrappedInstance()); // real instance
}
render() {
retrun (
<ChildDecoratedByConnect ref={ this.getRef }/>
);
}
}
获取props.children的ref
以上两种场景都是中规中矩的操作(it means that 不够骚),假如我们想要在组件中获取props.children的属性,那么就有点麻烦了,直接贴代码:
class Child extends PureComponent {
getRef = node => {
console.log(node); // 开心又快乐
}
getChild = () => {
const { children } = this.props;
const element = React.Children.toArray(children)[0]; // 获取children第一个子元素
// clone这个子元素
return React.cloneElement(element, {
ref: this.getRef // 重写ref
});
}
render() {
return (
<div>{ this.getChild() }</div>
);
}
}
我们没有直接渲染props.children,而是通过React.clone复制了一个副本,并且修改其ref的值,达到我们的目的(这里只渲染了一个子元素,多个子元素请自己实现)。
上面的例子中,存在一个问题,由于Child组件复写了props.children的ref,会导致props.children原有的ref无法使用。为了解决这个问题,我们可以结合上文提到的forwardRef,将实例传给原有的ref
class Child extends PureComponent {
getRef = node => {
const { forwardedRef } = this.props;
// function类型ref
if ((typeof forwardedRef).toUpperCase() === 'FUNCTION') {
forwardedRef(node);
} else {
// 使用React.createRef生成的ref对象
forwardedRef.current = node;
}
console.log(node); // 开心又快乐
}
getChild = () => {
const { children } = this.props;
const element = React.Children.toArray(children)[0];
return React.cloneElement(element, {
ref: this.getRef
});
}
render() {
return (
<div>{ this.getChild() }</div>
);
}
}
在子组件的getRef函数中,将实例赋值给原ref对象或者作为参数调用ref函数,即可使得外层的函数也可以获得此实例。
结尾
以上内容,如有错漏,欢迎指正。
好了,到点下班!!!
@Author: PaperCrane