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:nullargments:domEventName:事件名,eventSystemFlags:事件类型标记,targetContainer:目标容器。
dispatchEvent
因为不管是dispatchDiscreteEvent、dispatchUserBlockingUpdate最后都会去执行dispatchEvent,所以我们可以看看他的实现。
dispatchEventsForPlugins
所以到这里,React的事件系统就解析完了,在这里上面的问题就很好解答了,React对事件名与事件处理函数对做了绑定,并在创建rootFiber的时候就做了事件注册、事件绑定、事件调度。那么他们的执行流程大致如下:
总结
这一章主要是介绍组件在mount、update、destroy阶段的生命周期执行顺序与React事件系统的注册,绑定,调度更新等










评论