首页 > 其他分享 >同步与异步

同步与异步

时间:2024-06-13 18:43:33浏览次数:13  
标签:异步 同步 函数 iterator Generator Promise 执行

同步与异步

单线程

由于 JavaScript​ 是一门单线程的语言,因此 JavaScript​ 在同一个时间只能做一件事。单线程意味着,如果在同个时间有多个任务的话,这些任务就需要进行排队,前一个任务执行完,才会执行下一个任务,会造成运行阻塞,严重的话会造成页面长时间无响应,js执行时间过长等等。

JavaScript​ 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript​ 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。

单线程的好处

  • 实现起来比较简单
  • 执行环境相对单纯

单线程的坏处

  • 坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行

如果排队是因为计算量大,CPU​ 忙不过来,倒也算了,但是很多时候 CPU​ 是闲着的,因为 IO​ 操作(输入输出)很慢(比如 Ajax​ 操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript​ 语言的设计者意识到,这时 CPU​ 完全可以不管 IO​ 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO​ 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript​ 内部采用的 “事件循环” 机制(Event Loop​)。

单线程虽然对 JavaScript​ 构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JavaScript​ 程序是不会出现堵塞的,这就是为什么 Node​ 可以用很少的资源,应付大流量访问的原因。

为了利用多核 CPU​ 的计算能力,HTML5​ 提出 Web Worker​ 标准,允许 JavaScript​ 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM​。所以,这个新标准并没有改变 JavaScript​ 单线程的本质。

同步

同步行为对应内存中顺序执行的处理器指令。每条指令都会严格按照它们出现的顺序来执行,而每条指令执行后也能立即获得存储在系统本地(如寄存器或系统内存)的信息。这样的执行流程容易分析程序在执行到代码任意位置时的状态(比如变量的值)。

同步操作的例子

简单的数学计算:

let xhs = 3

xhs = xhs + 4

在程序执行的每一步,都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才会执行。等到最后一条指定执行完毕,存储在 xhs​ 的值就立即可以使用。

首先,操作系统会在栈内存上分配一个存储浮点数值的空间,然后针对这个值做一次数学计算,再把计算结果写回之前分配的内存中。所有这些指令都是在单个线程中按顺序执行的。在低级指令的层面,有充足的工具可以确定系统状态。

异步

异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的(同步操作则必须要等)。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间的等待。

异步操作的例子

在定时回调中执行一次简单的数学计算:

let xhs = 3

setTimeout(() => (xhs = xhs + 4), 1000)

这段程序最终与同步代码执行的任务一样,都是把两个数加在一起,但这一次执行线程不知道 xhs 值何时会改变,因为这取决于回调何时从消息队列出列并执行。

异步代码不容易推断。虽然这个例子对应的低级代码最终跟前面的例子没什么区别,但第二个指令块(加操作及赋值操作)是由系统计时器触发的,这会生成一个入队执行的中断。到底什么时候会触发这个中断,这对 JavaScript​ 运行时来说是一个黑盒,因此实际上无法预知(尽管可以保证这发生在当前线程的同步代码执行之后,否则回调都没有机会出列被执行)。无论如何,在排定回调以后基本没办法知道系统状态何时变化。

为了让后续代码能够使用 xhs​ ,异步执行的函数需要在更新 xhs​ 的值以后通知其他代码。如果程序不需要这个值,那么就只管继续执行,不必等待这个结果了。

异步的应用场景

  • 定时任务:setTimeout、setInterval
  • 网络请求:ajax请求、动态创建img标签的加载
  • 事件监听器:addEventListener

任务队列和事件循环

JavaScript​ 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue​),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列)。

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

JavaScript​ 引擎怎么知道异步任务有没有结果,能不能进入主线程了呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop​)。

异步操作的模式

回调函数

回调函数是异步操作最基本的方法。但是回调函数有一个非常严重的缺点,那就是回调地狱(Callback Hell)问题。

Callback Hell

回调函数例子

下面是两个函数 f1​ 和 f2​,代码的意图是 f2​ 必须等到 f1​ 执行完成,才能继续执行。

function f1() {
  // ...
}

function f2() {
// ...
}

f1()
f2()

