setState 到底是同步的,还是异步的(1),2024年最新4面阿里拿到P7Offer

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Web前端全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024c (备注前端)
img

正文

});

console.log(‘triple setState后的count’, this.state.count)

}

reduce = () => {

setTimeout(() => {

console.log(‘reduce setState前的count’, this.state.count)

this.setState({

count: this.state.count - 1

});

console.log(‘reduce setState后的count’, this.state.count)

},0);

}

render(){

return 

点我增加

点我增加三倍

点我减少

}

}

接着我把组件挂载到 DOM 上:

import React from “react”;

import ReactDOM from “react-dom”;

import App from “./App”;

const rootElement = document.getElementById(“root”);

ReactDOM.render(

<React.StrictMode>

</React.StrictMode>,

rootElement

);

此时浏览器里渲染出来的是如下图所示的三个按钮:

此时有个问题,若从左到右依次点击每个按钮,控制台的输出会是什么样的?读到这里,建议你先暂停 1 分钟在脑子里跑一下代码,看看和下图实际运行出来的结果是否有出入。

如果你是一个熟手 React 开发,那么 increment 这个方法的输出结果想必难不倒你——正如许许多多的 React 入门教学所声称的那样,“setState 是一个异步的方法”,这意味着当我们执行完 setState 后,state 本身并不会立刻发生改变。因此紧跟在 setState 后面输出的 state 值,仍然会维持在它的初始状态(0)。在同步代码执行完毕后的某个“神奇时刻”,state 才会“恰恰好”地增加到 1。

但这个“神奇时刻”到底何时发生,所谓的“恰恰好”又如何界定呢?如果你对这个问题搞不太清楚,那么 triple 方法的输出对你来说就会有一定的迷惑性——setState 一次不好使, setState 三次也没用,state 到底是在哪个环节发生了变化呢?

带着这样的困惑,你决定先抛开一切去看看 reduce 方法里是什么光景,结果更令人大跌眼镜,reduce 方法里的 setState 竟然是同步更新的!这…到底是我们初学 React 时拿到了错误的基础教程,还是电脑坏了?

要想理解眼前发生的这魔幻的一切,我们还得从 setState 的工作机制里去找线索。

异步的动机和原理——批量更新的艺术


我们首先要认知的一个问题:在 setState 调用之后,都发生了哪些事情?你可能会更倾向于站在生命周期的角度去思考这个问题,得出一个如下图所示的结论:

从图上我们可以看出,一个完整的更新流程,涉及了包括 re-render(重渲染) 在内的多个步骤。re-render 本身涉及对 DOM 的操作,它会带来较大的性能开销。假如说“一次 setState 就触发一个完整的更新流程”这个结论成立,那么每一次 setState 的调用都会触发一次 re-render,我们的视图很可能没刷新几次就卡死了。这个过程如我们下面代码中的箭头流程图所示:

this.setState({

count: this.state.count + 1    ===>    shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate

});

this.setState({

count: this.state.count + 1    ===>    shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate

});

this.setState({

count: this.state.count + 1    ===>    shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate

});

事实上,这正是 setState 异步的一个重要的动机——避免频繁的 re-render

在实际的 React 运行时中,setState 异步的实现方式有点类似于 Vue 的 $nextTick 和浏览器里的 Event-Loop每来一个 setState,就把它塞进一个队列里“攒起来”。等时机成熟,再把“攒起来”的 state 结果做合并,最后只针对最新的 state 值走一次更新流程。这个过程,叫作“批量更新”,批量更新的过程正如下面代码中的箭头流程图所示:

this.setState({

count: this.state.count + 1    ===>    入队,[count+1的任务]

});

this.setState({

count: this.state.count + 1    ===>    入队,[count+1的任务,count+1的任务]

});

this.setState({

count: this.state.count + 1    ===>    入队, [count+1的任务,count+1的任务, count+1的任务]

});

合并 state,[count+1的任务]

执行 count+1的任务

