1. 同步与异步概述
# 1. 同步与异步概述
# 1.1 同步行为 synchronous
内存中顺序执行的处理器指令(排队执行)
# 1.1.1 特点
- 每条指令都会严格按照它们出现的顺序执行
- 每条指令执行后能立即获得存储在系统本地的信息
- 容易分析程序在执行到代码任意位置时的状态
# 1.1.2 例子
let x = 3;
x = x + 4;
等到最后一条指令执行完毕,存储在x的值立即可以使用
let girlName = "穆念慈"
function hr() {
girlName = "黄蓉"
console.log(`我是${girlName}`);
}
function gj() {
console.log(`${girlName}你好,我是郭靖,认识一下吧`);
}
hr()
gj()
//=>我是黄蓉
//=>黄蓉你好,我是郭靖,认识一下吧
# 1.2 异步行为asynchronous
类似于系统中断,即当前进程外部的实体可以触发代码执行
# 1.2.1 必要性
同步执行的代码必须要强制等待一个长时间的操作(比如向服务器发送请求并等待相应) 需要等待但是又不能阻塞程序的时候需要使用异步
# 1.2.2 特点
异步模式不会去等待某个耗时任务的结束才开始下一个任务,而是立即执行下一个任务,而耗时任务的后续逻辑一般会通过回调函数的方式来定义。
- 异步代码不容易推断
- 异步指令会生成一个入队执行的中断,什么时候触发中断,对JavaScript来说是个黑盒,无法预知
- 当前线程所有同步代码执行结束,回调才有机会出列被执行
# 1.2.3 例子
let x = 3;
setTimeout(() => x = x + 4, 1000);
线程不知道x
值何时会改变,这取决于回调何时从消息队列出列并执行
let girlName = "穆念慈"
function hr() {
setTimeout(() => {
girlName = "黄蓉"
console.log('我是黄蓉');
}, 0);
}
function gj() {
console.log(`${girlName}你好,我是郭靖,认识一下吧`);
}
hr()
gj()
//=>穆念慈你好,我是郭靖,认识一下吧
//=>我是黄蓉
# 1.3 异步运行机制
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
- 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
- 主线程不断重复上面的第三步
# 1.4 为什么要异步编程
为了让后续代码能够使用 x
,异步执行的函数需要在更新x的值后通知其他代码
如果程序不需要这个值,那么就继续执行,不必等待这个结果
所以要设计一个能够知道 x
什么时候可以读取的系统
来看一个例子
<button onclick="updateSync()">同步</button>
<button onclick="updateAsync()">异步</button>
<div id="output"></div>
<script>
function updateSync() {
for (var i = 0; i < 500; i++) {
document.getElementById('output').innerHTML = i;
}
}
function updateAsync() {
var i = 0;
function updateLater() {
document.getElementById('output').innerHTML = (i++);
if (i < 500) {
setTimeout(updateLater, 0);
}
}
updateLater();
}
</script>
- 同步任务 在updateSync函数运行过程中UI更新被阻塞,只有当它结束退出后才会更新UI,所以只有最后结果
- 异步任务 可以看到页面的变化过程
# 1.5 前端中异步的使用场景
1)定时任务:setTimeout,setInverval 2)网络请求:ajax请求,img图片的动态加载 3)事件绑定或者叫DOM事件,比如一个点击事件,我不知道它什么时候点,但是在它点击之前,我该干什么还是干什么。用addEventListener注册一个类型的事件的时候,浏览器会有一个单独的模块去接收这个东西,当事件被触发的时候,浏览器的某个模块,会把相应的函数扔到异步队列中,如果现在执行栈中是空的,就会直接执行这个函数。 4)ES6中的Promise
# 1.6 异步与并行的区别
- 异步是单线程的,并行是多线程的
- 异步:主线程的任务以同步的方式执行完毕,才会去依次执行任务列队中的异步任务
- 并行:两个或多个事件链随时间发展交替执行,以至于从更高的层次来看,就像是同时在运行(尽管在任意时刻只处理一个事件)
# 2. 关于回调函数
# 2.1 回调的定义
由调用者定义,交给执行者执行的函数
在JavaScript中,回调函数具体的定义为:函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A。我们就说函数A叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。
# 2.2 异步与回调
回调函数不一定属于异步,一般同步会阻塞后面的代码,通过输出结果也就得出了这个结论。
回调函数,一般在同步情境下是最后执行的,而在异步情境下有可能不执行,因为事件没有被触发或者条件不满足。
# 2.3 回调函数应用场景
- 资源加载:动态加载js文件后执行回调,加载iframe后执行回调,ajax操作回调,图片加载完成执行回调,AJAX等等。
- DOM事件及Node.js事件基于回调机制(Node.js回调可能会出现多层回调嵌套的问题)。
- setTimeout的延迟时间为0,这个hack经常被用到,settimeout调用的函数其实就是一个callback的体现
- 链式调用:链式调用的时候,在赋值器(setter)方法中(或者本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为你需要取值器返回你需要的数据而不是this指针,如果要实现链式方法,可以用回调函数来实现。
- setTimeout、setInterval的函数调用得到其返回值。由于两个函数都是异步的,即:调用时序和程序的主流程是相对独立的,所以没有办法在主体里面等待它们的返回值,它们被打开的时候程序也不会停下来等待,否则也就失去了setTimeout及setInterval的意义了,所以用return已经没有意义,只能使用callback。callback的意义在于将timer执行的结果通知给代理函数进行及时处理。
# 3. 以往的异步编程模式
Promise出现之前只支持定义回调函数来表明异步操作完成
串联多个异步任务通常需要深度嵌套回调函数(“回调地狱”)
function double(value) {
setTimeout(() => setTimeout(console.log, 0, value*2), 1000);
}
// 没有箭头函数就相当于
function double(value) {
setTimeout(function() {
setTimeout(function() {
console.log(value*2);
}, 0);
}, 1000);
double(3); // 6(大约1000毫秒之后)
let girlName = "裘千尺"
function hr(callBack) {
setTimeout(() => {
girlName = "黄蓉"
console.log('我是黄蓉');
callBack()
}, 0);
}
function gj() {
console.log(`${girlName}你好,我是郭靖,认识一下吧`);
}
hr(gj)
//=>我是黄蓉
//=>黄蓉你好,我是郭靖,认识一下吧
# 3.1 异步返回值
给异步操作提供一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)
function double(value, callback) {
setTimeout(() => callback(value * 2), 1000);
}
double(3, x => console.log(`我会得到: ${x}`)); // 我会得到:6 (大约1000毫秒后)
// 没有箭头函数就相当于
function double(value, callback) {
setTimeout(function() {
callback(value * 2)
}, 1000);
}
// 定义回调函数
function getResult(x) {
console.log(`我会得到: ${x}`);
}
double(3, getResult);
}); // 我会得到:6 (大约1000毫秒后)
这里的 setTimeout 调用告诉 JavaScript运行时 在1000 毫秒之后把一个函数推到 消息队列 上
这个函数会由运行时负责异步调度执行
而位于函数闭包中的回调及其参数在异步执行时仍然是可用的
# 3.2 失败处理
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw '第一个参数必须提供一个数字';
}
success(2 * value);
}catch (error) {
failure(error);
}
}, 1000);
}
// 定义回调函数
const successCallback = x => console.log(`成功:${x}`);
const failureCallback = error => console.log(`失败:${error}`);
double(3, successCallback, failureCallback); // 成功:6 (大约1000毫秒之后)
double('b', successCallback, failureCallback); // 失败:第一个参数必须提供一个数字 (大约1000毫秒之后)
这种模式已经不可取了,因为必须在初始化异步操作时定义回调
异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它
# 3.3 嵌套异步回调(“回调地狱”)
如果异步返值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw '第一个参数必须提供一个数字';
}
success(2 * value);
}catch (error) {
failure(error);
}
}, 1000);
}
// 定义回调函数
const successCallback = x => {
double(x, (y) => console.log(`成功:${y}`);
};
const failureCallback = error => console.log(`失败:${error}`);
double(3, successCallback, failureCallback); // 成功:12 (大约1000毫秒之后)
随着代码越来越复杂,嵌套回调策略不具有扩展性
# 4. JavaScript中的异步操作
# XMLHttpRequest
XMLHttpRequest对象,主要用于浏览器的数据请求与数据交互。XMLHttpRequest对象提供两种请求数据的方式,一种是同步,一种是异步。可以通过参数进行配置。默认为异步。
同步Ajax请求: 当请求开始发送时,浏览器事件线程通知主线程,让Http线程发送数据请求,主线程收到请求之后,通知Http线程发送请求,Http线程收到主线程通知之后就去请求数据,等待服务器响应,过了N年之后,收到请求回来的数据,返回给主线程数据已经请求完成,主线程把结果返回给了浏览器事件线程,去完成后续操作。
异步Ajax请求: 当请求开始发送时,浏览器事件线程通知,浏览器事件线程通知主线程,让Http线程发送数据请求,主线程收到请求之后,通知Http线程发送请求,Http线程收到主线程通知之后就去请求数据,并通知主线程请求已经发送,主进程通知浏览器事件线程已经去请求数据,则 浏览器事件线程,只需要等待结果,并不影响其他工作。
# setInterval&setTimeout
setInterval与setTimeout同属于异步方法,其异步是通过回调函数方式实现。 其两者的区别则setInterval会连续调用回调函数,则setTimeout会延时调用回调函数只会执行一次。
# requestAnimationFarme
requestAnimationFrame字面意思就是去请求动画帧,在没有API之前都是基于setInterval,与setInterval相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。
具体一点讲,如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次,如果刷新率是75Hz,那么这个时间间隔就变成了1000/75=13.3ms,换句话说就是,requestAnimationFrame的步伐跟着系统的刷新步伐走。
它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
# Object.observe - 观察者
Object.observe是一个提供数据监视的API,在chrome中已经可以使用。
是ECMAScript 7 的一个提案规范,官方建议的是谨慎使用级别,但是个人认为这个API非常有用,例如可以对现在流行的MVVM框架作一些简化和优化。
虽然标准还没定,但是标准往往是滞后于实现的,只要是有用的东西,肯定会有越来越多的人去使用,越来越多的引擎会支持,最终促使标准的生成。
从observe字面意思就可以知道,这玩意儿就是用来做观察者模式之类。
# Promise
Promise是对异步编程的一种抽象。
它是一个代理对象,代表一个必须进行异步处理的函数返回的值或抛出的异常。
也就是说Promise对象代表了一个异步操作,可以将异步对象和回调函数脱离开来,通过then方法在这个异步操作上面绑定回调函数。
# Generator&Async/Await
ES6的Generator却给异步操作又提供了新的思路,马上就有人给出了如何用Generator来更加优雅的处理异步操作。
Generator函数是协程在ES6的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。
异步操作需要暂停的地方,都用yield语句注明。
Async/Await与Generator类似,Async/await是Javascript编写异步程序的新方法。
以往的异步方法无外乎回调函数和Promise。
但是Async/await建立于Promise之上,个人理解是使用了Generator函数做了语法糖。
async函数就是隧道尽头的亮光,很多人认为它是异步操作的终极解决方案。
# 5. 浏览器是多线程的
- GUI渲染线程 - GUI渲染线程处于挂起状态的,也就是冻结状态
- JavaScript引擎线程 - 用于解析JavaScript代码
- 定时器触发线程 - 浏览器定时计数器并不是 js引擎计数
- 浏览器事件线程 - 用于解析BOM渲染等工作
- http线程 - 主要负责数据请求
- EventLoop轮询处理线程 - 事件被触发时该线程会把事件添加到待处理队列的队尾
# 参考
关于js中的同步和异步 https://www.cnblogs.com/c3gen/p/6170504.html (opens new window) 浅析JavaScript异步 https://www.cnblogs.com/aaron---blog/p/10903118.html (opens new window) javascript异步中的回调 https://segmentfault.com/a/1190000017935821 (opens new window) 谈一谈javascript异步 https://www.qdtalk.com/2018/12/23/javascript-async/ (opens new window)