上面代码的问题在于,如果 f1​ 是异步操作,f2​ 会立即执行,不会等到 f1​ 结束再执行。 这时,可以考虑改写 f1​,把 f2​ 写成 f1​ 的回调函数。

回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling​),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。

Promise

Promise​作为典型的微任务之一,它的出现可以使JS​达到异步执行的效果。

Promise函数例子

const promise = new Promise((resolve, reject) => {
 resolve('a');
});
promise
    .then((arg) => { console.log(`执行resolve,参数是${arg}`) })
    .catch((arg) => { console.log(`执行reject,参数是${arg}`) })
    .finally(() => { console.log('结束promise') });

如果,我们需要嵌套执行异步代码,相比于回调函数来说,Promise​的执行方式如下列代码所示:

const promise = new Promise((resolve, reject) => {
 resolve(1);
});
promise.then((value) => {
     console.log(value);
     return value * 2;
    }).then((value) => {
     console.log(value);
     return value * 2;
    }).then((value) => {
    console.log(value);
    }).catch((err) => {
  console.log(err);
    });

即通过 then​ 来实现多级嵌套(链式调用),每个 Promise​ 都会经历的生命周期是:

  • 进行中( ​pending​​ ) - 此时代码执行尚未结束,所以也叫未处理的( unsettled​ )

  • 已处理( settled​ ) - 异步代码已执行结束 已处理的代码会进入两种状态中的一种:

    • 已完成( ​fulfilled​​ ) - 表明异步代码执行成功,由 ​resolve()​​ 触发
    • 已拒绝( ​rejected​​ )- 遇到错误,异步代码执行失败 ,由 ​reject()​​ 触发

因此,pending​,fulfilled​,rejected​ 就是 Promise​ 中的三种状态。在 Promise​ 中,要么包含 resolve()​ 来表示 Promise​ 的状态为 fulfilled​,要么包含 reject()​ 来表示 Promise​ 的状态为 rejected​。不然我们的 Promise​ 就会一直处于 pending​ 的状态,直至程序崩溃~。