值得注意的是,只要我们的同步代码还在执行,“攒起来”这个动作就不会停止。(注:这里之所以多次 +1 最终只有一次生效,是因为在同一个方法中多次 setState 的合并动作不是单纯地将更新累加。比如这里对于相同属性的设置,React 只会为其保留最后一次的更新)。因此就算我们在 React 中写了这样一个 100 次的 setState 循环:

test = () => {

console.log(‘循环100次 setState前的count’, this.state.count)

for(let i=0;i<100;i++) {

this.setState({

count: this.state.count + 1

})

}

console.log(‘循环100次 setState后的count’, this.state.count)

}

也只是会增加 state 任务入队的次数,并不会带来频繁的 re-render。当 100 次调用结束后,仅仅是 state 的任务队列内容发生了变化, state 本身并不会立刻改变:

“同步现象”背后的故事:从源码角度看 setState 工作流


读到这里,相信你对异步这回事多少有些眉目了。接下来我们就要重点理解刚刚代码里最诡异的一部分——setState 的同步现象

reduce = () => {

setTimeout(() => {

console.log(‘reduce setState前的count’, this.state.count)

this.setState({

count: this.state.count - 1

});

console.log(‘reduce setState后的count’, this.state.count)

},0);

}

从题目上看,setState 似乎是在 setTimeout 函数的“保护”之下,才有了同步这一“特异功能”。事实也的确如此,假如我们把 setTimeout 摘掉,setState前后的 console 表现将会与 increment 方法中无异:

reduce = () => {

// setTimeout(() => {

console.log(‘reduce setState前的count’, this.state.count)

this.setState({

count: this.state.count - 1

});

console.log(‘reduce setState后的count’, this.state.count)

// },0);

}

点击后的输出结果如下图所示:

现在问题就变得清晰多了:为什么 setTimeout 可以将 setState 的执行顺序从异步变为同步

这里我先给出一个结论:并不是 setTimeout 改变了 setState,而是 setTimeout 帮助 setState “逃脱”了 React 对它的管控只要是在 React 管控下的 setState,一定是异步的

接下来我们就从 React 源码里,去寻求佐证这个结论的线索。

时下虽然市场里的 React 16、React 17 十分火热,但就 setState 这块知识来说,React 15 仍然是最佳的学习素材。因此下文所有涉及源码的分析,都会围绕 React 15 展开。关于 React 16 之后 Fiber 机制给 setState 带来的改变,不在本讲的讨论范围内

解读 setState 工作流


我们阅读任何框架的源码,都应该带着问题、带着目的去读。React 中对于功能的拆分是比较细致的,setState 这部分涉及了多个方法。为了方便你理解,我这里先把主流程提取为一张大图:

接下来我们就沿着这个流程,逐个在源码中对号入座。首先是 setState 入口函数:

ReactComponent.prototype.setState = function (partialState, callback) {

this.updater.enqueueSetState(this, partialState);

if (callback) {

this.updater.enqueueCallback(this, callback, ‘setState’);

}

};

入口函数在这里就是充当一个分发器的角色,根据入参的不同,将其分发到不同的功能函数中去。这里我们以对象形式的入参为例,可以看到它直接调用了 this.updater.enqueueSetState 这个方法:

enqueueSetState: function (publicInstance, partialState) {

// 根据 this 拿到对应的组件实例

var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, ‘setState’);

// 这个 queue 对应的就是一个组件实例的 state 数组

var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);

queue.push(partialState);

//  enqueueUpdate 用来处理当前的组件实例

enqueueUpdate(internalInstance);

}

这里我总结一下,enqueueSetState 做了两件事:

  • 将新的 state 放进组件的状态队列里;

  • 用 enqueueUpdate 来处理将要更新的实例对象

继续往下走,看看 enqueueUpdate 做了什么:

function enqueueUpdate(component) {

ensureInjected();

// 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段

if (!batchingStrategy.isBatchingUpdates) {

// 若当前没有处于批量创建/更新组件的阶段,则立即更新组件

batchingStrategy.batchedUpdates(enqueueUpdate, component);

return;

}

// 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”

dirtyComponents.push(component);

if (component._updateBatchNumber == null) {

component._updateBatchNumber = updateBatchNumber + 1;

}

}

