ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

js和异步

2019-04-22 21:52:18  阅读:168  来源: 互联网

标签: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任务执行时首先遇到了setTimeoutsetTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中。
setTimeout(function() {
    console.log('timeout1');
})

在这里插入图片描述

  • 第三步:script执行时遇到Promise实例。Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了,而后续的.then则会被分发到micro-taskPromise队列中去(具体Promise细节见ES6)。因此,构造函数执行时,里面的参数进入函数调用栈执行。for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1promise2会依次输出。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    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.EventEmitterEventEmitter 的核心就是事件触发(.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 p1p2p3都处于完成状态后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
PromiseGenerator:《深入理解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. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有