除此之外,Promise​ 不仅很好的解决了链式调用的问题,它还有很多神奇的操作:

  • Promise.all(promises)​:接收一个包含多个 Promise​ 对象的数组,等待所有都完成时,返回存放它们结果的数组。如果任一被拒绝,则立即抛出错误,其他已完成的结果会被忽略
  • Promise.allSettled(promises)​:接收一个包含多个 Promise​ 对象的数组,等待所有都已完成或者已拒绝时,返回存放它们结果对象的数组。每个结果对象的结构为 {status: 'fulfilled' // 'rejected', value // reason}
  • Promise.race(promises)​:接收一个包含多个 Promise​ 对象的数组,等待第一个有结果(完成 / 拒绝)的 Promise​,并把其result / error​ 作为结果返回
function getPromises() {
    return [
        new Promise(((resolve, reject) => setTimeout(() => resolve(1), 1000))),
        new Promise(((resolve, reject) => setTimeout(() => reject(new Error('2')), 2000))),
        new Promise(((resolve, reject) => setTimeout(() => resolve(3), 3000))),
    ];
}

Promise.all(getPromises()).then(console.log);
Promise.allSettled(getPromises()).then(console.log);
Promise.race(getPromises()).then(console.log);

Generator

Generator​ 是 ES6​ 提出的一种异步编程的方案。因为手动创建一个 iterator​ 十分麻烦,因此 ES6​ 推出了 Generator​,用于更方便的创建 iterator​。即 Generator​ 就是一个返回值为 iterator​ 对象的函数。

iterator是什么

在讲 Generator​ 之前,我们先来看看 iterator​ 是什么?

iterator​ 中文名叫迭代器。它 JS中各种不同的数据结构 (Object、Array、Set、Map)提供统一的访问机制。任何数据结构只要部署了 Iterator​ 接口,就可以完成遍历操作。 因此 iterator​ 也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。

iterator​ 的作用

  • 为各种数据结构提供一个统一和简便的访问接口
  • 使得数据结构的成员能够按某种次序排列
  • ES6​ 创造了一种新的遍历命令 for…of​ 循环,Iterator​ 接口主要供 for…of​ 消费

iterator的结构

iterator​有 next​ 方法,该方法返回一个包含 value​ 和 done​ 两个属性的对象(我们假设叫 result​ )。value​是迭代的值,后者是表明迭代是否完成的标志。true​ 表示迭代完成,false​ 表示没有。iterator​ 内部有指向迭代位置的指针,每次调用 next​,自动移动指针并返回相应的 result​。

原生具备iterator接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数里的 arguments 对象
  • NodeList 对象

这些数据结构都有一个 Symbol.iterator 属性,可以直接通过这个属性来直接创建一个迭代器。也就是说,Symbol.iterator 属性只是一个用来创建迭代器的接口,而不是一个迭代器,因为它不含遍历的部分。

使用 Symbol.iterator 接口生成 iterator 迭代器来遍历数组的过程的例子:

let arr = ['a','b','c'];

let iter = arrSymbol.iterator;

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

for ... of 的循环内部实现机制其实就是 iterator,它首先调用被遍历集合对象的 Symbol.iterator 方法,该方法返回一个迭代器对象,迭代器对象是可以拥有 .next()​ 方法的任何对象,然后,在 for ... of 的每次循环中,都将调用该迭代器对象上的 .next​ 方法。然后使用 for i of​ 打印出来的 i​ 也就是调用 .next 方法后得到的对象上的 value 属性。

对于原生不具备 iterator 接口的数据结构,比如 Object,我们可以采用自定义的方式来创建一个遍历器。

let obj = {a: "hello", b: "world"};
// 自定义迭代器
function createIterator(items) {
    let keyArr = Object.keys(items);
    let i = 0;
    return {
        next: function () {
            let done = (i >= keyArr.length);
            let value = !done ? items[keyArr[i++]] : undefined;
            return {
                value: value,
                done: done,
            };
        }
    };
}

let iterator = createIterator(obj);
console.log(iterator.next()); // "{ value: 'hello', done: false }"
console.log(iterator.next()); // "{ value: 'world', done: false }"
console.log(iterator.next()); // "{ value: undefined, done: true }"

接下来,来聊聊Generator。

通过一个例子来看看Gnerator的特征:

function* createIterator() {
  yield 1;
  yield 2;
  yield 3;
}

// generators可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

Generator 函数是 ES6 提供的一种异步编程解决方案。形式上,Generator 函数是一个普通函数,但是有两个特征:

  • function 关键字与函数名之间有一个星号
  • 函数体内部使用 yield 语句,定义不同的内部状态

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object)。

打印看看 Generator 函数返回值的内容,发现 Generator 函数的返回值的原型链上确实有 iterator 对象该有的 next,这充分说明了 generator 的返回值是一个 iterator。除此之外还有函数该有的 return 方法和 throw 方法。

在普通函数中,我们想要一个函数最终的执行结果,一般都是 return 出来,或者以 return 作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。但在 generator 中,就打破了这几点,所以 generator 和普通的函数完全不同。当以 function*​ 的方式声明了一个 Generator 生成器时,内部是可以有许多状态的,以 yield 进行断点间隔。期间我们执行调用这个生成的 Generator,他会返回一个遍历器对象,用这个对象上的方法,实现获得一个 yield 后面输出的结果。

function* generator() {
    yield 1
    yield 2
};

let iterator = generator();
iterator.next() // {value: 1, done: false}
iterator.next() // {value: 2, done: false}
iterator.next() // {value: undefined, done: true}

yield 和 return 的区别:

  • 都能返回紧跟在语句后面的那个表达式的值
  • yield 相比于 return 来说,更像是一个断点。遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句不具备位置记忆的功能
  • 一个函数里面,只能执行一个 return 语句,但是可以执行多次 yield 表达式
  • 正常函数只能返回一个值,因为只能执行一次 return;Generator 函数可以返回一系列的值,因为可以有任意多个 yield

语法注意点:

  • yield 表达式只能用在 Generator 函数里面
  • yield 表达式如果用在另一个表达式之中,必须放在圆括号里面
  • yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号
  • 如果 return 语句后面还有 yield 表达式,那么后面的 yield 完全不生效

