前几天面试问道 react 的相关知识,对我打击比较大,感受对 react 认识很是肤浅,因此在这里从新梳理一下,想一想以前没有仔细思考过的东西。javascript
另外有说的不对的地方还请帮我指正一下,先谢谢各位啦。java
目录索引:react
React 有一套合理的运行机制去控制程序在指定的时刻该作什么事,当一个生命周期钩子被触发后,紧接着会有下一个钩子,直到整个生命周期结束。面试
生命周期表明着每一个执行阶段,好比组件初始化,更新完成,立刻要卸载等等,React 会在指定的时机执行相关的生命周期钩子,使咱们能够有机在程序运行中会插入本身的逻辑。算法
咱们写代码的时候每每会有不少组件以及他们的子组件,各自调用不一样的生命周期,这时就要解决谁先谁后的问题,在 react v16 以前是采用了递归调用的方式一个一个执行,而在如今 v16 的版本中则采用了与之彻底不一样的处理(调度)方式,名叫 Fiber,这个东西 facebook 作了有两年时间,实现很是复杂。redux
具体 Fiber 它是一个什么东西呢?不要着急,咱们先从最基本的生命周期钩子看起。性能优化
首先看一下 React V16.4 后的生命周期概况(图片来源) 异步
constructor()
- 类构造器初始化static getDerivedStateFromProps()
- 组件初始化时主动触发render()
- 递归生成虚拟 DOMcomponentDidMount()
- 完成首次 DOM 渲染static getDerivedStateFromProps()
- 每次 render() 以前执行shouldComponentUpdate()
- 校验是否须要执行更新操做render()
- 递归生成虚拟 DOMgetSnapshotBeforeUpdate()
- 在渲染真实 DOM 以前componentDidUpdate()
- 完成 DOM 渲染componentWillUnmount()
- 组件销毁以前被直接调用static getDerivedStateFromProps()
这个钩子会在每一个更新操做以前(即便props没有改变)执行一次,使用时应该保持谨慎。componentDidMount()
和 componentDidUpdate()
执行的时机是差很少的,都在 render
以后,只不过前者只在首次渲染后执行,后者首次渲染不会执行getSnapshotBeforeUpdate()
执行时能够得到只读的新 DOM 树,此函数的返回值为 componentDidUpdate(prevProps, prevState, snapshot)
的第三个参数关于 Fiber,强烈建议听一下知乎上程墨Morgan的 live 《深刻理解React v16 新功能》,这里潜水员的例子和图片也是引用于此 live。async
咱们知道 React 是经过递归的方式来渲染组件的,在 V16 版本以前的版本里,当一个状态发生变动时,react 会从当前组件开始,依次递归调用全部的子组件生命周期钩子,并且这个过程是同步执行的且没法中断的,一旦有很深很深的组件嵌套,就会形成严重的页面卡顿,影响用户体验。函数
React 在V16版本以前的版本里引入了 Fiber 这样一个东西,它的英文涵义为纤维,在计算机领域它排在在进程和线程的后面,虽然 React 的 Fiber 和计算机调度里的概念不同,可是能够方便对比理解,咱们大概能够想象到 Fiber 多是一个比线程还短的时间片断。
Fiber 把当前须要执行的任务分红一个个微任务,安排优先级,而后依次处理,每过一段时间(很是短,毫秒级)就会暂停当前的任务,查看有没有优先级较高的任务,而后暂停(也可能会彻底放弃)掉以前的执行结果,跳出到下一个微任务。同时 Fiber 还作了一些优化,能够保持住以前运行的结果以到达复用目的。
咱们能够把调度当成一个潜水员在海底寻宝,v16 以前是经过组件递归的方式进行寻宝,从父组件开始一层一层深刻到最里面的子组件,也就是以下图所示。
而替换成了 Fiber 后,海底变成的狭缝(简单理解为递归变成了遍历),潜水员会每隔一小段时间浮出水面,看看有没有其余寻宝任务。注意此时没有寻到宝藏的话,那么以前潜水的时间就浪费了。就这样潜水员会一直下潜和冒泡,具体以下图所示。
从生命周期那张图片纵向来看,Fiber 将整个生命周期分红了三个阶段:
componentWillMount()
,componentWillUpdate()
,componentWillReceiveProps()
的三个生命周期钩子被加上了 UNSAFE
标记简而言之:以 render() 为界,以前执行的生命周期都有可能会打断并屡次调用,以后的生命周期是不可被打断的且只会调用一次。因此尽可能把反作用的代码放在只会执行一次的 commit 阶段。
除了上面经常使用的钩子,React 还提供了以下钩子:
static getDerivedStateFromError()
在 render 阶段执行,经过返回 state 更新组件状态componentDidCatch()
在 commit 阶段执行,能够放一些有反作用的代码理解了生命周期和三个执行阶段,就能够比较容易理解组件状态的更新机制了。
这个方法可让咱们更新组件的 state 状态。第一个参数能够是对象,也能够是 updater 函数,若是是函数,则会接受当前的 state 和 props 做为参数。第二个参数为函数,是在 commit 阶段后执行,准确的说是在 componentDidUpdate()
后执行。
setState() 的更新过程是异步的(除非绑定在 DOM 事件中或写在 setTimeout 里),并且会在最后合并全部的更新,以下:
Object.assign(
previousState,
{quantity: state.quantity + 1},
{quantity: state.quantity + 1},
...
)
复制代码
之因此设计成这样,是为了不在一次生命周期中出现屡次的重渲染,影响页面性能。
若是咱们想强制刷新一个组件,能够直接调用该方法,调用时会直接执行 render()
这个函数而跳过 shouldComponentUpdate()
。
function wait() {
return new Promise(resolve => {
setTimeout(() => {
resolve();
console.log("wait");
}, 0);
});
}
//......省略组件建立
async componentDidMount() {
await wait();
this.setState({
name: "new name"
});
console.log("componentDidMount");
}
componentDidUpdate() {
console.log("componentDidUpdate");
}
render() {
console.log(this.state);
return null
}
//......省略组件建立
// 输出结果以下
// wait
// {name: "new name"}
// componentDidUpdate
// componentDidMount
// 注意 componentDidUpdate 的输出位置,通常状况下
// componentDidUpdate 都是在componentDidMount 后面
// 执行的,可是这里由于setState 写在了 await 后面
// 因此状况相反。
复制代码
了解 react 生命周期和更新机制确实有利于编写代码,特别是当代码量愈来愈大时,错用的 setState 或生命周期钩子均可能埋下愈来愈多的雷,直到有一天没法维护。。。
个人我的建议以下:
getDerivedStateFromProps()
当成是 UNSAFE_componentWillReceiveProps()
的替代品,由于 getDerivedStateFromProps()
会在每次 render() 以前执行,即便 props 没有改变