29 React常考知识点
29.1 生命周期
在
V16版本中引入了Fiber机制。这个机制一定程度上的影响了部分生命周期的调用,并且也引入了新的2个API来解决问题
在之前的版本中,如果你拥有一个很复杂的复合组件,然后改动了最上层组件的
state,那么调用栈可能会很长

- 调用栈过长,再加上中间进行了复杂的操作,就可能导致长时间阻塞主线程,带来不好的用户体验。
Fiber就是为了解决该问题而生 Fiber本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新

- 对于如何区别优先级,
React有自己的一套逻辑。对于动画这种实时性很高的东西,也就是16 ms必须渲染一次保证不卡顿的情况下,React会每16 ms(以内) 暂停一下更新,返回来继续渲染动画 - 对于异步渲染,现在渲染有两个阶段:
reconciliation和commit。前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。
1. Reconciliation 阶段
componentWillMountcomponentWillReceivePropsshouldComponentUpdatecomponentWillUpdate
2. Commit 阶段
componentDidMountcomponentDidUpdatecomponentWillUnmount
因为
Reconciliation阶段是可以被打断的,所以Reconciliation阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起Bug。由此对于Reconciliation阶段调用的几个函数,除了shouldComponentUpdate以外,其他都应该避免去使用,并且V16中也引入了新的API来解决这个问题。
getDerivedStateFromProps用于替换componentWillReceiveProps,该函数会在初始化和update时被调用
class ExampleComponent extends React.Component {
// Initialize state in constructor,
// Or with a property initializer.
state = {};
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.someMirroredValue !== nextProps.someValue) {
return {
derivedData: computeDerivedState(nextProps),
someMirroredValue: nextProps.someValue
};
}
// Return null to indicate no change to state.
return null;
}
}
getSnapshotBeforeUpdate用于替换componentWillUpdate,该函数会在update后DOM更新前被调用,用于读取最新的DOM数据
更多详情 http://blog.poetries.top/2018/11/18/react-lifecircle
29.2 setState
setState在React中是经常使用的一个API,但是它存在一些的问题经常会导致初学者出错,核心原因就是因为这个API是异步的。- 首先
setState的调用并不会马上引起state的改变,并且如果你一次调用了多个setState,那么结果可能并不如你期待的一样。
handle() {
// 初始化 `count` 为 0
console.log(this.state.count) // -> 0
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // -> 0
}
- 第一,两次的打印都为
0,因为setState是个异步API,只有同步代码运行完毕才会执行。setState异步的原因我认为在于,setState可能会导致DOM的重绘,如果调用一次就马上去进行重绘,那么调用多次就会造成不必要的性能损失。设计成异步的话,就可以将多次调用放入一个队列中,在恰当的时候统一进行更新过程。
Object.assign(
{},
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
)
当然你也可以通过以下方式来实现调用三次
setState使得count为3
handle() {
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
}
如果你想在每次调用
setState后获得正确的state,可以通过如下代码实现
handle() {
this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
console.log(this.state)
})
}
更多详情 http://blog.poetries.top/2018/12/20/react-setState
29.3 性能优化
- 在
shouldComponentUpdate函数中我们可以通过返回布尔值来决定当前组件是否需要更新。这层代码逻辑可以是简单地浅比较一下当前state和之前的state是否相同,也可以是判断某个值更新了才触发组件更新。一般来说不推荐完整地对比当前state和之前的state是否相同,因为组件更新触发可能会很频繁,这样的完整对比性能开销会有点大,可能会造成得不偿失的情况。 - 当然如果真的想完整对比当前
state和之前的state是否相同,并且不影响性能也是行得通的,可以通过immutable或者immer这些库来生成不可变对象。这类库对于操作大规模的数据来说会提升不错的性能,并且一旦改变数据就会生成一个新的对象,对比前后state是否一致也就方便多了,同时也很推荐阅读下immer的源码实现 - 另外如果只是单纯的浅比较一下,可以直接使用
PureComponent,底层就是实现了浅比较state
class Test extends React.PureComponent {
render() {
return (
<div>
PureComponent
</div>
)
}
}
这时候你可能会考虑到函数组件就不能使用这种方式了,如果你使用
16.6.0之后的版本的话,可以使用React.memo来实现相同的功能
const Test = React.memo(() => (
<div>
PureComponent
</div>
))
通过这种方式我们就可以既实现了
shouldComponentUpdate的浅比较,又能够使用函数组件
29.4 通信
1. 父子通信
- 父组件通过
props传递数据给子组件,子组件通过调用父组件传来的函数传递数据给父组件,这两种方式是最常用的父子通信实现办法。 - 这种父子通信方式也就是典型的单向数据流,父组件通过
props传递数据,子组件不能直接修改props, 而是必须通过调用父组件函数的方式告知父组件修改数据。
2. 兄弟组件通信
对于这种情况可以通过共同的父组件来管理状态和事件函数。比如说其中一个兄弟组件调用父组件传递过来的事件函数修改父组件中的状态,然后父组件将状态传递给另一个兄弟组件
3. 跨多层次组件通信
如果你使用
16.3以上版本的话,对于这种情况可以使用Context API
// 创建 Context,可以在开始就传入值
const StateContext = React.createContext()
class Parent extends React.Component {
render () {
return (
// value 就是传入 Context 中的值
<StateContext.Provider value='test'>
<Child />
</StateContext.Provider>
)
}
}
class Child extends React.Component {
render () {
return (
<ThemeContext.Consumer>
// 取出值
{context => (
name is { context }
)}
</ThemeContext.Consumer>
);
}
}
4. 任意组件
这种方式可以通过
Redux或者Event Bus解决,另外如果你不怕麻烦的话,可以使用这种方式解决上述所有的通信情况
29.5 HOC 是什么?相比 mixins 有什么优点?
很多人看到高阶组件(
HOC)这个概念就被吓到了,认为这东西很难,其实这东西概念真的很简单,我们先来看一个例子。
function add(a, b) {
return a + b
}
现在如果我想给这个
add函数添加一个输出结果的功能,那么你可能会考虑我直接使用console.log不就实现了么。说的没错,但是如果我们想做的更加优雅并且容易复用和扩展,我们可以这样去做
function withLog (fn) {
function wrapper(a, b) {
const result = fn(a, b)
console.log(result)
return result
}
return wrapper
}
const withLogAdd = withLog(add)
withLogAdd(1, 2)
- 其实这个做法在函数式编程里称之为高阶函数,大家都知道
React的思想中是存在函数式编程的,高阶组件和高阶函数就是同一个东西。我们实现一个函数,传入一个组件,然后在函数内部再实现一个函数去扩展传入的组件,最后返回一个新的组件,这就是高阶组件的概念,作用就是为了更好的复用代码。 - 其实
HOC和Vue中的mixins作用是一致的,并且在早期React也是使用mixins的方式。但是在使用class的方式创建组件以后,mixins的方式就不能使用了,并且其实mixins也是存在一些问题的,比如
- 隐含了一些依赖,比如我在组件中写了某个
state并且在mixin中使用了,就这存在了一个依赖关系。万一下次别人要移除它,就得去mixin中查找依赖 - 多个
mixin中可能存在相同命名的函数,同时代码组件中也不能出现相同命名的函数,否则就是重写了,其实我一直觉得命名真的是一件麻烦事。。 - 雪球效应,虽然我一个组件还是使用着同一个
mixin,但是一个mixin会被多个组件使用,可能会存在需求使得mixin修改原本的函数或者新增更多的函数,这样可能就会产生一个维护成本
HOC解决了这些问题,并且它们达成的效果也是一致的,同时也更加的政治正确(毕竟更加函数式了)
29.6 事件机制
React其实自己实现了一套事件机制,首先我们考虑一下以下代码:
const Test = ({ list, handleClick }) => ({
list.map((item, index) => (
<span onClick={handleClick} key={index}>{index}</span>
))
})
- 以上类似代码想必大家经常会写到,但是你是否考虑过点击事件是否绑定在了每一个标签上?事实当然不是,
JSX上写的事件并没有绑定在对应的真实DOM上,而是通过事件代理的方式,将所有的事件都统一绑定在了document上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。 - 另外冒泡到
document上的事件也不是原生浏览器事件,而是React自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用event.stopPropagation是无效的,而应该调用event.preventDefault
那么实现合成事件的目的是什么呢?总的来说在我看来好处有两点,分别是:
- 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力
- 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。