使用Generator的其余注意事项:

  • yield 不能跨函数。并且 yield 需要和 * 配套使用,别处使用无效。2、箭头函数不能用做 generator
  • 箭头函数不能用做 generator
function* createIterator(items) {
  items.forEach(function (item) {
    // 语法错误
    yield item + 1;
  });
}

那么 Generator 到底有什么用呢?

  • 因为 Generator 可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个 generator 就可以实现需要用面向对象才能实现的功能
  • Generator 还有另一个巨大的好处,就是把异步回调代码变成“同步”代码。这个在 ajax 请求中很有用,避免了回调地狱

async/await

async / await 是 ES7 提出的关于异步的终极解决方案。互联网上关于 async / await 是谁的语法糖这块有两个版本,这两个版本的所发都没有错。

  • 第一个版本说 async / await 是 Generator 的语法糖
  • 第二个版本说 async / await 是 Promise 的语法糖

关于第一个版本的 async / await 是 Generator 的语法糖:

所谓 Generator 语法糖,表明的就是 aysnc / await 实现的就是 generator 实现的功能。但是 async / await 比 generator 要好用。因为 generator 执行 yield 设下的断点采用的方式就是不断的调用 iterator 方法,这是个手动调用的过程。针对 generator 的这个缺点,后面提出了co这个库函数来自动执行 next,相比于之前的方案,这种方式确实有了进步,但是仍然麻烦。而 async 配合 await 得到的就是断点执行后的结果。因此 async / await 比 generator 使用更普遍。

总结下来,async 函数对 Generator 函数的改进,主要体现在以下三点:

  • 内置执行器:Generator 函数的执行必须靠执行器,因为不能一次性执行完成,所以之后才有了开源的 co 函数库。但是,async 函数和正常的函数一样执行,也不用 co 函数库,也不用使用 next 方法,而 async 函数自带执行器,会自动执行
  • 适用性更好:co 函数库有条件约束,yield 命令后面只能是 Thunk 函数或 Promise 对象,但是 async 函数的 await 关键词后面,可以不受约束
  • 可读性更好:async和 await,比起使用 * 号和 yield,语义更清晰明了

关于 async / await 是 Promise 的语法糖:

如果不使用 async / await 的话,Promise 就需要通过链式调用来依次执行 then 之后的代码:

function counter(n) {
 return new Promise((resolve, reject) => { 
    resolve(n + 1);
    });
}

function adder(a, b) {
return new Promise((resolve, reject) => {
resolve(a + b);
});
}

function delay(a) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(a), 1000);
});
}

// 链式调用写法
function callAll() {
counter(1)
.then((val) => adder(val, 3))
.then((val) => delay(val))
.then(console.log);
}
callAll(); // 5

虽然相比于回调地狱来说,链式调用确实顺眼多了。但是其呈现仍然略繁琐了一些。而async/await的出现,就使得我们可以通过同步代码来达到异步的效果:

async function callAll(){
   const count = await counter(1);
   const sum = await adder(count + 3);
   console.log(await delay(sum));
}
callAll();// 5
复制代码

由此可见,Promise搭配async/await的使用才是正解!

小结

  • promise 让异步执行看起来更清晰明了,通过 then 让异步执行结果分离出来
  • async / await 其实是基于Promise的。async 函数其实是把 promise 包装了一下。使用 async 函数可以让代码简洁很多,不需要 promise 一样需要些 then,不需要写匿名函数处理promise的resolve值,也不需要定义多余的 data 变量,还避免了嵌套代码
  • async函数是Generator函数的语法糖。async函数的返回值是 promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。同时,我们还可以用 await 来替代 then 方法指定下一步的操作
  • 感觉 Promise + async 的操作最为常见。因为 Generator 被 async 替代了呀

异步操作的流程控制

串行执行

我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。

并行执行

即所有异步任务同时执行,等到全部完成以后,才执行final函数。

定时器的创建和清除

标签:异步,同步,函数,iterator,Generator,Promise,执行
From: https://www.cnblogs.com/thornboo/p/18246523/synchronous-and-asynchronous-bjeuc

