一、JavaScript的异步操作
在JavaScript的世界中,所有代码都是单线程执行的。
由于这个“缺陷”,导致JavaScript的所有网络操作,浏览器事件,都必须是异步执行。异步执行可以用回调函数实现:
function callback() {console.log('Done');
}
console.log('before setTimeout()');
setTimeout(callback, 1000); // 1秒钟后调用callback函数
console.log('after setTimeout()');
观察上述代码执行,在Chrome的控制台输出可以看到:
before setTimeout()
after setTimeout()
(等待1秒后)
Done
可见,异步操作会在将来的某个时间点触发一个函数调用。
AJAX就是典型的异步操作。以上一节的代码为例:
request.onreadystatechange = function () {if (request.readyState === 4) {if (request.status === 200) {return success(request.responseText);} else {return fail(request.status);}}
}
把回调函数success(request.responseText)
和fail(request.status)
写到一个AJAX操作里很正常,但是不好看,而且不利于代码复用。
有没有更好的写法?比如写成这样:
var ajax = ajaxGet('http://...');
ajax.ifSuccess(success).ifFail(fail);
这种链式写法的好处在于,先统一执行AJAX逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用success
函数或fail
函数。
古人云:“君子一诺千金”,这种“承诺将来会执行”的对象在JavaScript中称为Promise对象。
1-1、setTimeout函数
setTimeout
是 JavaScript 中一个非常常用的函数,用于在指定的延迟后执行一个函数或计算一个表达式。它返回一个代表定时器的ID,这个ID可以用来在将来必要的时候取消定时器(使用 clearTimeout
函数)。
语法:
var timerId = setTimeout(func|code, [delay], [arg1], [arg2], ...)
func|code
:要执行的函数或要计算的表达式。delay
:可选参数,表示延迟的毫秒数,即多长时间后执行函数或表达式。默认是 0。arg1, arg2, ...
:可选参数,表示传递给函数的额外参数。
示例:
setTimeout(function() { console.log('Hello, world!');
}, 2000);
在这个例子中,console.log('Hello, world!')
将在 2 秒(2000 毫秒)后执行。
function greet(name) { console.log('Hello, ' + name + '!');
} setTimeout(greet, 2000, 'Alice');
在这个例子中,greet
函数将在 2 秒后执行,并且会传递 'Alice'
作为参数。
setTimeout
只会执行一次指定的函数或代码。如果你想要重复执行某个函数或代码,你应该使用setInterval
或者在函数内部再次调用setTimeout
。
1-2、清除定时器 clearTimeout
函数
如果你想要在某个时刻取消定时器,你可以使用 clearTimeout
函数,并传入 setTimeout
返回的定时器ID。
语法:
clearTimeout(timeoutID)
示例1:
var timerId = setTimeout(function() { console.log('This will not be logged.');
}, 5000); // 假设在某个时刻,我们决定取消这个定时器
clearTimeout(timerId);
在这个例子中,由于我们调用了 clearTimeout
,所以 console.log
不会被执行。
示例2:
// 设置一个定时器,将在 2 秒后执行
var timerId = setTimeout(function() { console.log('Timer executed!');
}, 2000); // 在 1 秒后取消定时器
setTimeout(function() { clearTimeout(timerId); console.log('Timer cleared!');
}, 1000);
你将在控制台看到 "Timer cleared!" 的输出,而不会看到 "Timer executed!" 的输出。
1-3、链式调用
链式调用,允许我们在单个对象上连续调用多个方法,并且每次方法调用都返回同一个对象,以便可以进一步调用其他方法。
优点:这种模式可以提高代码的可读性和简洁性。
1-4、Promise对象
Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。
在JavaScript中,Promise
对象用于处理异步操作,它代表了一个可能现在、将来或永远不可用的值。
Promise
对象有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。一旦Promise
的状态从pending
变为fulfilled
或rejected
,就不会再改变。
创建Promise
对象的基本语法如下:
const promise = new Promise((resolve, reject) => { // 异步操作代码 if (/* 异步操作成功 */) { resolve(value); // 将Promise的状态设置为fulfilled,并传递一个值 } else { reject(error); // 将Promise的状态设置为rejected,并传递一个错误 }
});
resolve
和reject
都是函数,它们由Promise
构造函数传递进来,用于处理异步操作的结果。
示例:
const promise = new Promise((resolve, reject) => { setTimeout(() => { const success = true; if (success) { resolve('异步操作成功!'); } else { reject('异步操作失败!'); } }, 1000);
}); promise.then(value => { console.log(value); // 如果Promise状态为fulfilled,则执行这里的代码
}).catch(error => { console.error(error); // 如果Promise状态为rejected,则执行这里的代码
});
在JavaScript的Promise
构造函数中,resolve
和reject
是两个非常关键的函数参数。它们被用来改变Promise
对象的状态,并传递最终的结果或错误给Promise
链中的后续处理函数。
1、resolve
函数
resolve
函数用于将Promise
对象的状态从pending
(进行中)变为fulfilled
(已完成),并传递一个值给后续的.then()
处理函数。
2、reject
函数
reject
函数用于将Promise
对象的状态从pending
变为rejected
(已拒绝),并传递一个错误对象给后续的.catch()
处理函数。
3、Promise链中的错误处理
在Promise
链中,如果你没有在每个.then()
之后立即使用.catch()
来处理可能发生的错误。
你可以在链的末尾使用单个.catch()
来捕获所有之前的.then()
中可能抛出的错误。这是因为,一旦Promise
链中的某个环节发生错误,该错误会“冒泡”到链的末尾,除非在某个环节被捕获处理。
promise .then(result => { // 处理结果... // 如果这里发生错误且没有捕获,它会传递到链的末尾的.catch()中 }) .then(anotherResult => { // 处理另一个结果... // 同样,这里的错误也会“冒泡”到链的末尾 }) .catch(error => { // 处理链中任何环节的错误 console.error(error); });
我们先看一个最简单的Promise例子:
生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:
<div id="test-promise-log" style="border: solid 1px #ccc; padding: 1em; margin: 15px 0;"><p>Log:</p></div><script>"use script"function testLian(){// 清除log:var logging = document.getElementById('test-promise-log');while (logging.children.length > 1) {logging.removeChild(logging.children[logging.children.length - 1]);}// 输出log到页面:function log(s) {var p = document.createElement('p');p.innerHTML = s;logging.appendChild(p);}new Promise(function (resolve, reject) {log('start new Promise...');var timeOut = Math.random() * 2;log('set timeout to: ' + timeOut + ' seconds.');setTimeout(function () {if (timeOut < 1) {log('call resolve()...');resolve('200 OK');}else {log('call reject()...');reject('timeout in ' + timeOut + ' seconds.');}}, timeOut * 1000);}).then(function (r) {log('Done: ' + r);}).catch(function (reason) {log('Failed: ' + reason);});}</script>
可见Promise最大的好处是在异步执行的流程中,把执行代码和处理结果的代码清晰地分离了:
4、Promise处理若干异步任务
Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。
要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。有了Promise,我们只需要简单地写:
job1.then(job2).then(job3).catch(handleError);
其中,job1
、job2
和job3
都是Promise对象。
下面的例子演示了如何串行执行一系列需要异步计算获得结果的任务:
// 0.5秒后返回input*input的计算结果:
function multiply(input) {return new Promise(function (resolve, reject) {log('calculating ' + input + ' x ' + input + '...');setTimeout(resolve, 500, input * input);});
}// 0.5秒后返回input+input的计算结果:
function add(input) {return new Promise(function (resolve, reject) {log('calculating ' + input + ' + ' + input + '...');setTimeout(resolve, 500, input + input);});
}var p = new Promise(function (resolve, reject) {log('start new Promise...');resolve(123);
});p.then(multiply).then(add).then(multiply).then(add).then(function (result) {log('Got value: ' + result);
});
setTimeout
可以看成一个模拟网络等异步执行的函数。
1-5、AJAX异步执行函数转换为Promise对象
现在,我们把上一节的AJAX异步执行函数转换为Promise对象,看看用Promise如何简化异步处理:
'use strict';// ajax函数将返回Promise对象:
function ajax(method, url, data) {var request = new XMLHttpRequest();return new Promise(function (resolve, reject) {request.onreadystatechange = function () {if (request.readyState === 4) {if (request.status === 200) {resolve(request.responseText);} else {reject(request.status);}}};request.open(method, url);request.send(data);});
}var log = document.getElementById('test-promise-ajax-result');
var p = ajax('GET', '/api/categories');
p.then(function (text) { // 如果AJAX成功,获得响应内容log.innerText = text;
}).catch(function (status) { // 如果AJAX失败,获得响应代码log.innerText = 'ERROR: ' + status;
});
1-6、Promise.all()
除了串行执行若干异步任务外,Promise还可以并行执行异步任务。
试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()
实现如下:
"use script"var p1 = new Promise(function (resolve, reject) {setTimeout(resolve, 500, 'A1');});var p2 = new Promise(function (resolve, reject) {setTimeout(resolve, 600, 'A2');});// 同时执行p1和p2,并在它们都完成后执行then:Promise.all([p1, p2]).then(function (results) {// 获得一个Array: ['A1', 'A2']console.log(results);});
1-7、Promise.race()
有些时候,多个异步任务是为了容错。
比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()
实现:
var p1 = new Promise(function (resolve, reject) {setTimeout(resolve, 500, 'A1');
});
var p2 = new Promise(function (resolve, reject) {setTimeout(resolve, 600, 'A2');
});
Promise.race([p1, p2]).then(function (result) {console.log(result); // 'A1'
});
由于p1
执行较快,Promise的then()
将获得结果'A1'
。p2
仍在继续执行,但执行结果将被丢弃。
如果我们组合使用Promise,就可以把很多异步任务以并行和串行的方式组合起来执行。