这个 enqueueUpdate 非常有嚼头,它引出了一个关键的对象——batchingStrategy,该对象所具备的isBatchingUpdates属性直接决定了当下是要走更新流程,还是应该排队等待;其中的batchedUpdates 方法更是能够直接发起更新流程。由此我们可以大胆推测,batchingStrategy 或许正是 React 内部专门用于管控批量更新的对象。

接下来,我们就一起来研究研究这个 batchingStrategy

/**

* batchingStrategy源码

**/

var ReactDefaultBatchingStrategy = {

// 全局唯一的锁标识

isBatchingUpdates: false,

// 发起更新动作的方法

batchedUpdates: function(callback, a, b, c, d, e) {

// 缓存锁变量

var alreadyBatchingStrategy = ReactDefaultBatchingStrategy.isBatchingUpdates

// 把锁“锁上”

ReactDefaultBatchingStrategy.isBatchingUpdates = true

if (alreadyBatchingStrategy) {

callback(a, b, c, d, e)

} else {

// 启动事务,将 callback 放进事务里执行

transaction.perform(callback, null, a, b, c, d, e)

}

}

}

batchingStrategy 对象并不复杂,你可以理解为它是一个“锁管理器”。

这里的“锁”,是指 React 全局唯一的 isBatchingUpdates 变量,isBatchingUpdates 的初始值是 false,意味着“当前并未进行任何批量更新操作”。每当 React 调用 batchedUpdate 去执行更新动作时,会先把这个锁给“锁上”(置为 true),表明“现在正处于批量更新过程中”。当锁被“锁上”的时候,任何需要更新的组件都只能暂时进入 dirtyComponents 里排队等候下一次的批量更新,而不能随意“插队”。此处体现的“任务锁”的思想,是 React 面对大量状态仍然能够实现有序分批处理的基石。

理解了批量更新整体的管理机制,还需要注意 batchedUpdates 中,有一个引人注目的调用:

transaction.perform(callback, null, a, b, c, d, e)

这行代码为我们引出了一个更为硬核的概念——React 中的 Transaction(事务)机制。

理解 React 中的 Transaction(事务) 机制


Transaction 在 React 源码中的分布可以说非常广泛。如果你在 Debug React 项目的过程中,发现函数调用栈中出现了 initializeperformclosecloseAll 或者 notifyAll 这样的方法名,那么很可能你当前就处于一个 Trasaction 中

Transaction 在 React 源码中表现为一个核心类,React 官方曾经这样描述它:Transaction 是创建一个黑盒,该黑盒能够封装任何的方法。因此,那些需要在函数运行前、后运行的方法可以通过此方法封装(即使函数运行中有异常抛出,这些固定的方法仍可运行),实例化 Transaction 时只需提供相关的方法即可。

React

  • 介绍一下react

  • React单项数据流

  • react生命周期函数和react组件的生命周期

  • react和Vue的原理,区别,亮点,作用

  • reactJs的组件交流

  • 有了解过react的虚拟DOM吗,虚拟DOM是怎么对比的呢

  • 项目里用到了react,为什么要选择react,react有哪些好处

  • 怎么获取真正的dom

  • 选择react的原因

  • react的生命周期函数

  • setState之后的流程

  • react高阶组件知道吗?

  • React的jsx,函数式编程

  • react的组件是通过什么去判断是否刷新的

  • 如何配置React-Router

  • 路由的动态加载模块

  • Redux中间件是什么东西,接受几个参数

  • redux请求中间件如何处理并发

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
好处

  • 怎么获取真正的dom

  • 选择react的原因

  • react的生命周期函数

  • setState之后的流程

  • react高阶组件知道吗?

  • React的jsx,函数式编程

  • react的组件是通过什么去判断是否刷新的

  • 如何配置React-Router

  • 路由的动态加载模块

  • Redux中间件是什么东西,接受几个参数

  • redux请求中间件如何处理并发

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-evhskIoe-1713386700881)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值