【React】每个人都可以理解的合成事件与setState机制

这是一篇给小白的,极为通俗易懂的个人理解总结。

当年初学 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,旅程结束~

如有理解偏差,请大佬们指教。感谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。