这是一篇给小白的,极为通俗易懂的个人理解总结。
当年初学 react,学习 setState 机制,总是被一堆源码中的函数名搞的一头雾水。
这里没有任何源码,只有经过个人理解,总结出来的简易流程。
希望能够帮助到,和曾经的自己(现在也差不多),一样想要精进、却举步维艰的你们~
我们从一道常见的面试题入手:
react 的合成事件,是如何映射到真实 dom 上的?
我们以一个简版的例子,看一下从用户点击动作开始,都发生了什么~
定义一个container:App
import React, { Component } from 'react'
class App extends Component {
constructor(props) {
super(props)
this.state = {
count: 0,
}
this.clickHandler = this.clickHandler.bind(this)
}
clickHandler() {
console.log('count1:', this.state.count)
this.setState({
count: 1,
})
console.log('count2:', this.state.count)
this.setState({
count: 2,
})
console.log('count3:', this.state.count)
}
render() {
console.log('count render', this.state.count)
return <button onClick={this.clickHandler}>点我更新count</button>
}
}
export default App
旅程开始~
1.前言:元素挂载
点击元素前,得先有元素。so,先简单看一下元素挂载过程~
ReactDOM.render() 方法,解析 jsx 生成 virtual dom tree(React16中,又由 virtual dom tree 生成 fiber tree),最终将真实的节点渲染到页面上。
在这个过程中,react 内部,会在每个真实的 dom 元素上,偷偷地添加一些属性,用来配合它搞一些事情。
其中,除了root根节点,其他所有元素,都被添加了这么个属性:__reactEventHandlers$*******
这里,我们直接读取了 button 元素的这个属性。
似乎有点剧透了,咳咳~ ┓( ´∀` )┏
下面,进入正题👇
2.事件触发
react 基于事件冒泡,统一在 document 上插入了原生的事件监听方法,用于捕获页面任意位置的用户操作。
【注】
经实测,这里给 document 插入的原生事件监听,取决于子元素设置了什么合成事件监听。
每个合成事件都有对应的原生事件,以此给 document 添加上需要的原生事件监听函数。
当用户点击 button,click 事件顺着 dom 树结构冒泡到 document 上,document 上的原生 onclick 事件响应函数被触发。
这个响应函数做了什么事呢?
大概是这样:
function documentClickHandler(e) {
// 获取真正触发点击事件的元素节点
const target = e.target
// 执行元素节点上注册的合成事件响应函数
target.__reactEventHandlers$*******.onClick()
}
ok,事情的本质,其实就是这么简单。
到这里,那个面试题的答案,已经很清晰了。
但是整个流程并没有结束,我们继续,看看 setState 这个小家伙,一会儿同步、一会儿异步,到底是在干啥。
3.执行合成事件响应函数:clickHandler
clickHandler() {
console.log('count1:', this.state.count)
this.setState({
count: 1,
})
console.log('count2:', this.state.count)
this.setState({
count: 2,
})
console.log('count3:', this.state.count)
}
这里,连续调用了两次 setState。但是,打印出的 count1、count2、count3,全部为 0。
这就是典型的,所谓 setState 的异步现象:调用 setState 后,state 的值并没有立即更新。
4.合成事件中的 setState
以下代码,由个人对这个流程的简化理解而来,全部是伪代码。并非由源码精简而来。
这里的简单例子,只为简单说明整体逻辑流程。
如有理解偏差,烦请指正。
let flag = false // 相当于源码的 isBatchingUpdates
function documentClickHandler(e) {
// 获取真正触发点击事件的元素节点
const target = e.target
// 设置标记
flag = true
// 执行元素节点上注册的合成事件响应函数
target.__reactEventHandlers$*******.onClick()
// 置回标记
flag = false
// 触发 setState,批量更新在 onClick 中缓存的那些 state
setState()
}
const arr = [ ] // 相当于源码的 dirtyComponents
// 开发者调用的setState,也是它👇
function setState(state) {
// 如果标记是 true,就先不做更新。缓存当前 state,等到 flag 为 false 的时候,做批量更新
if (flag) {
arr.push(state)
} else {
// 这里,没有传 state,就说明是合成事件执行完,调用进来的。批量更新缓存的那些 state
if (!state) {
let stateObj = {}
arr.forEach(state => {
stateObj = Object.assign(stateObj, state)
})
// 真正执行 setState(我臆想的方法。。总之应该会有个类似的东西。。)
doSetState(stateObj)
} else {
doSetState(state)
}
}
}
【注】
react 生命周期中的 setState,同理。
比如 componentDidMount 中连续调用了两次 setState,在组件挂载过程中,需要执行 componentDidMount 这个生命周期时:
doRender() {
// 做各种挂载需要的逻辑
// ......
// 挂载完成,执行生命周期函数。和合成事件的执行同理,用 flag 标记,告诉 setState 走批量更新
flag = true
componentDidMount()
flag = false
}
ok,旅程结束~
如有理解偏差,请大佬们指教。感谢。
网友评论