从react的一个简单小案例说起。实现一个功能,在网络请求之前,前端显示一个loading,等请求完数据之后loading消失,显示请求后的内容,这个功能在实际开发过程非常常见,组件代码如下,在写这个代码的过程出现了一些很有意思的问题,主要围绕setState的异步更新机制,让我感觉我对js的异步机制不太了解。
const Test = (props) => {
const [count, setCount] = useState(0);
const [loading, setLoading] = useState(false);
useEffect(() => {
console.log("s生命周期", loading,count)
}, [loading,count])
async function handleSetCount() {
setLoading(true);
setCount(2);
var res = async (count) => {
return new Promise(resolve => {
console.log("开始请求")
setTimeout(() => {
console.log("请求完毕")
resolve(count + 1);
}, 2000);
})
}
const data = await res(count);
console.log("获取完数据", count)
setCount(data)
console.log("获取完数据", count)
setLoading(false);
}
function handleChange(e) {
props.event(e.target.value)
}
return (
<div className={styles.input_warp}>
<h1>标题</h1>
<input className={styles.input} type='submit' onClick={handleSetCount} value={count}/>
<input className={styles.input} value={loading ? 'loading' : count} readOnly={true}/>
</div>
)
}
export default Test;
1.setState异步更新
先从setState的异步更新说起,在同步代码我们使用设置state的状态的时候,多个setState会被合并为一个State,所以如果我们开头是设置setLoading=true,结尾设置setLoading=false 中间没有任何操作的情况下是不会显示loading的。这时候就是考验对promise和async await的理解程度了。经过反复摸索终于发现setState的奥秘:当主线程同步代码执行完毕之后,触发一次试图更新,触发声明周期的更新函数。 上述代码的控制台输出如下:
开始请求 [input.jsx:18](<> "在调试器中查看源代码 → d:/Sun/【S3=AppInstall】/ReactWorkstation/react-house-demo/src/components/Test2/input.jsx:18")\
生命周期 true 2 [input.jsx:10](<> "在调试器中查看源代码 → d:/Sun/【S3=AppInstall】/ReactWorkstation/react-house-demo/src/components/Test2/input.jsx:10")\
请求完毕 [input.jsx:20](<> "在调试器中查看源代码 → d:/Sun/【S3=AppInstall】/ReactWorkstation/react-house-demo/src/components/Test2/input.jsx:20")\
获取完数据 0 [input.jsx:27](<> "在调试器中查看源代码 → d:/Sun/【S3=AppInstall】/ReactWorkstation/react-house-demo/src/components/Test2/input.jsx:27")\
获取完数据 0 [input.jsx:29](<> "在调试器中查看源代码 → d:/Sun/【S3=AppInstall】/ReactWorkstation/react-house-demo/src/components/Test2/input.jsx:29")\
生命周期 false 1
但是还有一个很奇怪的现象,触发生命周期函数之后,此时的state的状态按理说应该是,loading变成true,count变成2,而且此时页面上的loading已经变成true了,count已经变成2了。但是当 await后面的promise执行完成之后,继续执行后续代码,获取当前Loading和count依旧是count=0 ,loading=false,咱也不知道为啥,最后后面两次setState操作合并为一次,触发一次生命周期的组件更新操作。
谜底揭晓: 上面触发更新是因为执行期间遇到了SetTimeOut(),异步渲染关闭了。
setState在合成事件和生命周期函数中是异步的,在原生事件和定时器中都是同步的。 加分回答 setState本身不分同步或者异步,而是取决于是否处于batch update中。组件中的所有函数在执行时临时设置一个变量isBatchingUpdates = true,当遇到setState时,如果isBatchingUpdates是true,那么setState就是异步的,如果是false,那么setState就是同步的。那么什么时候isBatchingUpdates会被设置成false呢?
1.当函数运行结束时isBatchingUpdates = false
2.当函数遇到setTimeout、setInterval时isBatchingUpdates = false
3.当dom添加自定义事件时isBatchingUpdates = false
2.promise await async的执行顺序问题
await 等待获取promise对象的reslove()。 reslove()这个代码执行结束之后,async函数内部就开始继续向下执行,所以上述代码reslove() 放到setTimeOut`里面会看到loading效果,放到setTimeOut不会看到loading结果。
function res(data){
return new Promise((resolve)=>{
setTimeout(()=>{
console.log("计时结束",data)
},2000)
resolve(data+10);
console.log("promise内部,计时器之外",data)
})
}
async function test1(){
const a=await res(1);
console.log("await结束",a)
return 'return';
}
test1().then(r => console.log('await的then',r))
```
- 上述代码的输出
promise内部,计时器之外 1
await结束 11
await的then return
计时结束 1
将resolve(data+10);放到setTimeout内部的的代码输出,会等带计时器结束之后执行后续代码。
promise内部,计时器之外 1
计时结束 1
await结束 11
await的then return
没有resolve(data+10);这句话的时候,输出如下,async函数不再继续往下执行。
promise内部,计时器之外 1
计时结束 1
总结:遇到promise之后,promise.then会加入到微任务队列,但是promise内部的代码会放到当前执行栈依次执行。
3. 异步编程面试题
3.1 字节跳动面试题
async function async1() {
console.log("async1 start"); // 1 (2)
await async2();
console.log("async1 end"); //5 (6)
}
async function async2() {
console.log("async2"); // 4 (3)
}
console.log("script start"); // 0 (1)
setTimeout(function () {
console.log("setTimeout"); // 7 (8)
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1"); // 2 (4)
resolve();
}).then(function () {
console.log("promise2"); // 6 (7)
});
console.log("script end"); // 3 (5)
括号里面是正确答案,没加括号的是我的答案
总结:async函数中,await后面的部分就相当于promise的then函数,属于是微任务,所以需要等待执行栈里面的代码执行完毕执行,setTimeout属于宏任务,要等到微任务执行完毕后,才能执行。
宏任务:script、setTimeout、setInterval、setImmediate、UI、IO。
微任务:微任务包括 process.nextTick,promise ,MutationObs erver其中 process.nextTick 为 Node 独有。
3.2 循环异步编程题
console.log(1); // 1
setTimeout((_) => {
console.log(2); // 6
}, 1000);
async function fn() {
console.log(3); // 3
setTimeout((_) => {
console.log(4); // 7
}, 20);
return Promise.reject();
}
async function run() {
console.log(5); // 2
await fn();
console.log(6); // 5
}
run();
// 需要执行150MS左右
for (let i = 0; i < 90000000; i++) {}
setTimeout((_) => {
console.log(7); // 8
new Promise((resolve) => {
console.log(8); // 9
resolve();
}).then((_) => {
console.log(9); // 10
});
}, 0);
console.log(10); // 4
我的输出:1 5 3 10 6 2 4 7 8 9
正确输出:1 5 3 10 4 7 8 9 2
-
宏任务setTimeout队列放入的顺序是和放入时间有关系的,不是先放入先执行,而是根据,执行的时间。虽然后面执行是0,但是循环执行了150ms,所以20ms的宏任务先进入队列,所以先执行20ms的宏任务,再执行0ms的宏任务,再执行1s的宏任务
-
async函数中await后面是一个promise对象,如果promise对象里面的存在 resolv e,说明执行成功,后面函数继续执行,如果没有resolve或者reject,那么后面函数就不执行。 所以不会打印6。
console.log("AAAA");
setTimeout(() => console.log("BBBB"), 1000);
const start = new Date();
while (new Date() - start < 3000) {
}
console.log("CCCC");
setTimeout(() => console.log("DDDD"), 0);
new Promise((resolve, reject) => {
console.log("EEEE");
reject()
})
.then(() => console.log("FFFF"))
.then(() => console.log("GGGG"))
.catch(() => console.log("HHHH"));
console.log("IIII");
AAAA
CCCC
EEEE
IIII
HHHH
DDDD
BBBB
注意: while循环转了3s钟,1s的定时器先进宏任务队列。
new Promise(resolve => {
setTimeout(() => {console.log(0)}, 0)
resolve()
}).then(() => {
setTimeout(() => {console.log(1)}, 0)
})
setTimeout(() => {console.log(2)}, 0)
new Promise(resolve => {
setTimeout(resolve, 0)
}).then(() => {
console.log(3)
setTimeout(() => {console.log(4)}, 0)
new Promise(r => r()).then(() => {console.log(5)})
})
setTimeout(() => {console.log(6)}, 0)
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
for(var i = 9; i < 12; i++) {
setTimeout(() => {console.log(i)}, 0)
console.log(i)
}
我的输出:7 9 10 11 8 0 2 6 12 12 12 3 5 1 4
正确输出:7 9 10 11 8 0 2 3 5 6 12 12 12 1 4
总结: 微任务宏任务的执行顺序问题。 三个区域:同步代码执行栈,宏任务队列,微任务队列。执行顺序为,先执行同步代码执行栈,然后执行微任务队列,微任务队列执行结束之后,执行宏任务队列,如果在执行宏任务队列的过程又遇到微任务队列,这个时候就会继续执行微任务队列,等待微任务队列执行结束之后再回过来执行宏任务队列。可以理解为宏任务队列是大队列,同步代码一次排放在这个队列中,当宏任务中遇到同步代码就直接执行,遇到微任务就在这个大队列的后面添加一个微任务,一直到这个宏任务队列执行里面内容执行完毕为止。遇到宏任务,就再添加一个大队列。 转载连接:异步编程面试题解析
4. 异步基础知识
4.1 为什么需要异步
-
JS是单线程的。因为DOM渲染和JS执行共用一个线程,DOM渲染会阻塞JS执行,JS执行同样会阻塞DOM渲染,如果是多线程的话,会造成两个线程同时操作DOM情况,因此JS必须为单线程。
-
单线程同一个时间只能做一件事情,遇到等待,比如说网络请定时器等耗时比较长的任务不应该阻塞代码执行。