标签:function 异步 resolve console log js let Promise
1、js事件循环
js是单线程的,一个线程中,事件循环是唯一的,但是任务队列可以有多个。任务队列又可分为宏任务和微任务,
-
宏任务(
macro-task
)
script
(整体代码),setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
; -
微任务(
micro-task
)
process.nextTick
,Promise
,Object.observe
(已废弃),MutationObserver
(html5新特性)。
事件循环的顺序,决定了JavaScript
代码的执行顺序。它从script
(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task
。当所有可执行的micro-task
执行完毕之后。循环再次从macro-task
开始,找到其中一个任务队列执行,然后再执行所有的micro-task
,这样一直循环下去。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去。setTimeout
/Promise
等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务。
注:setTimeout
作为一个任务分发器,这个函数会立即执行,而它所要分发的任务,也就是它的第一个参数,才是延迟执行。但是到了setTimeout
设定的延时时间,他所分发的任务也不会立即执行。
是不是大段解释有些不直观,我们通过一个例子来理解:
setTimeout(function() {
console.log('timeout1');
})
new Promise(function(resolve) {
console.log('promise1');
for(var i = 0; i < 1000; i++) {
i == 99 && resolve();
}
console.log('promise2');
}).then(function() {
console.log('then1');
})
console.log('global1');
上面这段代码输出为:
promise1
promise2
global1
then1
timeout1
下面我们通过事件循环的顺序来分析以上代码的输出:
- 首先,事件循环从宏任务队列开始,这个时候,宏任务队列中,只有一个
script
(整体代码)任务。每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中去,所以,上面例子的第一步执行如下图所示。
- 第二步:
script
任务执行时首先遇到了setTimeout
,setTimeout
为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。
setTimeout(function() {
console.log('timeout1');
})
- 第三步:
script
执行时遇到Promise
实例。Promise
构造函数中的第一个参数,是在new
的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then
则会被分发到micro-task
的Promise
队列中去(具体Promise
细节见ES6
)。因此,构造函数执行时,里面的参数进入函数调用栈执行。for
循环不会进入任何队列,因此代码会依次执行,所以这里的promise1
和promise2
会依次输出。
script
任务继续往下执行,最后只有一句输出了globa1
,然后,全局任务就执行完毕了。 - 第四步:第一个宏任务
script
执行完毕之后,就开始执行所有的可执行的微任务。这个时候,微任务中,只有Promise
队列中的一个任务then1
,因此直接执行就行了,执行结果输出then1
,当然,他的执行,也是进入函数调用栈中执行的。
- 第五步:当所有的
micro-task
执行完毕之后,表示第一轮的循环就结束了。这个时候就得开始第二轮的循环。第二轮循环仍然从宏任务macro-task
开始。
这个时候,我们发现宏任务中,只有在setTimeout
队列中还要一个timeout1
的任务等待执行。因此就直接执行即可。
这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会再输出其他东西了。
当我们在执行setTimeout
任务中遇到setTimeout
时(或是其他的复杂的嵌套),它仍然会将对应的任务分发到setTimeout
队列中去,但是该任务就得等到下一轮事件循环执行了。整个过程仍然如我们开头所说。
2、js异步的方式
Javascript
中异步的5种实现方法,分别为callback
(回调函数)、发布订阅模式
、Promise
对象、ES6
的生成器函数、async/await
。
1)callback
回调函数是Javascript
异步编程中最常见的,由于JavaScript
中的函数是一等公民,可以将其以参数形式传递,故就有了回调函数一说,node.js
更是将回调发挥到了极致。
var fs = require('fs'); //node.js文件系统封装在fs模块中,提供了文件的读取、删除等操作
readFile("example.txt", function (err, contents) {
if(err) {
throw err;
}
console.log(contents);
});
console.log("Hi!");
使用了回调模式,Node
端会异步执行读取文件的请求,用事件循环的原理来理解就是我将文件读取事件放到事件队列中, 然后事件队列不再有事件接入(空闲)的时候,就进行事件循环,循环到文件读取事件的时候,就从线程池中分配一个线程处理这个事件,在这个过程中事件循环依然在处理队列中其他的事件,然后当我开辟出的线程处理完文件读取的事件后,就执行回调函数将结果(比如文件内容)放在事件队列的末尾,当循环到这个事件后,就可以进行文件内容的显示或是其他操作了,即或是抛出错误或是输出contents
。(细节见node
的异步I/O
)
虽然回调模式运行效果不错,但是有时会出现嵌套太多的回调函数,使自己陷入回调地狱。而且当有多个异步操作并行执行或结束,你需要跟踪多个回调函数。而Promise
就能改进这样的情况。
2)发布订阅模式
是设计模式的一种,并不是javascript
特有的内容,简单介绍一下发布订阅模式,发布订阅是两个东西,即发布和订阅,想象一下,有家外卖,你可以点外卖,这就是订阅,当你的外卖做好了,就会有人给你打电话叫你去取外卖,这就是发布,简单来说,发布订阅模式,有一个事件池,用来给你订阅(注册)事件,当你订阅的事件发生时就会通知你,然后你就可以去处理此事件:
发布订阅模式是很重要的一种设计模式,在JavaScript
中应用非常广泛,比如一些前端框架比如Vue
中的响应式数据就是根据发布订阅者模式结合数据劫持来实现的,node
中更是通过events
模块实现了发布订阅,events
模块只提供了一个对象: events.EventEmitter
,EventEmitter
的核心就是事件触发(.emit
)与事件监听器(.on
)功能的封装。可以通过require("events");
来访问该模块。
//event.js 文件
var events = require('events');
var emitter = new events.EventEmitter();
emitter.on('someEvent', function(arg1, arg2) {
console.log('listener1', arg1, arg2);
});
emitter.on('someEvent', function(arg1, arg2) {
console.log('listener2', arg1, arg2);
});
emitter.emit('someEvent', 'arg1 参数', 'arg2 参数');
运行结果为:
$ node event.js
listener1 arg1 参数 arg2 参数
listener2 arg1 参数 arg2 参数
以上例子中,emitter
为事件 someEvent
注册了两个事件监听器,然后触发了 someEvent
事件。运行结果中可以看到两个事件监听器回调函数被先后调用。 这就是EventEmitter
最简单的用法。
3)Promise
① Promise
不会传递一个回调函数给目标函数,而是返回一个Promise
,看一个例子:
let promise = readFile("example.txt");
promise.then(function (contents) {
//完成
console.log(contents); //异步操作成功则输出contents
}, function (err) {
//拒绝
console.log(err.message); //异步操作失败输出错误信息
})
在这个实例中,在执行文件读取的过程中,promise
变成pending
(进行中)的状态,即异步操作尚未完成,操作结束后,Promise
可能会进入settled
(已处理)状态,有两种结果:
Fullfilled
异步操作成功完成Rejected
异步操作未能成功
Promise
的这三种状态是不能用编程检测的,但是当Promise
的状态改变时,可以通过then()
方法采取特定的行动。then()
方法接受两个参数:
- 第一个是当
Promise
的状态变为Fullfilled
时要调用的参数,与异步操作相关的数据都会传给这个完成函数; - 第二个是
Promise
变为Rejected
时要调用的函数,所有与失败状态相关的数据都会传递给这个拒绝函数。
Promise
还有一个catch()
方法,相当于只给其传入拒绝处理程序的then()
方法:
promise.catch(function (err) {
//拒绝
console.log(err.message);
})
then()
方法和catch()
方法一起使用才能处理异步操作结果。
② 创建未完成的Promise
用Promise
构造函数可以创建新的Promise
,构造函数只接受一个参数:包含初始化Promise
代码的执行器函数。执行器函数接收两个参数:
resolve()
执行器成功完成时调用reject()
执行器失败时调用
看一个例子:
let promise = new Promise(function (resolve, reject) { //执行器函数会立即执行
console.log("Promise");
resolve(); //调用resolve()后会触发异步操作
})
promise.then(function () { //传入then()方法的函数会被添加到任务队列中异步执行,完成处理程序总是在执行器完成后被添加到任务队列的末尾
console.log("Resolved");
})
console.log("Hi!");
所以最后的输出为:
Promise
Hi!
Resolved
③ 创建已处理的Promise
Promise.resolve()
Promise.resolve()
方法只接受一个参数并返回一个完成态的Promise
,不会有任务编排的过程,只需要向Promise
添加一个或多个完后处理程序来获取值:
let promise = Promise.resolve(42);
promise.then(function(value) {
console.log(value); //42
})
Promise.reject()
他与Promise.resolve()
很像,唯一的区别是创建出来的是拒绝态的Promise
:
let promise = Promise.reject(42);
promise.catch(function(value) {
console.log(value); //42
})
④ 串联Promise
每次调用then()
或者catch()
方法时实际上创建并返回了另一个Promise
,只有当第一个Promise
完成或被拒绝后,第二个才会被解决,promise
就是通过then
的链式调用来解决了回调函数灾难,在后面的生成器实现异步,及async/await
中,Promise
将发挥巨大作用。看另一个例子:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
console.log("First"); //执行器函数会立即执行
})
p1.then(function (value) {
console.log(value);
}).then(function () { //then()方法返回的是一个Promise,所以可以继续调用then()方法
console.log("Finished")
})
console.log("Second");
所以结果为:
First
Second
42
Finished
⑤ 响应多个Promise
如果想通过监听多个Promise
来决定下一步的操作,则可以使用ES6
中提供的Promise.all()
和Promise.race()
。
Promise.all()
该方法只接受一个参数并返回一个Promise
,该参数是一个含有多个受监视Promise
的可迭代对象(比如数组)。只有当可迭代对象中所有的Promise
都被解决后返回的Promise
才会解决:
let p1 = new Promise(function (resolve, reject) {
resolve(42);
})
let p2 = new Promise(function (resolve, reject) {
resolve(43);
})
let p3 = new Promise(function (resolve, reject) {
resolve(44);
})
let p4 = Promise.all([p1, p2, p3]);
p4.then(function (value) {
console.log(value); //[ 42, 43, 44 ]
})
在这段代码中,每个Promise
解决时都传入一个数字,调用Promise.all()
方法创建的Promise p4
,最终当Promise p1
、p2
、p3
都处于完成状态后p4
才被完成。传入p4
完成处理程序的是一个包含每个解决值的数组。
所有传入Promise.all()
方法的Promise
只要有一个被拒绝,那么返回的Promise
没等所有Promise
都完成就立即被拒绝。
let p1 = new Promise(function (resolve, reject) {
resolve(42);
})
let p2 = new Promise(function (resolve, reject) {
reject(43);
})
let p3 = new Promise(function (resolve, reject) {
resolve(44);
})
let p4 = Promise.all([p1, p2, p3]);
p4.catch(function (value) {
console.log(value); //43
})
Promise.race()
这个方法和Promise.all()
不同之处在于只要有一个Promise
被解决返回的Promise
就被解决,无须等到所有的Promise
都被完成:
let p1 = Promise.resolve(42);
let p2 = new Promise(function (resolve, reject) {
resolve(43);
})
let p3 = new Promise(function (resolve, reject) {
resolve(44);
})
let p4 = Promise.race([p1, p2, p3]);
p4.then(function (value) {
console.log(value); //42
})
在这段代码中,p1
在创建时便处于已完成状态,其他的Promise
用于编排任务。实际上,传给Promise.race()
方法的Promise
会进行竞选,如果先解决的是已完成Promise
,则返回已完成Promise
;如果先解决的是已拒绝Promise
,则返回已拒绝Promise
;
4)Generator(生成器)函数
细节见博客中迭代器和生成器:https://blog.csdn.net/zl13015214442/article/details/88018172
Generator
函数是ES6
提供的一种异步编程解决方案,其行为类似于状态机。首先看一个简单的Generator
例子:
function *gen(){ //声明一个生成器
let t1 = yield "hello"; //yield 表示产出的意思 用yield来生成东西
console.log(t1);
let t2 = yield "world" + t1;
console.log(t2);
}
let d = gen(); //返回迭代器
let x = d.next(); //{value:"hello",done:false}
let y = d.next("aaaa"); //{value:"world",done:false}
d.next("bbbb"); //{value:undefined,done:true}
console.log(x.value,y.value);
仔细理解这句话:上面的例子中,如果把gen
函数当成一个状态机,则通过调用next()
方法来跳到下一个状态,即下一个yield
表达式,给next()
函数传值来把值传入上一个状态中,即上一个yield
表达式的结果。
//a.js
静夜思
const fs = require('fs');
function* gen(){ //生成器函数
let data = yield asyncReadFile('D:\\vue项目\\imoocmall1\\demo\\a.js');
console.log(data); //文件读取成功 则输出
let data2 = yield timer(1000);
console.log(data2); //过1s后输出 hello world
}
let it = gen();
it.next();
function timer(time){ //异步任务
setTimeout(() => it.next('hello world'),time)
}
function asyncReadFile(url) { //异步任务 读取文件
fs.readFile(url,(err,data) => {
it.next(data.toString())
})
}
console.log("李白");
输出结果为:
李白
静夜思
hello world
注:node
中回调函数优先级I/O
要优于setTimeout
。
可以看出通过暂停it.next()
方法的执行,来实现异步的功能,如果仅看gen
的函数里面内部,比如let data = yield asyncReadFile('D:\\vue项目\\imoocmall1\\demo\\a.js');
这一段,可以理解为data
等待异步读取文件asyncReadFile
的结果,如果有了结果,则输出,gen
继续向下执行。
我们可以实现更通用的异步任务执行器:run函数接受一个生成器函数,
//任务执行器
function run(taskDef) {
//创建一个迭代器
let task = taskDef();
// 开始执行任务
let result = task.next();
// 循环调用next()的函数
function step() {
// 如果任务未完成,则继续执行
if(!result.done) {
// 如果result.value是一个函数,会传入一个回调函数作为参数来调用它,回调函数遵循node有关执行错误的规定;可能的错误放在第一个参数err中,结果放在第二个参数中
if(typeof result.value === "function") {
result.value(function (err, data) {
if(err) {
result = task.throw(err);
return
}
result = task.next(data);
step();
})
} else {
// 如果不是函数,直接将其传入next()中
result = task.next(result.value);
step();
}
}
}
// 开始迭代执行
step();
}
现在,这个任务执行器已经可以用于所有的异步任务了。在node.js中,如果要从文件中读取一些数据,需要在fs.readFile()外围创建一层包装器,返回一个函数:
//让任务执行器调用的所有函数都返回一个可以执行回调过程的函数
let fs = require("fs");
function readFile(filename) {
return function (callback) {
fs.readFile(filename, callback);
}
}
下面是一段通过关键字yield
执行这个任务的代码:
run(function* () {
let nextval = yield 2;
console.log(nextval);
let contents = yield readFile("a.js");
console.log("打印出来的文件内容为" + contents);
});
console.log("i am the first");
执行结果:
2
i am the first
打印出来的文件内容为哈哈哈
这段代码中没有任何回调变量,异步的readFile()
操作却正常执行,除了yield
关键字外,其他代码与同步代码完全一样,只不过函数执行的事异步操作。所以遵循相同的接口,可以编写一些读起来像是同步代码的异步逻辑。
但是这种模式也有缺点,我们不能百分百确定函数中返回的其他函数一定是异步的,可以通过Promise
来优化,即只要每个异步操作都返回Promise,就可以简化并通用化这个过程,下面是用Promise
简化的任务执行器:
//任务执行器
function run(taskDef) {
let task = taskDef();
let result = task.next();
// 递归函数遍历
(function step() {
if(!result.done) {
// 调用promise.resolve将函数调用的返回值转化成一个promise
let promise = Promise.resolve(result.value);
promise.then(function (value) {
result = task.next(value);
step();
}).catch(function (error) {
throw result = task.throw(error);
step();
})
}
}())
}
// 定义一个可用于任务执行器的函数
let fs = require("fs");
function readFile(filename) {
return new Promise(function (resolve, reject) {
fs.readFile(filename, function (err, contents) {
if(err) {
reject(err);
} else {
resolve(contents);
}
})
})
}
run(function* () {
let nextval = yield 2;
console.log(nextval);
let contents = yield readFile("a.js");
console.log("打印出来的文件内容为" + contents);
});
console.log("i am the first");
运行结果:
i am the first
2
打印出来的文件内容为哈哈哈
5)async/await
ES6
中使用Generator
函数来做异步,在ES2017
中,提供了async/await
两个关键字来实现异步,让异步变得更加方便。
async/await
本质上还是基于Generator
函数,可以说是Generator
函数的语法糖,async
表示该函数以异步模式执行,它就相当于之前写的run
函数,而await
就相当于yield
,只不过await
表达式后面只能跟着Promise
对象,如果不是Promise
对象的话,响应应该别包裹在Promise
中,即会通过Promise.resolve
方法使之变成Promise
对象。await
必须放在async修饰的函数里面,就相当于yield
只能放在Generator
生成器函数里一样。一个简单的例子:
const timer = time => new Promise((resolve,reject) => {
setTimeout(() => resolve('hello world'),time)
});
async function main() {//async函数
let start = Date.now();
//可以把await理解为 async wait 即异步等待(虽然是yield的变体),当Promise对象有值的时候将值返回,即Promise对象里resolve(data)里面的data,作为await表达式的结果
let data = await timer(1000);
console.log(data,'time = ',Date.now() - start,'ms') // hello world time = 1003 ms
}
main();
参考:
JS
事件循环:https://www.jianshu.com/p/12b9f73c5a4f
Node
异步I/O
,发布订阅:
《深入浅出node
》
http://www.cnblogs.com/onepixel/p/7143769.html
Promise
,Generator
:《深入理解ES6》
https://blog.csdn.net/daydream13580130043/article/details/83105098
标签:function,异步,resolve,console,log,js,let,Promise 来源: https://blog.csdn.net/zl13015214442/article/details/87913230
本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享; 2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关; 3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关; 4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除; 5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。