1. 同步与异步概述

# 1. 同步与异步概述

# 1.1 同步行为 synchronous

内存中顺序执行的处理器指令(排队执行)

# 1.1.1 特点

  1. 每条指令都会严格按照它们出现的顺序执行
  2. 每条指令执行后能立即获得存储在系统本地的信息
  3. 容易分析程序在执行到代码任意位置时的状态

# 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()

//=>我是黄蓉
//=>黄蓉你好,我是郭靖,认识一下吧

同步.gif

# 1.2 异步行为asynchronous

类似于系统中断,即当前进程外部的实体可以触发代码执行

# 1.2.1 必要性

同步执行的代码必须要强制等待一个长时间的操作(比如向服务器发送请求并等待相应) 需要等待但是又不能阻塞程序的时候需要使用异步

# 1.2.2 特点

异步模式不会去等待某个耗时任务的结束才开始下一个任务,而是立即执行下一个任务,而耗时任务的后续逻辑一般会通过回调函数的方式来定义。

  1. 异步代码不容易推断
  2. 异步指令会生成一个入队执行的中断,什么时候触发中断,对JavaScript来说是个黑盒,无法预知
  3. 当前线程所有同步代码执行结束,回调才有机会出列被执行

# 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()

//=>穆念慈你好,我是郭靖,认识一下吧
//=>我是黄蓉

异步.gif

# 1.3 异步运行机制

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
  2. 主线程之外,还存在一个任务队列(task queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行
  4. 主线程不断重复上面的第三步

a5eb6aa17cf7017c7ac5c92b6168e38.png

# 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 回调函数应用场景

  1. 资源加载:动态加载js文件后执行回调,加载iframe后执行回调,ajax操作回调,图片加载完成执行回调,AJAX等等。
  2. DOM事件及Node.js事件基于回调机制(Node.js回调可能会出现多层回调嵌套的问题)。
  3. setTimeout的延迟时间为0,这个hack经常被用到,settimeout调用的函数其实就是一个callback的体现
  4. 链式调用:链式调用的时候,在赋值器(setter)方法中(或者本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为你需要取值器返回你需要的数据而不是this指针,如果要实现链式方法,可以用回调函数来实现。
  5. 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. 浏览器是多线程的

  1. GUI渲染线程 - GUI渲染线程处于挂起状态的,也就是冻结状态
  2. JavaScript引擎线程 - 用于解析JavaScript代码
  3. 定时器触发线程 - 浏览器定时计数器并不是 js引擎计数
  4. 浏览器事件线程 - 用于解析BOM渲染等工作
  5. http线程 - 主要负责数据请求
  6. 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)

上次更新: 2022/5/9 21:14:36