react 源码中的生命周期和事件系统
这一章我想跟大家探讨的是React
的生命周期
与事件系统
。
jsx 的编译结果
因为前面也讲到jsx
在v17
中的编译结果,除了标签名
,其他的挂在标签上的属性
(比如class
),事件
(比如click
事件),都是放在_jsxRuntime.jsx
函数的第二参数上。表现为key:value
的形式,这里我们就会产生几个问题。
react
是怎么知道函数体(事件处理函数)是什么的呢?react
又是在什么阶段去处理这些事件的呢?
这里我们先卖个关子,我们先来看看一个完整的React
应用的完整的生命周期是怎么样的,我们都知道React
分为类组件
与函数组件
,两种组件的部分生命周期函数发生了一些变化
,在这里我会分别对两种组件的生命周期做讲解。
React 组件的生命周期
组件挂载的时候的执行顺序
因为在_jsxRuntime.jsx
编译jsx
对象的时候,我们会去做处理defaultProps
和propType
静态类型检查。所以这也算是一个生命周期吧。Class
组件具有单独的constructor
,在mount
阶段会去执行这个构造函数,我曾经做了部分研究,这个constructor
是类组件独有的,还是class
独有的?后来发现这个constructor
是class
独有的,怎么理解这句话呢?
在《重学 ES6》这本书中提到:
ES6
中新增了类的概念,一个类必须要有constructor
方法,如果在类中没有显示定义,则一个空的constructor
方法会被默认添加。对于ReactClassComponent
来讲需要constructor
的作用就是用来初始化state
和绑定事件,另外一点就是声明了constructor
,就必须要调用super
,我们一般用来接收props
传递。假如你不写constructor
,那就没法用props
了,当然了要在constructor
中使用props
,也必须用super
接收才行。所以对于类组件来讲的话,
constructor
也算是一个生命周期钩子。
getDerivedStateFromProps
会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null
则不更新任何内容。
render
被调用时,它会检查 this.props
和 this.state
的变化并返回以下类型之一:
React 元素。通常通过 JSX 创建。例如,
<div />
会被 React 渲染为 DOM 节点,<MyComponent />
会被 React 渲染为自定义组件,无论是<div />
还是<MyComponent />
均为 React 元素。数组或 fragments。 使得 render 方法可以返回多个元素。
Portals。可以渲染子节点到不同的 DOM 子树中。
字符串或数值类型。它们在 DOM 中会被渲染为文本节点。
**布尔类型或
null
**。什么都不渲染。(主要用于支持返回test && <Child />
的模式,其中 test 为布尔类型。)
componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。依赖于 DOM 节点的初始化应该放在这里。在这里适合去发送异步请求。
组件更新的时候的执行顺序
getDerivedStateFromProps
=> shouldComponentUpdate()
=> render()
=> getSnapshotBeforeUpdate()
=> componentDidUpdate()
其中
shouldComponentUpdate
也被称作为性能优化的一种钩子,其作用在于比较两次更新的state
或props
是否发生变化,决定是否更新当前组件,比较的方式是浅比较
,以前讲过这里不再复述。而
getSnapshotBeforeUpdate
函数在最近一次渲染输出(提交到DOM
节点)之前调用。它使得组件能在发生更改之前从DOM
中捕获一些信息。此生命周期方法的任何返回值将作为参数传递给componentDidUpdate()
。componentDidUpdate()
会在更新后会被立即调用。首次渲染不会执行
此方法。
组件卸载的时候执行顺序
componentWillUnmount()
会在组件卸载
及销毁
之前直接调用。在此方法中执行必要的清理操作,例如,清除timer
,取消网络请求
等等。
组件在发生错误的时候执行顺序
getDerivedStateFromError
=> componentDidCatch
关于这两个钩子,同学们可自行移步官网。
当然上面的只是ClassComponent
的生命周期执行顺序,而在新版本的 React 中已经删除掉了componentDidMount
、componentDidUpdate
、componentWillUnMount
,取而代之的是useEffect
、useLayoutEffect
。那究竟是谁代替了他们三个呢?这个问题我已经在 React 源码解析系列(八) -- 深入 hooks 的原理 中阐述过了,这里不再复述。
现在来回答第一个问题:react 是怎么知道函数体是什么的呢? 这个问题其实问的非常好,babel
解析后的jsx
本身只会去关注{事件名:函数名}
,但是每一个事件都是需要被注册、绑定的,然后通过事件触发,来执行绑定函数的函数体。解释这种问题还是得要去看一下源码里面的具体实现。
listenToAllSupportedEvents
我们在 React 源码解析系列(二) -- 初始化组件的创建更新流程中提到rootFiber
与FiberRoot
的创建,创建完毕之后我们就需要去创建事件,创建事件的入口函数为listenToAllSupportedEvents
。
我们在这里必须要关注一下allNativeEvents
是什么东西,allNativeEvents
在源码里体现为一个存储着事件名的Set
结构:
接下来看看listenToNativeEvent
究竟干了些什么。
相关参考视频讲解:进入学习
listenToNativeEvent
在这里我们关注一下获取优先级getEventPriorityForPluginSystem
这里,你会不会产生一个疑问,React
内部事件我们知道React
本身一定会给优先级的,但是非React
事件呢,比如原生事件
,他们的优先级是怎么确定的呢?不要急,我们看一看就知道了。
getEventPriorityForPluginSystem
eventPriorities
本身是一个 Map 结构,我们可以发现两个地方进行了eventPriorities.set()
的操作。
这就说明,在这两个函数里面已经做好了优先级的处理,那我们可以去看一下在哪里调用的这两个函数,我们发现在函数registerSimpleEvents
中,执行了这两个函数,往eventPriorities
里面添加优先级。
上述代码中可以看到有非常多的Plugin
,代码如下:
而在registerSimplePluginEventsAndSetTheirPriorities
函数里面,我们发现了注册事件registerTwoPhaseEvent
,我们继续去深究一下,究竟是怎么注册的。
registerTwoPhaseEvent
registerDirectEvent
前面说allNativeEvents
是一个存储事件名的Set
,这里往里面添加事件名
,就完成了事件注册
。还没有完,上面说过了事件注册,与事件绑定,但是用户点击的时候,应该怎么去触发呢?上面的代码,在获取了优先级之后,每个事件会根据当前优先级生成一个listenerWrapper
,这个listenerWrapper
也就是对应的事件触发绑定的函数。dispatchDiscreteEvent
、dispatchUserBlockingUpdate
、dispatchEvent
三个函数都通过 bind 执行,我们都知道 bind 绑定的函数,会返回一个新函数,并不会立即执行。所以我们也必须看看他的入参是什么。
this
:null
argments
:domEventName
:事件名,eventSystemFlags
:事件类型标记,targetContainer
:目标容器。
dispatchEvent
因为不管是dispatchDiscreteEvent
、dispatchUserBlockingUpdate
最后都会去执行dispatchEvent
,所以我们可以看看他的实现。
dispatchEventsForPlugins
所以到这里,React
的事件系统就解析完了,在这里上面的问题就很好解答了,React
对事件名与事件处理函数对做了绑定,并在创建rootFiber
的时候就做了事件注册
、事件绑定
、事件调度
。那么他们的执行流程大致如下:
总结
这一章主要是介绍组件在mount
、update
、destroy
阶段的生命周期执行顺序与React
事件系统的注册,绑定,调度更新等
评论