setState 的更新是同步还是异步,一直是人们津津乐道的话题。不过,实际上如果我们需要用到更新后的状态值,并不需要强依赖其同步/异步更新机制。在类组件中,我们可以通过this.setState的第二参数、componentDidMount、componentDidUpdate等手段来取得更新后的值;而在函数式组件中,则可以通过useEffect来获取更新后的状态。所以这个问题,其实有点无聊。
不过,既然大家都这么乐于讨论,今天我们就系统地梳理一下这个问题,主要分为两方面来说:
在类组件中,这个问题的答案是多样的,首先抛第一个结论:
通过reactdom.render(<App />, rootNode)方式创建应用,则为 legacy 模式,这也是create-react-app目前采用的默认模式;
通过ReactDOM.unstable_createRoot(rootNode).render(<App />)方式创建的应用,则为concurrent模式,这个模式目前只是一个实验阶段的产物,还不成熟。
是的,这不是玄学,我们来先抛出结论,再来逐步解释它。
实验代码如下:
class StateDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
}
render() {
return <div>
<p>{this.state.count}</p>
<button onClick={this.increase}>累加</button>
</div>
}
increase = () => {
this.setState({
count: this.state.count + 1
})
// 异步的,拿不到最新值
console.log('count', this.state.count)
// setTimeout 中 setState 是同步的
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
// 同步的,可以拿到
console.log('count in setTimeout', this.state.count)
}, 0)
}
bodyClickHandler = () => {
this.setState({
count: this.state.count + 1
})
// 可以取到最新值
console.log('count in body event', this.state.count)
}
componentDidMount() {
// 自己定义的 DOM 事件,setState 是同步的
document.body.addEventListener('click', this.bodyClickHandler)
}
componentWillUnmount() {
// 及时销毁自定义 DOM 事件
document.body.removeEventListener('click', this.bodyClickHandler)
}
}
要解答上述现象,就必须了解 setState 的主流程,以及 react 中的 batchUpdate 机制。
首先我们来看看 setState 的主流程:
由此我们可以判定:所谓的异步更新,都命中了batchUpdate,先保存在脏组件中就完事;而同步更新,总是会去更新所有的脏组件。
非常有意思,看来是否命中batchUpdate是关键。问题也随之而来了,为啥直接调用就能命中batchUpdate,而放在异步回调里或者自定义 DOM 事件中就命中不了呢?
这就涉及到一个很有意思的知识点:react 中函数的调用模式。对于刚刚的 increase 函数,还有一些我们看不到的东西,现在我们通过魔法让其显现出来:
increase = () => {
// 开始:默认处于bashUpdate
// isBatchingUpdates = true
this.setState({
count: this.state.count + 1
})
console.log('count', this.state.count)
// 结束
// isBatchingUpdates = false
}
increase = () => {
// 开始:默认处于bashUpdate
// isBatchingUpdates = true
setTimeout(() => {
// 此时isBatchingUpdates已经设置为了false
this.setState({
count: this.state.count + 1
})
console.log('count in setTimeout', this.state.count)
}, 0)
// 结束
// isBatchingUpdates = false
}
当 react 执行我们所书写的函数时,会默认在首位设置isBatchingUpdates变量。看到其中的差异了吗?当 setTimeout 执行其回调时,isBatchingUpdates早已经在同步代码的末尾被置为false了,所以没命中batchUpdate。
那自定义 DOM 事件又是怎么回事?代码依然如下:
componentDidMount() {
// 开始:默认处于bashUpdate
// isBatchingUpdates = true
document.body.addEventListener("click", () => {
// 在回调函数里面,当点击事件触发的时候,isBatchingUpdates早就已经设为false了
this.setState({
count: this.state.count + 1,
});
console.log("count in body event", this.state.count); // 可以取到最新值。
});
// 结束
// isBatchingUpdates = false
}
我们可以看到,当componentDidMount跑完时,isBatchingUpdates已经设置为false了,而点击事件后来触发,并调用回调函数时,取得的isBatchingUpdates当然也是false,不会命中batchUpdate机制。
总结:
这里要注意一点:React去加isBatchingUpdate的行为不是针对“函数”,而是针对“入口”。比如setTimeout、setInterval、自定义DOM事件的回调等,这些都是React“管不到”的入口,所以不会去其首尾设置isBatchingUpdates变量。
因为这个东西只在实验阶段,所以要开启 concurrent 模式,同样需要将 react 升级为实验版本,安装如下依赖:
npm install react@experimental react-dom@experimental
其他代码不用变,只更改 index 文件如下:
- ReactDOM.render(<App />, document.getElementById('root'));
+ ReactDOM.unstable_createRoot(document.getElementById('root')).render(<App />);
则可以发现:其更新都是异步的,在任何情况下都是如此。
在函数式组件中,我们会这样定义状态:
const [count, setCount] = useState(0)
这时候,我们发现当我们无论在同步函数还是在异步回调中调用 setCount 时,打印出来的 count 都是旧值,这时候我们会说:setCount 是异步的。
const [count, setCount] = useState(0);
// 直接调用
const handleStrightUpdate = () => {
setCount(1);
console.log(count); // 0
};
// 放在setTimeout回调中
const handleSetTimeoutUpdate = () => {
setTimeout(() => {
setCount(1);
console.log(count); // 0
});
};
setCount 是异步的,这确实没错,但是产生上述现象的原因不只是异步更新这么简单。原因主要有以下两点:
1,调用 setCount 时,会做合并处理,异步更新该函数式组件对应的 hooks 链表里面的值,然后触发重渲染(re-renders),从这个角度上来说,setCount确实是一个异步操作;
2,函数式的capture-value特性决定了console.log(count)语句打印的始终是一个只存在于当前帧的常量,所以就算无论 setCount 是不是同步的,这里都会打印出旧值。
来自:https://www.cnblogs.com/zhangnan35/p/14290542.html
javascript中alert是Bom中的成员函数,alert对话框是模态的,具有阻塞性质的,不点击是不会执行后续代码的。js的阻塞是指在调用结果返回之前,当前线程会被挂起, 只有在得到结果之后才会继续执行。
如何优化async代码?更好的编写async函数:使用return Promise.reject()在async函数中抛出异常,让相互之间没有依赖关系的异步函数同时执行,不要在循环的回调中/for、while循环中使用await,用map来代替它
Javascript语言的执行环境是单线程,异步模式非常重要。在浏览器端,耗时很长的操作都应该异步执行,避免浏览器失去响应,最好的例子就是Ajax操作。
js异步加载又被称为非阻塞加载,浏览器在下载JS的同时,还会进行后续页面处理。那么如何实现js异步加载呢?下面整理了多种实现方案供大家参考。异步加载js方案:Script Dom Element、onload时的异步加载、$(document).ready()、async属性、defer属性、es6模块type=module属性
回调函数方式:将异步方法如readFile封装到一个自定义函数中,通过将异步方法得到的结果传给自定义方法的回调函数参数。事件驱动方式:使用node events模块,利用其EventEmitter对象
JavaScript引擎是基于单线程 (Single-threaded) 事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列 (job queue) 中
传统的异步解决方案采用回调函数和事件监听的方式,而这里主要记录两种异步编程的新方案:ES6的新语法Promise;ES2017引入的async函数;Generator函数(略)
JS本身是一门单线程的语言,所以在执行一些需要等待的任务(eg.等待服务器响应,等待用户输入等)时就会阻塞其他代码。如果在浏览器中JS线程阻塞了,浏览器可能会失去响应,从而造成不好的用户体验。
请实现如下的函数,可以批量请求数据,所有的URL地址在urls参数中,同时可以通过max参数 控制请求的并发度。当所有的请求结束后,需要执行callback回调。发请求的函数可以直接使用fetch。
将setState()认为是一次请求而不是一次立即执行更新组件的命令。为了更为可观的性能,React可能会推迟它,稍后会一次性更新这些组件。React不会保证在setState之后,能够立刻拿到改变的结果。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!