setTimeout 是前端程序猿最经常使用的一个api,那么我们如何获得setTimeout 里回调函数的返回值呢?
1. setTimeout的异步特性
我们先看下面实例:
<script>
function foo() {
//延迟任务1
setTimeout(() => {
console.time('testTime')
console.log('aaa....')
}, 1000);
//延迟任务2
setTimeout(() => {
console.log('bbb....')
}, 2000);
//延迟任务3
setTimeout(() => {
console.log('ccc....')
console.timeEnd('testTime')
}, 3000);
}
foo();
console.log('ending...')
</script>
运行结果:
end...
aaa....
bbb....
ccc....
testTime: 1999.799072265625 ms
在foo函数里执行了三个延迟任务,我们发现总耗时取决于延时时间最长的任务3,而不是三个延迟任务时间的总和。
给人一种假象似乎setTimeout()一定是异步,但是事实是:setTimeout是单线程,类似异步,但不是真正的异步。
上面的代码改写如下:
<script>
function foo() {
//延迟任务1,立刻执行
setTimeout(() => {
console.time('testTime')
console.log('aaa....')
}, 0);
//延迟任务2
setTimeout(() => {
console.log('bbb....')
}, 2000);
//延迟任务3
setTimeout(() => {
console.log('ccc....')
console.timeEnd('testTime')
}, 3000);
}
foo();
console.log('ending...')
</script>
运行结果:
ending...
aaa....
bbb....
ccc....
testTime: 3000.72705078125 ms
我们发现,任务的执行顺序没有改变,而且总耗时反而增加了。所以说 setTimeout并不是真正的异步, 而是JavaScript在执行的时候会将setTimeout放入任务队列中等待主线程的执行(不阻塞主线程)全部执行完成后再通过event loop去询问任务队列中是否有可执行的代码,再继续放入主线程中执行,故产生了异步的假象。
结论:setTimeout()有异步的特性,但不是真正的异步。
2.如何获得setTimeout里回调函数的返回值
明白了setTimeout()有异步特性,下来咱们看看如何获取setTimeout里回调函数的返回值。看下面实例:
<script>
function test(){
let result = setTimeout(()=>{
console.log('execute callback...')
console.timeEnd('testTime')
return{
name: '张三'
}
},3000);
return result;
}
console.time('testTime')
console.log(test());
</script>
运行结果:
1
execute callback...
testTime: 3003.06591796875 ms
我们发现返回结果并不是‘张三’对象。而是整数 1,原来这个返回值是setTimeout方法的返回值,而不是setTimeout里面执行的回调函数的返回值。setTimeout是有返回值的, 表示当前setTimeout在页面中的所有setTimeout中的序号,并且默认从1开始。
于是很多小伙伴会想到改写如下:
<script>
function test(){
let result;
setTimeout(result = ()=>{
console.log('execute callback...')
console.timeEnd('testTime')
return{
name: '张三'
}
},3000);
return result;
}
console.time('testTime')
console.log(test());
</script>
运行结果:
()=>{
console.log('execute callback...')
console.timeEnd('testTime')
return{
name: '张三'
}
}
execute callback...
testTime: 3006.216064453125 ms
我们发现返回了函数文本,并且是立即返回的结果。并不是回调函数的返回值。
还有小伙伴尝试改写如下:
function fn(){
console.log('execute callback...')
console.timeEnd('testTime')
return{
name: '张三'
}
}
function test(callback){
let result;
setTimeout(result = callback(),3000);
return result;
}
console.time('testTime')
console.log(test(fn));
运行结果如下:
execute callback...
testTime: 0.10498046875 ms
{name: '张三'}
Uncaught SyntaxError: Unexpected identifier 'Object'
我们发现输出的{name: '张三'},是result = callback() 这行代码里,函数调用的返回值,并且整个表达式是一个对象类型,不符合setTimeout第一参数规定的类型(1."字符串代码" 2.方法 3.函数)。
由于setTimeout的异步特性,以上传统思维方式都无法正确获得setTimout里执行回调函数的返回值。
3.实现方案
通过以上实验,我得到以下结论:不能return来自异步回调的值。咱们需要提供回调或返回一个promise来间接地解决这个问题。
1)使用回调函数
<script>
function fn(resp) {
console.log(resp);
console.timeEnd('testTime')
}
function test(callback) {
setTimeout(() => {
console.log('execute callback...')
callback({name: '张三'})
}, 3000);
}
console.time('testTime')
test(fn);
</script>
运行结果:
execute callback...
{name: '张三'}
testTime: 3008.704833984375 ms
2)使用Promise
<script>
function fn() {
let promise = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('execute....')
resolve({name: '张三'});
}, 3000);
});
return promise;
}
console.time('testTime')
fn().then((resp)=>{
console.log(resp);
console.timeEnd('testTime')
})
</script>
运行结果:
execute....
{name: '张三'}
testTime: 3028.7841796875 ms
推荐使用使用async/await,使得异步代码更像同步代码,可读性更好。
<script>
async function fn() {
let promise = new Promise(function (resolve, reject) {
setTimeout(function () {
console.log('execute....')
resolve({name: '张三'});
}, 3000);
});
return promise;
}
console.time('testTime');
async function test () {
const result = await fn();
console.log(result);
console.timeEnd('testTime')
}
test();
</script>