【JavaScript】同步与异步-异步与并行-异步运行机制-为什么要异步编程-异步与回调-回调地狱-JavaScript中的异步操作

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

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

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()
//=>裘千尺你好,我是郭靖,认识一下吧
//=>我是黄蓉

1.3 异步运行机制

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

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
浅析JavaScript异步 https://www.cnblogs.com/aaron—blog/p/10903118.html
javascript异步中的回调 https://segmentfault.com/a/1190000017935821
谈一谈javascript异步 https://www.qdtalk.com/2018/12/23/javascript-async/

猜你喜欢

转载自blog.csdn.net/weixin_44972008/article/details/114380206