生命周期
React 16.4.x
版本问题
- 版本的更新会带来很多变化,不同的版本要看不同的文档,这里暂时以此时最新的16.4为例。
componentWillMount
、compoentWillUpdate
、componentWillReceiveProps
这几个生命周期已经列为不推荐。
简述
- 初始化:
- constructor
- static getDerivedStateFromProps
- UNSAFE_componentWillMount
- render
- componentDidMount
- 更新阶段update(props或者state改变引起)
- UNSAFE_componentWillReceiveProps
- static getDerivedStateFromProp
- shouldComponentUpdate
- UNSAFE_componentWillUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
- 销毁
- componentWillUnmount
- 错误处理
- componentDidCatch
render
- react组件唯一一个不能缺少的生命周期
- 应该是pure function
- 不应该直接和browser直接交互,如果有需要,应该在DidMount或者其他生命周期去做。
constructor
- 需要初始化state和绑定方法的时候才需要写
- 内部不可以调用
setState
,在其他地方才调用 - 其他有副作用或者订阅的东西,应该放在DidMount里
- 一个常见“误区”是:在初始化state的时候,使用props的值来赋值。
- 其中一个原因是本来就可以在需要的地方直接用props
- 另外一个原因是这样赋值,state是不会跟着props改变而改变的,对于可能的出发点而言是会落空的
- 当然,如果你可以要忽略以上的问题,其实是可以这么做的,相当于记录了props的初始值,比如用一个initialXXX的命名
componentDidMount
- 触发:是组件被渲染进组件树(注意不是渲染到浏览器里)
- 定位:
- 组件初始化里,需要对dom nodes做操作的部分,应该放在这个生命周期里
- 又比如需要从remote endpoint获取数据的话,就很适合放在这里
- 还可以放一些订阅在这里,但是需要注意在UnMount里去取消
- 可以在这里setState,会触发一次额外的render,而且用户感知不到中间状态(发生在浏览器升级视图之前),但是可能会引发一些问题,而且,通常来说,最好在constructor里去做好state的初始化。
- 如果需要去度量一个dom node,以便在此基础上(尺寸、位置)去render别的东西的话,可以在这里做。
componentDidUpdate
- 触发:
- 在updating发生之后触发,
- 首次render不会触发,
- 如果
shouldComponentUpdate
返回false也不会触发了。
- 参数:
prevProps prevState, snapshot
,注意,在内部还可以通过this.props来获取当前的props!所以可以很轻易的做对比!非一般的方便。(第三个参数snapshot
是生命周期getSnapshotBeforeUpdate
返回的值,如果没有这个生命周期,那就是undefined) - 定位:
- 在update之后需要对dom操作的时候
- 也可以针对前后props是否发生变化,来决定是否发起网络请求以更新数据
- 注意:
- 如果要在这里setState,一定要做一层判断包装(边界条件),否则会infinite loop。另外,这样也会触发一次render
- 如果只是为了mirror一些来祖先的props,还是建议直接用props
componentWillUnmount
- 触发:组件unmounted和destroy之后,是在dom移除之后而不是移除之前
- 定位:
- cleanup,比如无效化一些定时器,取消一些网络请求,清除在didMount钩子里被添加的订阅
- 注意:
- 不要setState,因为组件不会再render
——下面是其他一些很少被使用的生命周期:有时候很有效,但大部分情况下他们不出场——
shouldComponentUpdate
- 触发:
- 顾名思义
- 首次render的时候是不会触发的
- 如果update的时候调用了
forceUpdate()
,也会绕过这个生命周期
- 参数:
nextProps nextState
,和上面的DidUpdate类似,还可以在内部通过this.state或者this.props来比较 - 定位:
- 当state和props发生改变的时候,告诉react是否应该改变视图,默认返回的是true
- 仅仅为了优化性能而存在,
- 注意:
- 不应该用作阻止某次render,那可能会导致bug。作为替代,应该使用某些内置的PureComponent,这里我的理解是在组件内写一些渲染函数,然后在函数内部去做一些判断、比较。
- return false的时候也只是防止自己update,内部的子组件不受他影响,他们是否render取决于他们自己的state变化!
- 官方不建议做deep equality checks或者使用JSON.stringify,因为那样不仅低效而且对表现是有害的
- return false后,willUpdate和render和didUpdate就不会触发了
- 未来官方可能会修改这个钩子,不论返回什么还是触发re-render,只把她作为一个hint而非directive(指令)
- 必须有return,
true
或false
static getDerivedStateFromProps
- 触发:在render前被触发,触发后render马上就会触发
- 参数:
props state
- 定位:
- 返回一个obj来刷新state,或者返回null
- 当state依赖于props随时间的改变时候,可能会用上(目前我还理解不到位,从官方文档建议的代替这个钩子的方案里,我看到的是对props的一种包装,也就是在render里赋值一个变量,变量的值是包含props的表达式,看起来像vue的计算属性)
- 注意:
- 虽然参数可以让你访问到state和props,但是需要注意的是,这个方法访问不到组件的实例本身!也就是说,他访问不到组件内部除了state和props意外的其他任何代码!所以如果非要用这个钩子,有时候就要对全部的代码做改造——在组件的class定义之外,定义一些pure function,作为可以在组件内部和这个钩子内部都能被调用的代码,来减少重复的代码。
- 区别于componentWillReceiveProps——只会在父组件传来的props引起re-render时触发,不会响应本身的setState;这里的方法会在每一次render之前触发,不论是否有props改变了
- 官方推荐使用这个代替componentWillReceiveProps
- 官方认为如果你需要增加一个side-effect的话,应该采用
componentDidUpdate
getSnapshotBeforeUpdate
- 触发:在最近的一次render即将渲染到dom上之前
- 参数:
prevProps prevState
- 定位:
- 让用户可以在dom被某次render实际的改变之前,去获取dom上的一些数据
- 其return的值会变成
componentDidUpdate
的第三个参数snapshot
- 注意:
- 和
shouldComponentUpdate
一样,必须有return,区别是可以返回null
- 和
componentDidCatch
- 参数:
error info
- 角色定位:
- 使用这个生命周期,将使组件形成一个错误边界error boundary
- 在子组件的js上捕获任何错误后,不是让ui直接crash,而是渲染一个fallback UI
- 注意:
- 成为错误边界后,只会捕获自己以下的tree的错误
- 本身内部不能捕获错误
——即将退休的一些生命周期,已经被“不建议在新的代码里使用”——
注意到都有前缀UNSAFE,实际证明:调用形如UNSAFE_componentWillMount
也是能运行的,也许允许增加前缀就是为了方便以后被废弃的时候能快速定位并且迭代更新。这是一个很赞的点。——我继续看文档,发现不仅是让开发者快速定位,将来可能可以在更新react之后自动被迭代掉!这就更赞了。这些生命周期被设为UNSAFE就是因为可能会意外的在其中做了一些有副作用(side effect)。
componentWillMount
- 定位:
- 服务端渲染的唯一一个生命周期
- 触发:在mounting开始前
- 注意:
- 在这里setState不会导致多触发一次额外的render。如果只是想初始化state的话建议在
constructor
- 不要在这引入一些有副作用的东西或者增加订阅,应该在
componentDidMount
- 会支持到react17
- 在这里setState不会导致多触发一次额外的render。如果只是想初始化state的话建议在
componentWillReceiveProps
- 触发:
- 在一个已经mount的组件收到了props的新值的时候,
- 如果是父组件引起子组件re-render,那么即使props没有变化,子组件的这个生命周期也会触发!
- 首次render的时候是不会触发的
- 参数:
nextProps
- 注意:
- 经常导致bug和不一致性,所以会被废弃
- 如果需要在props改变后做一些特殊的side effect,可以用
componentDidUpdate
代替 - 会支持到react17
componentWillUpdate
- 触发:
- 在props或者state变化之后和render之前之间,
- 首次render的时候是不会触发的
- 如果
shouldComponentUpdate
返回false,就不会触发
- 定位:
- 用于在update之前做一些准备
- 参数:
nextProps nextState
- 注意:
- 不能在其中调用
setState
,不能在其return之前做任何可能引发update动作的事。 - 有时候可以被
componentDidUpdate
替代 - 如果需要操作dom,可以用
getSnapshotBeforeUpdate
代替
- 不能在其中调用
和生命周期有关的一些API
setState
参数:
setState(updater [, callback])
updater应该是是一个函数:
(prevState, props) => stateChage
,其中prevState不应该直接去突变,而应该在prevState和props的基础上重新构造一个新对象,然后把返回的对象和prevState进行浅合并得到新state,例如:(这就是为什么之前,即使在直接返回的对象里写...this.state
也能正确更新state的原因,因为它本来就会自动进行浅合并)1
this.setState((prevState, props) => {return {counter: prevState.counter + props.step}});
事实上,也可以用一个对象
stateChage
来代替第一个参数updater
函数。和上面一样,也是直接把这个对象浅合并到新state里去。值得注意的是,这样写和上面一样,也是异步的。对第一个参数,如果在setState里多次调用对state的改变会被“批量处理”——就是在这个对象里多次对state里的同一个东西做重复赋值,则前改动会被后改动覆盖,state不会改变多次,只会改变最后一次。就如下面的原理:
1
Object.assign(previousState, {quantity: state.quantity + 1}, {quantity: state.quantity + 1})
callback是可选的,通常可以用在componentDidUpdate里写逻辑来替代。
定位:
- 将state的改变加入队列,告知组件和子组件应该根据最新的state进行re-render,除非
shouldComponentUpdate
返回false - 是根据事件触发或者服务端响应来改变ui的主要手段(x in response to y :x响应y)
- 将state的改变加入队列,告知组件和子组件应该根据最新的state进行re-render,除非
- 注意:
- 应该将setState当作一个request而非一个即时命令,它可能会延迟或者批量处理,所以在setState之后立即读取this.state是有问题的!,应该在callback或者在
componentDidUpdate
里去获取最新的state - 只要调用setState,就会触发re-render,所以如果不方便对state的前后做对比(可变的对象或者说在shouldComponentUpdate里不能实现根据条件判断的逻辑),就应该在state前后值不同的时候再调用他,以避免不必要的render。——为了性能
- 应该将setState当作一个request而非一个即时命令,它可能会延迟或者批量处理,所以在setState之后立即读取this.state是有问题的!,应该在callback或者在
forceUpdate
- 用法:
component.forceUpdate(callback)
- 定位:强行触发render。
- re-render一般是被state或者props的改变触发的
- 如果render依赖一些其他的数据或者触发(非state和props),可以在那里通过这个方法强制触发re-render
- 注意:
- 这个方法会跳过
shouldComponentUpdate
! - 这个方法会触发子组件的所有生命周期,而且不会跳过子组件的
shouldComponentUpdate
,可以说挺奇葩了 - 官方不建议使用这个api,而是让react自己监听props和state来更新
- 这个方法会跳过