前端之异步JS
预备知识
异步的本质
事件队列:
像promise这样的异步操作被放入事件队列中,事件队列在主线程完成处理后运行,这样它们就不会阻止后续JavaScript代码的运行。排队操作将尽快完成,然后将结果返回到JavaScript环境。
异步概述
什么是同步
通常, 程序只有一个主线程, 同一时间只能做一件事,其他的事情都阻塞了,直到前面的操作完成, 才开始后面的操作.
什么是异步
异步是指 多个事件的发生不会互相阻塞干扰. 同一时间可以做多个事情.
使用方式: 从网络获取文件,访问数据库,从网络摄像头获得视频流,或者向VR头罩广播图像等.
线程
程序用线程完成任务, 而每个线程只能执行一个任务.
由于所有任务都是顺序执行的, 所以只有前面的任务结束, 后面的任务才可以执行.
现代计算机是多核的, 可以支持多线程:
JS是单线程的
只能在一个线程( 主线程 : main thread )上运行多个程序.
js 的异步操作依靠下面两种方法 :
- callback
- promise
异步简介
异步的两种使用方式
callbacks (老派异步)
callbacks 异步情况
A callback is a function that is passed as an argument to another function and is executed after its parent function has completed.
异步callbacks 其实就是函数,只不过是作为参数传递给那些在后台执行的其他函数. 当那些后台运行的代码结束,就调用callbacks函数,通知你工作已经完成.
举例: 异步callback 就是addEventListener()第二个参数以及setTimeout函数的第一个参数
回调函数用途广泛 — 他们不仅仅可以用来控制函数的执行顺序和函数之间的数据传递,还可以根据环境的不同,将数据传递给不同的函数,所以对下载好的资源,你可以采用不同的操作来处理,譬如 processJSON(), displayText(), 等等。
callbacks 同步情况
const gods = ['Apollo', 'Artemis', 'Ares', 'Zeus'];
gods.forEach(function (eachName, index){
console.log(index + '. ' + eachName);
});
forEach() 的参数是一个回调函数,回调函数本身带有两个参数,数组元素和索引值。它无需等待任何事情,立即运行。
promise (新派异步)
Promises 是新派的异步代码, fetch() API就是一个很好的例子, 相当于一个更高效的 XMLHttpRequest .
fetch('products.json').then(function(response) {
return response.json();
}).then(function(json) {
products = json;
initialize();
}).catch(function(err) {
console.log('Fetch problem: ' + err.message);
});
-
这里fetch() 只需要一个参数— 资源的网络 URL — 返回一个 promise.
promise 是表示异步操作完成或失败的对象。可以说,它代表了一种中间状态。 本质上,这是浏览器说“我保证尽快给您答复”的方式,因此得名“promise”。 -
then()中都包含一个回调函数, 如果前一个操作成功,该函数将运行,并且每个回调都接收前一个成功操作的结果作为输入,因此您可以继续对它执行其他操作。每个then()返回一个promise, 那么多个then()可以执行多个异步操作。
-
如果任意一个then()发生错误, 就会运行catch(), 用来报告发生的错误类型。try…catch不能与promise一起使用.
Promises 对比 callbacks
promises与callbacks相似。它们本质上是一个返回的对象,并将回调函数附加到该对象上,而不必将回调作为参数传递给另一个函数。
然而,Promise是专门为异步操作而设计的,与旧式回调相比具有许多优点:
- **您可以使用多个then()操作将多个异步操作链接在一起,并将其中一个操作的结果作为输入传递给下一个操作。**这种链接方式对回调来说要难得多,会使回调以混乱的“末日金字塔”告终。 (也称为回调地狱)。
- Promise总是严格按照它们放置在事件队列中的顺序调用。
- 错误处理: 所有的错误都由块末尾的一个.catch()块处理。
介绍异步JS
JS异步执行的函数
在一段时间后异步执行代码,
或重复异步执行代码,直到让它停下.
setTimeout()
在指定的时间后执行一段代码. 递归使用时, 无论代码运行多久, 每次代码运行间隔都是相同的.
超时执行
设置限定时间
语法:
let 标识符 = setTimeout("要执行的代码", 等待的毫秒数);
setTimeout("alert('对不起, 要你久候')", 3000 );
setTimeout("JavaScript 函数", 等待的毫秒数);
setTimeout("函数名()", 等待的毫秒数);
//**清除 setTimeout()** 用
clearTimeout(标识符);
调用 setTimeout 函数会在一个时间段过去后在队列中添加一个消息。这个时间段作为函数的第二个参数被传入。如果队列中没有其它消息,消息会被马上处理。但是,如果有其它消息,setTimeout 消息必须等待其它消息处理完。因此第二个参数仅仅表示最少的时间 而非确切的时间
如果等待的毫秒数为零, 则表示尽快执行., 表示主线程运行后立刻执行.
setInterval()
设置时间间隔, 重复执行.
假设代码需要40毫秒才能运行 - 那么间隔最终只有60毫秒。
语法:
var intervalID = setInterval(func, [delay, arg1, arg2, ...]);
- func 是函数, 经过delay时间后,执行这个函数.
- arg 是按顺序传入func函数的参数.
- intervalID 是setInterval() 计时器的唯一标识符, 通过 clearInterval()来删除setInterval()计时器.
function displayTime() {
let date = new Date();
let time = date.toLocaleTimeString();
document.getElementById('demo').textContent = time;
}
const createClock = setInterval(displayTime, 1000);
//清除interval
clearInterval(createClock);
代码的运行时间可能长于分配的时间间隔, 所以最好使用递归的setTimeut()
当你的代码有可能比你分配的时间间隔,花费更长时间运行时,最好使用递归的 setTimeout() - 这将使执行之间的时间间隔保持不变,无论代码执行多长时间,你不会得到错误
requestAnimationFrame() √
相当于setInterval(), 但进行了改进, requestAnimationFrame()是专门用来循环的函数, 目的 是在浏览器中高效运行动画. 通常每秒60次.
具体情况 它在浏览器重新加载显示内容之前执行指定的代码块,从而允许动画以适当的帧速率运行.
<body>
<div>
↻
</div>
<script>
const spinner = document.querySelector('div');
let rotateCount = 0;
let rAF; // requestAnimationFrame()标识符
let startTime = null; // 开始时间戳
let spinning = false;
function draw(timestamp) {
if (!startTime) {
startTime = timestamp;
}
rotateCount = (timestamp - startTime) / 3;
if (rotateCount > 359) {
rotateCount %= 360;
}
spinner.style.transform = `rotate(${rotateCount}deg)`;
rAF = requestAnimationFrame(draw);
}
document.body.addEventListener('click', () => {
if (spinning) {
cancelAnimationFrame(rAF);//取消动画
spinning = false;
} else {
draw();
spinning = true;
}
});
</script>
</body>
用于 更新动画 (例如,移动精灵,更新乐谱,刷新数据等).
动画的平滑度直接取决于动画的帧速率;
每秒帧数(fps)的数字越高,动画就越平滑;
由于大多数屏幕的刷新率为60Hz,因此在使用web浏览器时,可以达到的最快帧速率是每秒60帧(FPS)。然而,更多的帧意味着更多的处理,这通常会导致卡顿和跳跃-也称为丢帧或跳帧。
requestAnimationFrame() 会尽其所能利用现有资源提升帧速率。总是努力达到60帧/秒的值. 所以我们还要注意每次通过动画循环时要运行的代码量。
timestamp
时间戳系统用来产生和管理时间戳,对签名对象进行数字签名产生时间戳,以证明原始文件在签名时间之前已经存在。
Promise的使用
Promise 是 JavaScript 语言的一个较新的功能,允许推迟进一步的操作,直到上一个then()操作完成或失败.
- Promise 是一个对象,代表操作的中间状态;
- Promise (承诺) 保证在未来返回结果(成功, 正确处理结果; 错误, 处理错误);
常见应用:
Promise 常见的交互之一就是 Web API 返回的 promise 对象。getUserMedia() 来访问用户的摄像头和麦克风。
promise 的使用
chooseToppings()
.then(function(toppings) {
return placeOrder(toppings);
})
.then(function(order) {
return collectOrder(order);
})
.then(function(pizza) {
eatPizza(pizza);
})
.catch(failureCallback);
promise 的简化(箭头函数)
chooseToppings()
.then(toppings =>
placeOrder(toppings)
)
.then(order =>
collectOrder(order)
)
.then(pizza =>
eatPizza(pizza)
)
.catch(failureCallback);
// 或者
chooseToppings()
.then(toppings => placeOrder(toppings))
.then(order => collectOrder(order))
.then(pizza => eatPizza(pizza))
.catch(failureCallback);
箭头函数 () => x 的原型是 ()=> {return x;}
promise 的注意事项
- promise与事件监听器类似
- 一个promise只能成功或失败一次。一旦失败就执行catch()函数
promise实例
fetch和then会按顺序执行,fetch和then成功,会将返回值交给下一个,若以上过程有错,则直接执行catch块。
URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。
语法
objectURL = URL.createObjectURL(object);
一下两种写法等价:
// 调用 fetch() 方法,将图像的URL作为参数从网络中提取
// fetch() 返回的promise对象存储在一个名为promise的变量
let promise = fetch('https://img1.baidu.com/it/u=2769047888,512921539&fm=253&fmt=auto&app=138&f=JPEG?w=657&h=500');
let promise2 = promise.then(reponse => {
if (!reponse.ok) { //Fetch promises 不会产生 404 或 500错误, 这里就是为了产生404错误
// console.log(reponse.ok);
throw new Error(`HTTP error! status: ${response.this.state}`);
} else {
return reponse.blob();
}
})
let promise3 = promise2.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
});
// promise响应失败情况
let errorCase = promise3.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
等价于
fetch('https://img1.baidu.com/it/u=2769047888,512921539&fm=253&fmt=auto&app=138&f=JPEG?w=657&h=500')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.this.state}`);
} else {
return response.blob();
}
})
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
Promise.all()
Promise.all() 方法接收一个promise的iterable类型(注:Array,Map,Set都属于ES6的iterable类型)的输入,并且只返回一个Promise实例, 那个输入的所有promise的resolve回调的结果是一个数组
- 若参数是数组,返回值也是数组,内部元素都是promise(fetch的返回值)。
- 响应多个promise,在执行对应代码。
- 任何一个 promise 拒绝,整个块将拒绝。
语法:
// a,b,c 都是promise类型
Promise.all([a, b, c])
.then(values => {
...
})
.then()// 无错情况下, 顺序执行
.catch()// 有错执行
.finally(); // promise 最后一定执行
模板实例: √
- 自定义promise例子
// 根据url获取资源, 根据type进行解读(图像blob 和文本text)
function fetchAndDecode(url, type) {
return fetch(url).then(response => {
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
if(type === 'blob') {
return response.blob();
} else if(type === 'text') {
return response.text();
}
}
})
.catch(e => {
console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
})
.finally(() => {
console.log(`fetch attempt for "${url}" finished.`);
});
}
入门实例1:
let timeoutPromise = new Promise((resolve, reject) => {
setTimeout(function() {
resolve('Success!');
}, 2000);
})
timeoutPromise
.then(message => {
alert(message);
})
入门实例1 改进:
function timeoutPromise(message, interval) {
return new Promise((resolve, reject) => {
if (message === '' || typeof message !== 'string') {
reject('Message is empty or not a string');
} else if (interval < 0 || typeof interval !== 'number') {
reject('Interval is negative or not a number');
} else {
setTimeout(function() {
resolve(message);
}, interval);
}
});
};
timeoutPromise('Hello there!', 1000)
.then(message => {
alert(message);
})
.catch(e => {
console.log('Error: ' + e);
});
以上例子是故意简单, 下方一个更真实的例子
function promisifyRequest(request) {
return new Promise(function(resolve, reject) {
request.onsuccess = function() {
resolve(request.result);
};
request.onerror = function() {
reject(request.error);
};
});
}
request 等价于 fetch(URL)
- 当request的success event触发时,onsuccess处理程序将使用请求的result实现(fullfill)promise。
- 当request的error event触发时,onerror处理程序拒绝带有请求error的promise
异步编程的简单实现
Promises 有点复杂, 但是现代的浏览器都配备了 async 函数和 await 操作符
async
async 关键字加到函数声明中,表示返回值是 promise 类型。
async 加到函数之前,使它成为异步函数,该函数会返回一个 promise。使用返回值 promise 时,用 .then(value => {})
语法:
async function name([param[, param[, ...param]]]) {
statements
}
let hello = async () => { return "Hello" };
hello().then((value) => console.log(value))
await
await 只在异步函数里面才起作用。是配合 async 一起使用的
位置: 任何异步的,基于 promise 的函数之前。包括Web API函数。
作用: 暂停在该行代码上,直到 promise 完成;
暂停时,其他正在等待执行的代码就有可以执行了。
// 图片
async function myFetch(url) {
let response = await fetch(url);
return response.blob();
}
myFetch('https://img1.baidu.com/it/u=3702625202,3169032464&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500')
.then(myBlob => {
console.log(myBlob);
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
Promise.all()
async function fetchAndDecode(url, type) {
let response = await fetch(url);
let content;
if (type === 'blob') content = await response.blob();
else if (type === 'text') content = await response.text();
return content;
}
async function displayContent() {
let a = fetchAndDecode('https://img1.baidu.com/it/u=3702625202,3169032464&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500', 'blob');
let b = fetchAndDecode('https://img1.baidu.com/it/u=2769047888,512921539&fm=253&fmt=auto&app=138&f=JPEG?w=657&h=500', 'blob');
let c = fetchAndDecode('https://img2.baidu.com/it/u=703207382,3730089159&fm=253&fmt=auto&app=138&f=JPEG?w=260&h=195', 'blob');
let values = await Promise.all([a, b, c]);
let ObjectURL1 = URL.createObjectURL(values[0]);
let ObjectURL2 = URL.createObjectURL(values[1]);
let ObjectURL3 = URL.createObjectURL(values[2]);
let image1 = document.createElement('img');
let image2 = document.createElement('img');
let image3 = document.createElement('img');
image1.src = ObjectURL1;
image2.src = ObjectURL2;
image3.src = ObjectURL3;
document.body.appendChild(image1);
document.body.appendChild(image2);
document.body.appendChild(image3);
}
displayContent()
.catch(e => console.log)
注: Async/await 让你的代码更同步,但await 关键字会阻塞其后的await代码,直到promise完成。如果await比较多,会阻塞异步程序自身的运行。
将Promise对象存储在变量中,同时开始它们,最后等到完全执行完毕。这样可以缓解上述问题。
将可以同时进行的await操作放入变量中同时执行
function timeoutPromise(interval) {
return new Promise((resolve, reject) => {
setTimeout(function(){
resolve("done");
}, interval);
});
};
//原型 9s多 --------------------------------------------------
async function timeTest() {
await timeoutPromise(3000);
await timeoutPromise(3000);
await timeoutPromise(3000);
}
let startTime = Date.now();
timeTest().then(() => {
let finishTime = Date.now();
let timeTaken = finishTime - startTime;
alert("Time taken in milliseconds: " + timeTaken);
})
//改 3s多 --------------------------------------------------
async function timeTest() {
const timeoutPromise1 = timeoutPromise(3000);
const timeoutPromise2 = timeoutPromise(3000);
const timeoutPromise3 = timeoutPromise(3000);
await timeoutPromise1;
await timeoutPromise2;
await timeoutPromise3;
}
let startTime = Date.now();
timeTest().then(() => {
let finishTime = Date.now();
let timeTaken = finishTime - startTime;
alert("Time taken in milliseconds: " + timeTaken);
})
Async/await 的类方法
class Person {
constructor(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
}
async greeting() {
return await Promise.resolve(`Hi! I'm ${this.name.first}`);
};
farewell() {
console.log(`${this.name.first} has left the building. Bye for now!`);
};
}
let han = new Person('Han', 'Solo', 25, 'male', ['Smuggling']);
han.greeting().then(console.log);
正确选择实现异步的方法 √
异步回调
特征:将回调函数作为参数传递给另一个函数,
作用:方便对异步操作的返回结果进行处理。
实例:XMLHttpRequest API加载资源
XMLHttpRequest从 URL 检索数据,不会刷新整个页面。只更新页面的一部分,不会中断用户正在做的事情。
function loadAsset(url, type, callback) {
let xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.responseType = type;
xhr.onload = function() {
console.log(xhr.response);
callback(xhr.response);
};
xhr.send();
}
function displayImage(blob) {
let objectURL = URL.createObjectURL(blob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}
loadAsset('https://img2.baidu.com/it/u=1814268193,3619863984&fm=253&fmt=auto&app=138&f=JPEG?w=632&h=500', 'blob', displayImage);
setTimeout()
特征:经过指定时间(单位:ms)后运行函数。
作用:无论代码运行时间是多少,间隔时间一定会是指定的时长。(可以递归调用)
let sayHello = setTimeout(() => {
alert('Hello!')
}, 2000);
setTimeout()的递归用法及退出递归:
let i = 0;
let a = setTimeout(function sayNumber() {
console.log(i++);
a = setTimeout(sayNumber, 1000);
}, 1000);
document.addEventListener('click', () => {
clearTimeout(a);
})
setInterval()
特征:重复执行一个函数,执行时间包含在时间间隔中。
作用:效率比requestAnimationFrame()低,但可以指定时间间隔
实例:
<div id="nowTime"></div>
<div id="nowDate"></div>
setInterval(() => {
let date = new Date();
let nowTime = date.toLocaleTimeString();
let nowDate = date.toDateString();
document.querySelector('#nowTime').textContent = nowTime;
document.querySelector('#nowDate').textContent = nowDate;
}, 1000);
执行时间包含在时间间隔中
requestAnimationFrame()
特征: requestAnimationFrame()会自动循环以达到最佳帧速率(60帧)重复且高效地运行函数。
作用:不可以指定帧率,只有在指定帧率的情况下才使用setInterval() / setTimeout()。
实例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>异步实例</title>
<style>
div {
display: inline-block;
}
</style>
</head>
<body>
<div>
↻
</div>
<script>
const spinner = document.querySelector('div');
let rotateCount = 0; // 旋转角度
let startTime = null;
let rAF;
function draw(timestamp) {
if (!startTime) {
startTime = timestamp;
}
let rotateCount = (timestamp - startTime) / 3;
spinner.style.transform = 'rotate(' + rotateCount + 'deg)';
if (rotateCount > 359) {
rotateCount = 0;
}
rAF = requestAnimationFrame(draw);
}
draw();
</script>
</body>
</html>
promises
特征:进行异步操作,再用异步结果进行下一步操作。
作用:Promises链可能很复杂。如果你嵌套了许多promises,可能会变成回调地狱。
实例:从服务器获取图片,并添加到网页中
fetch('https://img1.baidu.com/it/u=4127991555,3421789262&fm=253&fmt=auto&app=138&f=JPEG?w=680&h=454')
.then(response => response.blob())
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
Promise.all()
特征:等待多个promises完成,然后根据所有的结果进行进一步操作。
作用:从服务器获取多个资源,并使用Promise.all()等待所有资源可用。
实例:从服务器获取多个图片,并添加到网页中
function fetchAndDecode(url, type) {
return fetch(url).then(response => {
if (type === 'blob') {
return response.blob();
} else if (type === 'text') {
return response.text();
}
})
.catch(e => {
console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
});
}
let coffee = fetchAndDecode('https://img1.baidu.com/it/u=4127991555,3421789262&fm=253&fmt=auto&app=138&f=JPEG?w=680&h=454', 'blob');
let tea = fetchAndDecode('https://img0.baidu.com/it/u=1721391133,702358773&fm=253&fmt=auto&app=120&f=JPEG?w=500&h=625', 'blob');
Promise.all([coffee, tea]).then(values => {
console.log(values);
let objectURL1 = URL.createObjectURL(values[0]);
let objectURL2 = URL.createObjectURL(values[1]);
let image1 = document.createElement('img');
let image2 = document.createElement('img');
image1.src = objectURL1;
image2.src = objectURL2;
document.body.appendChild(image1);
document.body.appendChild(image2);
});
async/await
特征:像编写同步回调代码的语法来运行异步操作。
作用:async 写在 function 前,await 写在async函数内,等待一个Promise 对象。如果返回值不是promise对象,await 会把该值转换为已正常处理的Promise
实例:
// 图片
async function myFetch(url) {
let response = await fetch(url);
return response.blob();
}
myFetch('https://img1.baidu.com/it/u=3702625202,3169032464&fm=253&fmt=auto&app=138&f=JPEG?w=667&h=500')
.then(myBlob => {
console.log(myBlob);
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});