相关文章

  • 解决@LocalStorageProp值同步问题的详细指南
    在华为鸿蒙操作系统(HarmonyOS)的开发中,@LocalStorageProp是一个关键的装饰器,用于在页面级别的UI状态存储中实现数据的单向同步。然而,开发者在使用@LocalStorageProp时可能会遇到值未按预期同步的问题。本文将详细介绍如何正确使用@LocalStorageProp,并通过父组件的状态更新来......
  • cwRsync小小的工具大大的坑——windows文件同步工具
    *cwRsync是linux工具:Rsync,在windows上的版本两台windos主机之间的某个或多个目录进行同步,源端安装服务端,目标端安装客户端下载地址:cwRsync:cwRsync文件同步工具下载内容如下cwRsyncServer_4.0.5_Installer.zip为服务端软件cwRsync_4.0.5_Installer.zip为客户端软件服......
  • AP3462 4-30V 输入、1.2A 固定输出同步降压驱动器
    产品描述 AP3462是一款支持宽电压输入的同步降压电源管理芯片,输入电压4-30V范围内可实现1.2A的连续电流输出。AP3462具有优秀的恒压特性。AP3462采用电流模式的环路控制原理,实现了快速的动态响应。AP3462工作开关频率为220kHz,具有良好的EMI特性。AP3462内置有......
  • AP3464 4-30V 输入、2.4A 输出同步降压驱动器
    产品描述 AP3464是一款支持宽电压输入的同步降压电源管理芯片,输入电压4-30V范围内可实现2.4A的连续电流输出。通过调节FB端口的分压电阻,设定输出1.8V到28V的稳定电压。AP3464具有优秀的恒压/恒流(CC/CV)特性。AP3464采用电流模式的环路控制原理,实现了快速的动态......
  • AP3465 4-30V 输入、3A 输出同步降压驱动器
    产品描述 AP3465是一款支持宽电压输入的同步降压电源管理芯片,输入电压4-30V范围内可实现3A的连续电流输出。通过调节FB端口的分压电阻,设定输出1.8V到28V的稳定电压。AP3465具有优秀的恒压/恒流(CC/CV)特性。AP3465采用电流模式的环路控制原理,实现了快速的动态响......
  • 线程池的使用:批量导入、数据汇总、异步保存搜索记录
    文章目录1、场景一:MySQL批量导入数据到ES1.1CountDownLatch1.2流程图1.3代码实现1.4效果2、场景二:数据汇总2.1流程图2.2代码实现3、场景三:异步调用3.1需求3.2代码实现1、场景一:MySQL批量导入数据到ES场景:需要将库里的1000万左右的数据量,导入到ES索引库中......
  • 探索Java并发容器的深坑与妙用:从同步到并发的进化之路
    探索Java并发容器的深坑与妙用:从同步到并发的进化之路在Java的世界里,并发编程一直是一个复杂而又充满挑战的领域。尤其是在处理并发容器时,不同版本、不同类型的容器带来的种种问题和优化,更是让人眼花缭乱。今天,我们将深入探讨Java并发容器的演化历程,揭示其中的深坑与妙用,帮......
  • 京准电钟 | NTP网络时间同步协议原理及其应用介绍
    京准电钟|NTP网络时间同步协议原理及其应用介绍京准电钟|NTP网络时间同步协议原理及其应用介绍京准电子科技官微——ahjzsz摘要:首先对计算机网络时间同步相关技术进行了介绍,然后阐述了时间同步技术在现代计算机网络中的应用与发展,最后指出时间同步网络在下一代网络(N......
  • 如何将 iPhone 的照片同步到 windows 电脑上
    首先在电脑上,新建一个文件夹,并把共享权限打开。文件夹右键属性,共享,添加Everyone。然后,让手机和电脑连接到同一个局域网,手机热点即可。在手机端看文件app,找到电脑的共享文件夹。手机端文件连接服务器,输入电脑的IP地址。电脑IP地址查询:电脑端【设置】/【网络......
  • (天源)代理 TP4366E ESOP-8 1A 同步移动电源
    产品描述TP4366E是一款专为移动电源设计的同步升压的单芯片解决方案,内部集成了线性充电管理模块、同步放电管理模块、电量检测与LED指示模块、保护模块。TP4366E内置充电与放电功率MOS,充电电流固定为0.8A,同步升压支持1A输出电流。TP4366E内部集成......