七、生命周期
组件的生命周期可分成三个状态:
- Mounting(挂载,初始化):已插入真实 DOM
- Updating(更新,运行时):正在被重新渲染
- Unmounting(卸载,销毁):已移出真实 DOM
生命周期图谱可以参考链接:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
类组件如何实现类似vue的计算属性: https://zh-hans.reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#what-about-memoization
$ cnpm i memoize-one -S
src/index.js
// src/index.js import React from 'react' import ReactDOM from 'react-dom/client' // 引入react组件时,后缀名可以不写 // import App from './01_props/01_Parent_Child' // 父子组件 // import App from './01_props/02_Parent_Child_value' // 父组件给子组件传值 // import App from './01_props/03_Parent_Child_default' // 父组件给子组件传值,子组件设置默认值 // import App from './01_props/04_Parent-Child_type' // 父组件给子组件传值,子组件设置默认值,验证数据类型 // import App from './01_props/05_App_props_children' // 类插槽 // import App from './01_props/06_App_mutiple_props_children' // 类具名插槽 多个插槽 // import App from './01_props/07_App_mouse_tracker' // 鼠标跟随 // import App from './01_props/08_App_render_props' // 渲染属性 - 其他组件共享状态 // import App from './02_state/01_App_state_es6' // 初始化状态 - es6的构造函数 // import App from './02_state/02_App_state_es7' // 初始化状态 - es7 属性初始化器 // import App from './02_state/03_App_setState_function' // 修改状态 传递函数 // import App from './02_state/04_App_setState_object' // 修改状态 传递对象 import App from './02_state/05_App_computed' // 类组件实现类似于vue的计算属性 const root = ReactDOM.createRoot(document.querySelector('#root')) root.render(<App />)
src/02_state/05_App_computed.jsx
// src/02_state/05_App_computed.jsx // 输入框输入关键词,列表展示相关数据 import React, { Component } from 'react' class Search extends Component { state = { word: '', count: 0 // 为了验证计算属性 } render () { console.log('执行了') const filterList = this.props.list.filter(item => { return item.includes(this.state.word) && this.state.word !== '' }) console.log(filterList) return ( <div> <button onClick={ () => { this.setState({ count: this.state.count + 1 }) } }>加1</button>{ this.state.count } <br/> <input onChange={ (event) => { this.setState({ word: event.target.value }) } } value = { this.state.word }/> { this.state.word } <ul> { // 不要在jsx代码中使用forEach,使用map filterList.map((item, index) => { return <li key = { index }>{ item }</li> }) } </ul> </div> ) } } export default class App extends Component { state = { list: [ '你好呀', '很高兴为你服务', '好高兴', '你中午吃啥' ] } render() { return ( <div> <Search list = { this.state.list }/> </div> ) } } // src/02_state/05_App_computed.jsx // 输入框输入关键词,列表展示相关数据 import React, { Component } from 'react' import computed from 'memoize-one' class Search extends Component { state = { word: '', count: 0 // 为了验证计算属性 } filter = computed((list, word) => { console.log('11111') // 只有 list 和word 改变时才会重新计算 return list.filter(item => { return item.includes(word) && word !== '' }) }) render () { console.log('执行了') // const filterList = this.props.list.filter(item => { // return item.includes(this.state.word) && this.state.word !== '' // }) const filterList = this.filter(this.props.list, this.state.word) // console.log(filterList) return ( <div> <button onClick={ () => { this.setState({ count: this.state.count + 1 }) } }>加1</button>{ this.state.count } <br/> <input onChange={ (event) => { this.setState({ word: event.target.value }) } } value = { this.state.word }/> { this.state.word } <ul> { // 不要在jsx代码中使用forEach,使用map filterList.map((item, index) => { return <li key = { index }>{ item }</li> }) } </ul> </div> ) } } export default class App extends Component { state = { list: [ '你好呀', '很高兴为你服务', '好高兴', '你中午吃啥' ] } render() { return ( <div> <Search list = { this.state.list }/> </div> ) } }
7.1 三个阶段
7.1.1 装载阶段
当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
-
constructor()
: 在 React 组件挂载之前,会调用它的构造函数。如果不需要对类组件添加初始化数据以及绑定事件,那么就不需要写
constructor
-
static getDerivedStateFromProps()
: 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。 -
render()
: render() 方法是 class 组件中唯一必须实现的方法。 -
componentDidMount()
: 在组件挂载后(插入 DOM 树中)立即调用。
render() 方法是 class 组件中唯一必须实现的方法,其他方法可以根据自己的需要来实现。
7.1.2 更新阶段
每当组件的 state 或 props 发生变化时,组件就会更新。
当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
static getDerivedStateFromProps()
: 在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。shouldComponentUpdate()
:当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。render()
: render() 方法是 class 组件中唯一必须实现的方法。getSnapshotBeforeUpdate()
: 在最近一次渲染输出(提交到 DOM 节点)之前调用。componentDidUpdate()
: 在更新后会被立即调用,如果你需要执行副作用(例如,数据提取或动画)以响应 props 中的更改。
render() 方法是 class 组件中唯一必须实现的方法,其他方法可以根据自己的需要来实现。
7.1.3 卸载阶段
当组件从 DOM 中移除时会调用如下方法:
componentWillUnmount()
: 在组件卸载及销毁之前直接调用。
7.1.4 Error boundaries
Error boundaries 是 React 组件,它会在其子组件树中的任何位置捕获 JavaScript 错误,并记录这些错误,展示降级 UI 而不是崩溃的组件树。Error boundaries 组件会捕获在渲染期间,在生命周期方法以及其整个树的构造函数中发生的错误。
项目中需要使用的最多的生命周期的钩子函数为
render
,componentDidMount
,componentDidUpdate
,componentWillUnmount
详细介绍范例:https://zhuanlan.zhihu.com/p/392532496
src/index.js
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
// 引入react组件时,后缀名可以不写
// import App from './01_props/01_Parent_Child' // 父子组件
// import App from './01_props/02_Parent_Child_value' // 父组件给子组件传值
// import App from './01_props/03_Parent_Child_default' // 父组件给子组件传值,子组件设置默认值
// import App from './01_props/04_Parent-Child_type' // 父组件给子组件传值,子组件设置默认值,验证数据类型
// import App from './01_props/05_App_props_children' // 类插槽
// import App from './01_props/06_App_mutiple_props_children' // 类具名插槽 多个插槽
// import App from './01_props/07_App_mouse_tracker' // 鼠标跟随
// import App from './01_props/08_App_render_props' // 渲染属性 - 其他组件共享状态
// import App from './02_state/01_App_state_es6' // 初始化状态 - es6的构造函数
// import App from './02_state/02_App_state_es7' // 初始化状态 - es7 属性初始化器
// import App from './02_state/03_App_setState_function' // 修改状态 传递函数
// import App from './02_state/04_App_setState_object' // 修改状态 传递对象
// import App from './02_state/05_App_computed' // 类组件实现类似于vue的计算属性
import App from './02_state/06_App_lifeCycle' // 生命周期
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
// try catch
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
// logErrorToMyService(error, errorInfo);
console.log(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(
<ErrorBoundary>
<App root={root}/>
</ErrorBoundary>
)
src/02_state/06_App_lifeCycle.jsx
// src/02_state/06_App_lifeCycle.jsx
import React, { Component } from 'react';
// 挂载阶段 - 初始化阶段
// constructor 初始化状态以及事件绑定
// static getDerivedStateFromProps 让组件在 props 变化时更新 state
// render
// componentDidMount 等同于vue中的mounted
// 更新阶段 - 运行时阶段
// static getDerivedStateFromProps
// shouldComponentUpdate react组件提升性能的关键 返回值false 表示不更新
// render
// getSnapshotBeforeUpdate
// componentDidUpdate 除了类似vue的updated,还可以实现watch的功效,条件请求数据
// 卸载阶段 - 销毁阶段
// componentWillUnmount
class App extends Component {
// constructor (props) { // Useless constructor
// super(props)
// }
state = { count: 100 }
// static getDerivedStateFromProps (props, state) { // 一般不使用
// // getDerivedStateFromProps 会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。
// // 它应返回一个对象来更新 state,如果返回 null 则不更新任何内容
// // state 的值在任何时候都取决于 props
// }
componentDidMount () {
// 等同于 vue中的 mounted
// 数据请求,实例化操作,DOM操作,定时器,计时器,订阅数据变化
// 修改状态
this.setState({ count: this.state.count + 100 })
}
shouldComponentUpdate (nextProps, nextState) {
// 可以作为react组件的性能优化的手段,但是也要慎用
// return true
return false
}
// getSnapshotBeforeUpdate(prevProps, prevState) {
// // 在最近一次渲染输出(提交到 DOM 节点)之前调用。
// // 它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。
// // 此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。
// }
componentDidUpdate(prevProps, prevState, snapshot) {
// 参照vue中 updated
// 实例化操作,DOM操作, 特定条件可以请求数据以及修改数据
// if (this.props.userID !== prevProps.userID) { // 监听数据的变化
// this.fetchData(this.props.userID);
// }
}
componentWillUnmount () {
// 参照vue beforeDestory
// 清理对象,取消订阅,消除定时器计时器
// 当count的值等于210的时候销毁组件
}
render() {
// 挂载阶段 依据初始化数据渲染数据
// 更新阶段 当该组件的状态或者属性发生改变时触发此函数,也就输数据的改变引起视图的二次渲染
return (
<div>
<p>{ this.state.count }</p>
<button onClick={ () => {
if (this.state.count === 210) {
console.log('销毁组件')
// 销毁组件
this.props.root.unmount()
} else {
this.setState({ count: this.state.count + 1 } )
}
} }>加</button>
</div>
);
}
}
export default App;
7.2 两个时期
将应用的渲染过程分为mount
阶段(应用首次渲染)和update
阶段(应用状态更新),无论在mount
阶段还是update
阶段,都会经历两个子阶段,一个是render
阶段,一个是commit
阶段。
mount时:在render
阶段会根据jsx对象构建新的workInProgressFiber
树,然后将相应的fiber
节点标记为Placement
,表示这个fiber
节点需要被插入到dom
树中,然后会这些带有副作用的fiber
节点加入一条叫做Effect List
的链表中。在commit
阶段会遍历render
阶段形成的Effect List
,执行链表上相应fiber
节点的副作用,比如Placement
插入,或者执行Passive
(useEffect的副作用)。将这些副作用应用到真实节点上update时:在render
阶段会根据最新状态的jsx对象对比current Fiber
,再构建新的workInProgressFiber
树,这个对比的过程就是diff算法
,diff算法
又分成单节点的对比和多节点的对比,对比的过程中同样会经历收集副作用的过程,也就是将对比出来的差异标记出来,加入Effect List
中,这些对比出来的副作用例如:Placement
(插入)、Update
(更新)、Deletion
(删除)等。在commit
阶段同样会遍历Effect List
,将这些fiber节点上的副作用应用到真实节点上。
参考链接: https://blog.csdn.net/bemystery/article/details/121897223
7.3 入门理解React Fiber架构
在 React 16 之前,VirtualDOM
的更新采用的是Stack
架构实现的,也就是循环递归方式。不过,这种对比方式有明显的缺陷,就是一旦任务开始进行就无法中断,如果遇到应用中组件数量比较庞大,那么VirtualDOM
的层级就会比较深,带来的结果就是主线程被长期占用,进而阻塞渲染、造成卡顿现象。
为了避免出现卡顿等问题,我们必须保障在执行更新操作时计算时不能超过16ms,如果超过16ms,就需要先暂停,让给浏览器进行渲染,后续再继续执行更新计算。而Fiber
架构就是为了支持“可中断渲染”而创建的。
在React
中,Fiber
使用了一种新的数据结构fiber tree
,它可以把虚拟dom tree
转换成一个链表,然后再执行遍历操作,而链表在执行遍历操作时是支持断点重启的,示意图如下。
官方介绍中,Fiber
被理解为是一种数据结构,但是我们也可以将它理解为是一个执行单元。
Fiber
可以理解为一个执行单元,每次执行完一个执行单元,React Fiber
就会检查还剩多少时间,如果没有时间则将控制权让出去,然后由浏览器执行渲染操作。React Fiber
与浏览器的交互流程如下图。
可以看到,React
首先向浏览器请求调度,浏览器在执行完一帧后如果还有空闲时间,会去判断是否存在待执行任务,不存在就直接将控制权交给浏览器;如果存在就会执行对应的任务,执行完一个新的任务单元之后会继续判断是否还有时间,有时间且有待执行任务则会继续执行下一个任务,否则将控制权交给浏览器执行渲染,这个流程是循环进行的。
所以,我们可以将Fiber
理解为一个执行单元,并且这个执行单元必须是一次完成的,不能出现暂停。并且,这个小的执行单元在执行完后计算之后,可以移交控制权给浏览器去响应用户,从而提升了渲染的效率。
在官方的文档中,Fiber
被解释为是一种数据结构,即链表结构。在链表结构中,每个 Virtual DOM
都可以表示为一个 fiber
,如下图所示。通常,一个 fiber
包括了 child
(第一个子节点)、sibling
(兄弟节点)、return
(父节点)等属性,React Fiber
机制的实现,就是依赖于上面的数据结构。
通过介绍,我们知道Fiber
使用的是链表结构,准确的说是单链表树结构。为了方便理解 Fiber
的遍历过程,下面我们就看下Fiber
链表结构。
在上面的例子中,每一个单元都包含了payload
(数据)和nextUpdate
(指向下一个单元的指针)两个元素
参考链接:https://segmentfault.com/a/1190000042271919