1. 同步异步简介
javascript是一门单线程的语言,不同于java这些语言可以通过开辟一个新的线程达到多线程“同时”工作,JS不能同时进行多个任务和流程。
同步和异步其本质都是在只有一条流水线,也就是单线程,它们之间的差异就是在这个流水线上各个流程的执行顺序不同。
同步任务,所有的任务在主线程上排队,只有前一个任务执行完毕,才能执行后一个任务;异步任务,要执行的任务并不进入主线程,而是进入任务队列,等待主线程任务执行完毕,任务队列通知主线程,请求执行任务,此时异步任务才会进入主线程执行。
所谓的异步就是一件事情并不是连续完成的,可以分为多个阶段完成的,任务执行一段后会暂停,转而执行其他任务,过段时间又会再次执行这个任务,然后循环这样的任务执行切换,直至任务完成。
相反,同步是指一个任务是无间断的连续的执行完成的,上一个任务不完成,下一个任务永远无法开始。
2. promise对象
2.1 基本介绍
因为异步任务是在同步任务执行之后才会执行,故我们很难预测到何时才会将异步任务执行完毕,为了找到异步任务执行完毕后进行操作,JavaScript起初引入了回调函数和事件的解决方案。但回调函数在进行多层嵌套后就会产生回调地域问题,给程序开发者和js解析器带来了很大的挑战。
ES6引入了Promise对象,并统一了用法。Promise对象简单的说,就是一个容器,对象中保存着某个未来才会结束的事件结果。Promise可以获取异步操作的消息,同时统一了API,保证了各种异步操作都可以使用同一种方法进行处理。
Promise对象的状态只受异步执行结果的影响,状态有pending(进行中)、fulfilled(已成功)、rejected(已失败),一旦对象的状态转为fulfilled或rejected,就意味着状态不能在改变。promise可以将异步操作以同步操作的方式表达出来,避免了层层嵌套的回调函数。
2.2 基本用法
ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
// resolve和reject两个函数由JS引擎提供,不用自己部署
let promise = new Promise((resolve,reject) => {
if(){
resolve('successed!');
}else{
reject('error');
}
});
// promise实例完成状态转换后,使用then方法指定resolve和reject状态的回调函数
promise.then((value) => {
/*scuccess*/},(error) => {
/*error*/
});
// catch是then方法的别名,用于指定发生错误时的回调函数,reject能抛出错误,若resolve后再抛出错误时无效的
// 建议使用promise时使用下面的书写方式
promise.then(value => {})
.catch(error => {});
// Promise.all();方法用于将多个Promise实例包装成一个新的Promise实例,
// 只有所有的实例均成功,新实例才会成功,若不是promise实例调用resolve函数
let p = Promise.all([p1,p2]);
// Promise.race()方法同样是将多个promise实例包装成一个新的promise实例
// 多个实例总只要有一个实例率先改变的状态就代表了新实例的状态,若不是promise实例调用resolve函数
Promise.race([p1.p2]);
// Promise.resolve()方法将现有对象转成Promise对象
Promise.resolve('foo') /*等价于*/new Promise(resolve => resolve('foo'););
// Promise.reject()方法与Promise.resolve方法类似,只不多返回的实例状态是rejected
// done()方法用于最后做到处理未能捕捉到的错误,总是处于回调链的尾端,保证万无一失
promise.then()
.catch()
.done();
// finally()方法无论实例最终装填如何均会执行,
// 与donn方法的不同之处是接收一个普通的回调函数作为参数,该函数不管怎样必须执行
promise.then()
.catch()
.done()
.finally();
// Promise.try()可以不用区分同步函数或异步函数,它会使同步函数同步执行,异步函数异步执行,与async相同
Promise.try()
.then()
.catch()
.done()
.finally();
3.Generator函数
3.1 基本介绍
Generator函数是ES6提供了一种异步编程解决方案,语法行为与传统函数完全不同。Generator函数可以理解成是一个状态机,内部封住了多个状态,执行Generator函数会返回一个遍历器对象,而非像普通函数那样返回一个函数执行结果,通过遍历遍历器对象获取函数内部的每一个状态。
Generator函数在形式上与普通函数并没有太大的区别:function命令与函数名之间有一个星号;函数体内部使用yield表达式定义不同的内部状态。
Generator函数调用之后并不立即执行,返回的也不是一个执行结果,而是一个指向内部状态的指针对象,只有在调用next方法才会将指针移向下一个状态,并执行两个状态之间的代码。在指针执行的过程中会产生一个状态结果对象,即{value:当前yield表达式结果,done:false/true},其中value的结果遵循若next函数传入参数,next没传入便执行yield后边的表达式结果,done若遇到最后一个yield或return便会转为true,表示结束当前Generator函数。
function* func (){
yield 状态1;
yidld 状态2;
return;
}
let hw = func();
3.2 基本使用
Generator函数调用之后并不立即执行,返回的也不是一个执行结果,而是一个指向内部状态的指针对象,只有在调用next方法才会将指针移向下一个状态,并执行两个状态之间的代码。在指针移动的过程中会产生一个状态结果对象,即{value:当前yield表达式结果,done:false/true},其中value的结果遵循若next函数传入参数,next没传入便执行yield后边的表达式结果,done若遇到最后一个yield或return便会转为true,表示结束当前Generator函数。
yield表达式若用在表达式中,必须放在小括号内:console.log('yield' + yield 123);
yield表达式本身没有返回值,或者说返回值是undefined,next方法可以带一个参数充当yield表达式的返回值。next方法的参数表示的是上一次yield表达式的返回值,故在第一次执行next方法时传入的参数是无效的,且V8引擎也会自动忽略掉第一次使用next的参数,所以第一次使用next方法不用携带参数,若真的想在第一次使用next方法就传入参数,就在Generator函数外边再包一层。
function* func (){
yield 状态1;
yidld 状态2;
return;
}
let hw = func();
// 遍历器对象遍历方式1:next
hw.next();
hw.next();
hw.next();
// 遍历器对象遍历方式2:for循环,done为true时就会停止,且不包含返回对象
for(let f of hw){
console.log(f); // 注意不能用forEach方式循环,forEach函数参数为普通对象
}
// 扩展运算符,解构赋值,Array.from方法等内部都是遍历器接口,均可将Generator函数返回的Iterator对象当做参数
[...hw];
Array.from(hw);
let [x,y,z] = hw;
原生的JavaScript对象并没有遍历接口,故无法使用for…of循环,但通过Generator函数为它加上接口就可以使用了。
// 方式1:
function* objectEntries(obj){
let propKeys = Reflect.ownKeys(obj);
for(let propKey of propKeys){
yield [propKey,obj[propKey]];
}
}
for(let [key,value] of objectEntries({k:val1,k2:val2})){
console.log(key,value);
}
// 方式2
function* objectEntries(){
let propKeys = Object.keys(this);
for(let propKey of propKeys ){
yield [propKey,this[propKey]];
}
}
let obj = {k1:val1,k2:val2};
obj[Symbol.iterator] = objectEntries;
for(let [key,value] of obj){
console.log(key,value);
}
Generator函数返回的遍历器对象,有throw方法用来抛出异常,有return方法用来返回给定的值且终结函数。
next()方法是将yield表达式替换成一个值;
throw() 是将 yield 表达式替换成⼀个 throw 语句;
return() 是将 yield 表达式替换成⼀个 return 语句。
Generator函数内部调用了另一个Generator函数默认是没有任何效果的,但当在调用另一个Generator函数前加上yield*之后就等于将另一个Generator函数的遍历器对象状态进行了遍历。
// yield*:一个Generator函数内嵌套另一个Generator函数
function* gen1(){
yield 'aa';
yield 'bb';
}
function* gen2(){
yield 1;
yield* gen1();
yield 2;
}
// 等价于
function* gen2(){
yield 1;
yield 'aa';
yield 'bb';
yield 2;
}
// Generator函数作为对象对象属性
{
* gen1(){}, // 简写方式
gen2:function* (){} // 完整形式
}
Generat函数返回的遍历器是Generator函数实例,同时也继承了Generator函数的原型,这意味着可以若Generator函数原型上含有的属性可以通过Generator函数返回的遍历器进行获取;Generator函数本身不能作为普通的构造函数,这意味着不能使用new命令,因为Generator函数返回的总是遍历器对象而非this对象,这也就同时意味着无法访问到在Generator函数内部this对象上的属性。
若想让Generator函数既能返回一个正常的对象实例,既可用next方法又能获得正常的this。可以使用call方法绑定Generator函数内部的this,这样构造函数调用之后,这个空对象就是Generator函数的实例对象了。
// Generator函数原型属性
function* gen1(){}
gen1.prototype.hello = () => {};
gen1().hello();
//
function* gen2(){
this.a= 2;
}
let o = new gen2(); // 错误
gen2.a; // undefined
// 改造
function* gen3(){
this.a = 1;
yield this.b = 2;
}
let obj = {};
var f = gen3.call(obj );
f.next();
obj.a;
// 或
function* gen4(){
this.a = 1;
yield this.b = 2;
}
var f = gen4.call(gen4.prototype);
f.next();
f.a;
// 或
function* gen5(){
this.a= 1;
yield this.b = 2;
}
function F(){
return gen5.call(gen5.prototype);
}
var f = new F();
f.next();
f.a;
3.3 含义
- Generator状态机实现
传统的状态机是借助变量实现的,例如
let flag = true;
let func = () => {
if(flag){
// code
}else {
// code
}
};
// func函数没运行一次就会改变一次状态,但是多了一个保存状态的变量,显得有些累赘
let func = function* (){
while(true){
// code
yield;
// code
yield;
}
}
// 使用generat更加的简洁安全,也更符合函数式编程思想,写法也更优雅
- Generator与协程
协程是程序运行的一种方式,即可以使用单线程实现,也可以使用多线程实现,前者是一种特殊的子线程,后者是一种特殊的线程。
协程与子例程之间的差异:子例程采用的是堆栈式的后进先出的执行方式,也就说只有子进程执行完毕才能结束父进程;而协程则不同,多个进程之间是并行的,但在某一时刻正在执行的进程只有一个,各个进程之间可以相互切换执行权,当某一进程在执行的过程中时,其他进程均属于暂停状态。从实现的角度看,子例程在内存中只会存在一个栈,而协程则会同时存在多个栈,且只有一个栈正处于执行状态。
协程与普通线程的差异:根据协程的线程执行特点可以看出,它比较适合用于多线程运行的环境,它有着与普通线程许多相似的特点,都有自己的执行上下文,可分享全局变量。但它们的不同之处在于,协程是多个进程同时并行处于运行状态,但运行的协程只能有一个,其他协程均处于暂停状态,哪个进程执行是各协程之间自己分配的;而普通线程是抢先式的,具体哪个线程先获取得到资源,是由运行环境决定的。
JavaScript程序是单进程的,程序只能保持一个调用栈,引入协程概念后,意味着每个任务均可保持一个自己的调用栈,这样的好处是当遇到错误时不像异步操作的回调函数原始调用栈早早就结束了,而是可找到原始的调用栈。
3.4 应用场景
Generator函数内通过yield可以暂停函数,也可返回任意表达式的值,这些特点可以为generator函数找到诸多的运用场景。
- 异步操作同步化表达
Generator函数执行暂停的效果在于只有在执行的next函数才会执行yield之后的代码,故可将异步操作写在yield表达式中,这实际上不需要在写回调函数了,因为异步操作之后的操作可写在yield表达式之后与下一个yield之间。所以generator有一个重要的应用就是处理异步操作,改写回调函数
let gen1 = function* (){
// 显示对话框
yield settimeout(()=>{},2000);
// 隐藏对话框
};
for(let f of gen1()){
console.log(f.value,f.done);
}
- 控制流管理
一个多步操作十分耗时,可以采用回调函数嵌套的方式,但会形成回调地域的结果;若使用promise虽然将回调地域改成了直线执行的形式,但加入了大量的Promise的语法,使代码显得很臃肿。若使用Generator就会显得很简洁。
let gen2 = function* (tasks){ // tasks是一个数组,内含有多个操作任务
try{
for(let i = 0; i< tasks.length; i++){
let task = steps[i];
yield task();
}
}catch(e){}
};
// 以上代码只适合同步操作,也就是每个任务都是同步的,不能有异步操作,因为这里的代码已得到返回值之后,就会立即向下执行,并没有判断异步操作何时完成
- 部署Iterator接口
let gen3 = function* (obj){
let keys = Object.keys(obj);
for(let i =0; i < keys.length;i++){
let key = keys[i];
yield [key,obj[key]];
}
}
let obj = {};
for(let [key,value] of gen3(obj)){
// code
}
- 作为数据结构
说是generator可以作为一个数据结构,不如说是可作为一个数组解构,学过数据结构的同学大多都知道,栈,队,队列以及一些重要的算法大多都是借助数组这个东西完成的,当然在c和c++中指针也可以实现。由于Generator函数可以返回一系列的值,且可以读任意表达式提供类似数组的接口。
let gen5 = function*(){
yield fs.readFile.bind(null, file1);
yield fs.readFile.bind(null, file2);
}
// 上边依次返回两个函数,由于使用了Generator函数,可以向处理数组那样处理这两个函数
for(let f of gen5()){
// f是一个函数,可像调用一个回调函数那样调用他
}
// ES5可以使用数组,模拟Generator的这种用法
function doStuff() {
return [
fs.readFile.bind(null, file1),
fs.readFile.bind(null, file2)]; }
3.5 异步应用
异步编程对于JavaScript语言而言,真的是太重要了,没有异步,这门语言真的会废了。在ES6之前,异步编程主要有回调函数,事件监听,发布/订阅,Promise对象,自从JavaScript进化到ES6之后,有多了Generator和async两种方式。
回调函数本身处理异步并没有什么缺点,但当需要很多的异步有一个顺序的话,那时就需要在回调函数中嵌套下一个异步操作,这样就会造成回调地域,给程序的阅读理解等带来很多问题,也会出现强耦合。
Promise对象正是对解决回调地域所产生的,本质上它并不是一个新的语法功能,只是一个新的写法罢了,改嵌套调用为链式调用,而Promise对象异步解决方案最大的问题就是代码冗余,每个原来的任务都需要用Promise进行包装,无论什么代码,一眼望上去,那就是一堆then哪,是原来的语义变的开始模糊了。
前面也说到了协程的概念,正是因为Generator函数可以暂停执行和恢复执行,这才是它可以封装异步任务的根本原因。整个Generator函数就像是一个异步任务的容器,异步操作需要暂停的地方,都用yield语句注明。Generator更像是将一个任务使用yield进行切割成多个阶段,然后再使用next方法决定何时执行任务的下一个阶段。
next方法的返回值的value属性,是Generator函数向外输出数据;next方法可以通过参数的方式向Generator函数内部输入数据,这样就完成了数据交换。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1); g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
另外,Generator函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
function* gen(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g = gen(1); g.next(); g.throw(' 出错了');
// 出错了
完整的异步任务封装,通过下边的代码就能看到,虽然Generator函数将异步操作表示的很简洁,但是流程管理却很不方便,也就是什么时候执行第一阶段,什么时候执行下一阶段。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
})
.then(function(data){
g.next(data);
});
由于Generator函数对流程管理真的太恶心了,Thunk 函数是⾃动执⾏ Generator 函数的⼀种⽅法,这个函数诞生可追溯到上个世纪60年代,往往是将参数放到⼀个临时函数之中,再将这个临时函数传⼊函数体。这个临时函数就叫做 Thunk 函数。JavaScript 语⾔是传值调⽤,它的 Thunk 函数含义有所不同。在 JavaScript 语⾔中,Thunk 函数替换的不是表达式,⽽是多参数函
数,将其替换成⼀个只接受回调函数作为参数的单参数函数。
这里就不叙述Thunkify函数和Generator异步编程解决方案了,因为ES2017推出了异步编程的终极解决方案async函数,使异步编程更加的方便。
4.async函数
4.1 基本介绍
由于Generator函数的直接进行异步编程真的是难用啊,导致出现了很多的第三方模块解决这个问题。ES2017标准引入了async函数,这个函数的出现让异步编程那是又上一层楼啊,相当的方便哪,我时常将它看做是JavaScript语言标准对异步编程的终极解决方案。
async函数本质上就是Generator函数的语法糖,具体原因如下:
// Generator函数异步解决方案
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
const gen = function* () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
// 写成 async 函数,就是下⾯这样。
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
细心的一比较就会发现async函数就是将Generator函数的星号替换成了async,将await换成了await而已,仅此而已。即使在形式上,改进很小,但是在看不见的地方,改进主要体现在下面几点:
- 内置执行器:Generator函数的执行必须依靠执行器,这才有了co模块,而async函数自带执行器,也就是说,async函数的执行与普通函数一样,只需要一行代码即可。不像Generator还需要使用next方法后者调用co模块才能真正的执行,得出结果。
- 更好的语义:这在表面就能看到,async和await比起星号和yield,语义更加清晰,async表示异步,await表示等待。
- 使用的广泛性:co模块规定,yield命令后边只能跟Thunk函数或Promise对象,而await命令后边,既可以跟Promise对象,也可以跟原始类型的数值(此时并不代表等同于同步操作)。
- 返回值:async函数返回值为Promise对象,这比Generator函数返回的Iterator对象方便,用户可使用then方法执行下一步操作,也可以说,async函数可看做是多个异步操作,包装成了一个Promise对象,而await命令就是内部的then命令的语法糖。
4.2 基本语法
async函数返回的是一个Promise对象,可使用then方法添加回调函数,当遇到await命令时,会等待操作完成,再接着执行函数体后面的语句。
async function getStockPriceByName(name) {
const symbol = await getStockSymbol(name);
const stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result) {
console.log(result);
});
async函数有多种使用方法
// 函数声明式
async function f1(){}
// 函数表达式
let f2 = async function(){}
// 对象的方法
let obj = {async f3(){}};
// class的方法
class cla {
constructor(){}
async f4(){}
}
// 箭头函数
const f6 = async () => {};
async函数返回的是一个promise对象,函数内部返回的值将是then方法的回调函数的参数。
async function f1(){
return 1;
}
f1().then(v => {
console.log(v); // 1
});
async内部抛出的错误也会是promise对象的状态变为rejected。
async function f2(){
throw new Error('error');
}
f2().then(
v => console.log(v),
error => console.log(error); // error
);
async函数返回的Promise对象的状态,只有async函数内部的所有await命令都执行完毕或者遇到错误和return,promise对象的状态才会转变,即,只有async函数内部的异步操作执行完毕才会执行then方法中指定的回调函数,当然若函数内部有reject,后面的异步不会再执行。
await命令后边应该是一个Promise对象,但若不是,则会被转成一个resolve的Promise对象。
若希望前一个异步操作失败,不影响后一个异步操作,可将前一个异步操作放到try…catch语句中,或者是在前一个promise对象后边跟一个catch方法,用来捕捉异常错误。
await后边的异步操作错误,也就相当于async函数返回的Promise对象被reject,为防止出错,可将异步操作放到try…catch代码块中,多个await可同时放入哦。
4.3 使用注意点
- 为了避免前一个异步操作错误对下一个异步操作造成影响,建议将所有的await都放到try…catch语句块中后每个异步操作后边再加一个catch方法。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另⼀种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
- 多个await命令后面的异步操作,如果不存在继发的关系,最好让他们同时触发。
// 相继触发,耗时
let f1 = await get1();
let f2 = await get2();
// 同时触发写法1
let [f1,f2] = await Promise.all([get1(),get2()]);
// 同时触发写法2
let f1P = get1();
let f2P= get2();
let f1 = await f1P;
let f2 = await f2P;
- await命令只能写在async函数内,否则报错。
4.4 async函数原理和与其他异步编程比较
- async函数的实现原理就是将Generator函数和自动执行器封装到一个函数里。
async function fn1(){}
/*等同于*/
function fn2(){
return spawn(function(){
// ...
});
}
// spawn函数是自动执行器
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
- 与其他异步处理方法比较
虽然 Promise 的写法⽐回调函数的写法⼤⼤改进,但是⼀眼看上去,代码完全都是 Promise 的 API(then、catch 等等),操作本身的语义反⽽不容易看出来。
Generator 函数遍历,语义⽐ Promise 写法更清晰,⽤户定义的操作全部都出现在 spawn 函数的内部。这个写法的问题在于,必须有⼀个任务运⾏器,⾃动执⾏ Generator 函数,上⾯代码的 spawn 函数就是⾃动执⾏器,它返回⼀个 Promise 对象,⽽且必须保证 yield 语句后⾯的表达式,必须返回⼀个 Promise。
Async 函数的实现最简洁,最符合语义,⼏乎没有语义不相关的代码。它将 Generator 写法中的⾃动执⾏器,改在语⾔层⾯提供,不暴露给⽤户,因此代码量最少。如果使⽤ Generator 写法,⾃动执⾏器需要⽤户⾃⼰提供。
4.5 for await …of
for…of 循环⾃动调⽤这个遍历器的 next ⽅法,会得到⼀个Promise 对象。await ⽤来处理这个 Promise 对象,⼀旦 resolve,就把得到的值(x)传⼊ for…of 的循环体。
for await…of 循环的⼀个⽤途,是部署了 asyncIterable 操作的异步接⼝,可以直接放⼊这个循环。
let body = '';
async function f() {
for await(const data of req) body += data;
const parsed = JSON.parse(body);
console.log('got', parsed);
}
如果 next ⽅法返回的 Promise 对象被 reject,for await…of 就会报错,要⽤ try…catch 捕捉。
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
注意,for await…of 循环也可以⽤于同步遍历器。
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
Next
dom,bom,Event事件。
本文深入探讨JavaScript中的同步与异步概念,重点讲解Promise对象和Generator函数在处理异步操作中的作用。Promise简化了异步操作,避免了回调地狱,而Generator函数通过yield表达式实现了状态机,提供了暂停和恢复执行的能力,便于异步控制流管理。此外,还介绍了async函数作为Promise的语法糖,进一步提升了异步编程的便捷性。
1218

被折叠的 条评论
为什么被折叠?



