JavaScript异步解决方案

215 阅读14分钟

异步概念

一个任务,不是连续完成的,可以理解该任务被人为分成两段,先执行一段,然后转而执行其他任务,等做好了准备,再回头来执行第二段。
比如:读取文件任务

  • 程序的第一段是想操作系统发送请求,要求读取哪个文件
  • 程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段,对读取到的文件内容做处理

解决方案

解决方案包括使用方式(以读取文件的例子)和错误处理。

回调函数

概念

  • JavaScript对于异步的实现,就是以回调函数的方式。
  • 回调函数,就是将把任务的第二段单独写在一个函数中,等到重新执行这个任务的时候(第一段任务完成),就调用这个函数。
  • Node规定:回调函数的参数第一个必须是错误对象 error
    • 异步操作分别两个阶段,第一阶段执行过程中遇到错误的时候,由于第一阶段的任务执行完毕之后对应的上下文环境已经结束了,第二阶段无法捕获到,只能通过参数的形式来传递错误。

代码

// 以回调函数的形式 实现读取文件
const fs = require('fs')


fs.readFile('./index1.html','utf-8',function(err,data){
    if(err) throw err
    console.log(data)
})

缺点: 回调地狱

  • 概念: 多个回调函数嵌套,下一个回调函数依赖于上个回调函数的执行结果
  • 原因: 人们习惯从上到下写代码,期望第一行发生的任何事情都会在第二行代码开始运行之前完成
  • 缺点:嵌套函数 之间存在强耦合性,很难处理每一个错误,代码庞大,难以维护。
  • 解决回调地狱:
    • 保持代码简短(通过函数名引用)
    • 模块化
    • 处理每一个错误

Promise

概念

  • Promise 对象用于表示一个异步操作的最终完成或者失败,以及结果值
  • Promise()是一个构造函数,用于实例化 Promise对象
  • Promise是一种新的写法,而不是新的功能
  • 回调函数本身没有问题,问题是多个回调函数嵌套,容易形成回调地狱,Promise就是为了解决这个问题

语法

// 实例化一个Promise对象
new Promise(
  // excutor 执行器
  function(resolve,reject){}
)
// 使用函数包裹
function createPromise(...){
	return new Promise(
  	function(resolve,reject)
  )
}

语法解析

  • Promise构造函数的参数 是一个函数,称为执行器
  • Promis构造函数实例化对象时 会立即调用执行器函数(马上执行 ,因此 Promise对象一般使用函数包裹)

执行器函数解析

参数

执行器函数接收两个参数,resolve和reject。这两个都是函数,由JS引擎提供

执行

执行器函数内通常会执行一些异步操作,一旦异步操作完成,可以通过判断,要么调用resolve函数将Promise的状态由pending改为fulfilled,或者调用reject将Promise的状态修改为rejected

错误

如果在执行器函数内抛出一个错误,那么该promise的状态就会变化rejected,执行器函数的返回值将被忽略。

Promise的状态

Promise对象有三种状态

  • pending 初始状态
  • fulfilled  成功状态
  • rejected 失败状态

pending状态的Promise对象可能会变为fulfilled状态并传递一个值给相应的状态处理方法。也可以变为失败状态并传递失败信息。

属性

  1. Promise.length length属性,其值总是为1
  2. Promise.prototype 表示Promise构造函数的原型

方法(由Promise调用)

Promise.resolve(value)

返回一个给定值解析后的Promise对象。(将参数转化为Promise对象)。

返回值规则
  • 如果value为基本类型的值,则返回一个resolved状态的Promise,并将value作为返回的Promise的第一个回调函数的参数。(作为成功回调函数的参数)
  • 如果value为Promise,则返回这个Promise。
  • 如果value为thenable对象,(定义了then方法的对象),返回的Promise会紧随这个对象,并且采用它的最终状态。(疑问)
注意

不要在解析为自身的thenable对象上调用Promise.resolve(),这将会导致无线递归。(有疑问)

Promise.reject(reason)

返回一个带有拒绝原因参数的Promise对象

Promise.reject("Testing static reject").then(
  function(reason) {
  // 成功时的回调函数未被调用
}, function(reason) {
  // 失败时的回掉函数 执行了
  console.log(reason); // "Testing static reject"
});

Promise.all(iterable)

Promise.race(iterable)

原型属性

原型属性是指Promise.prototype上的属性,即每个实例的公有属性。

  • Promise.prototype.constructor  返回被创建的实例的构造函数,默认为Promise()

原型方法

Promise.prototype.then(onFulfilled,onRejected)

参数

最多需要两个参数,Promise状态变为成功时的回调函数以及状态变为失败时的回调函数。

参数省略的情况

如果忽略针对某个状态的回调函数参数,或者提供非函数参数,那么then方法将会丢失关于该状态的回调函数信息,但是并不会产生错误。
如果调用then方法的Promise状态发生变化,但是then方法中没有关于这种状态的回调函数,那么then方法将自动创建一个没有经过回调函数处理的新Promise对象,这么新的Promise的状态就是原来的Promise的终态。(仔细理解)

var p = new Promise((resolve, reject) => {
    resolve('foo')
})
// 'bar'不是函数,会在内部替换为 (x)=>x
p.then('bar').then((value)=>{
	console.log(value) //'foo'
})

参数解析
  • onFulfilled 当Promise状态变成接受状态时,该参数作为回调函数被调用。该函数有一个参数,即接受的最终结果。
  • onRejected 当Promise变成拒绝状态,该参数作为回调函数被调用。该函数有一个参数,即拒绝的原因。
返回值

返回值存在以下几种情况:

  • 返回一个值
  • 不返回值
  • 返回一个Promise
    • 返回一个完成状态的Promise
    • 返回一个拒绝状态的Promise
    • 返回pengding状态的Promise

具体分析如下:

  1. 如果then中的回调函数返回一个值,那么then中返回 的promise会变成接受状态,并且将返回的值作为接受状态的回调函数的的参数值。
// 实例化一个Promise对象
var p = new Promise((resolve,reject)=>{ resolve('foo')})
// 首先 resolve函数 可以向回调函数传递一个值,通过resolve(value)的方式
p.then((value)=>{
	return 'xhb' // then中的回调函数返回 return一个值
})
// 首先 我们知道 then方法 会返回一个Promise对象
// 根据规则,如果在then中的回调函数中返回一个值, 会将整个then方法返回的promise的状态变为成功状态
// 并且 我们在上一个promise return的值,会作为下一个promise的then方法中第一个回调函数(完成状态的
//的回调函数)的参数,即可以在完成状态的回调函数接受到这个值,我们可以打印一下
p.then((value)=>{
	return 'xhb'
}).then(value => {
	console.log(value)
})

在浏览器中运行如下:

  1. 如果then中回调函数没有返回值,那么then方法返回的Promise状态会变成接受状态,并且这个promise的第一个回调函数(成功状态的回调)会接受一个undefined的参数

具体分析如下:

// 实例化一个Promise
var p = new Promise((resolve,reject)=>{ resolve("foo")})

p.then((value)=>{
  console.log(value) // 打印参数 这里并没有返回值 
  // 根据规则,then方法就会返回一个resolved状态的promise,并且接收一个值为undefinde的参数
}).then(
  function(value){
    // 接收一个值为undefined的参数
    console.log(value) // undefined
  }
)

在浏览器中运行如下:

  1. 如果then方法中的回调函数抛出一个错误,那么then方法将返回一个rejected状态的Promise,并且抛出的错误将作为这个Promise的第二个回调函数(拒绝状态的回调函数)的参数。
  2. 返回一个resolved状态的Promise,如果then中的回调函数返回一个已经是成功接受状态的Promise,那么then中返回的Promise也是成功状态,并且将那个Promise的接受状态的回调函数参数值作为then方法返回的Promise的接受状态的回调函数的参数(成功状态的回调函数的参数 会传递)

具体分析如下:

// 实例化一个Promise
var p = new Promise({(resolve,reject)=>{ resolve('foo')})
// 返回一个成功状态的Promise
// ? 调用构造函数  实例化Promise的时候,就已经执行了执行器函数,
p.then(()=>return new Promise((resolve)=>{ resolve('return PromiseValue')})

在浏览器中运行

Promise.prototype.catch(onRejected)

catch方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then()。

参数
  • OnReject   当Promise被rejected时,被调用的一个Funtion。该函数拥有一个参数:
    • reason rejection的原因
    • 如果 onRejected抛出一个错误或返回一个本身失败的Promise,通过catch()返回的Promise被rejected,否则,它将显示为成功。

Promise.prototype.finally(onFinally)

finally()方法返回一个Promise。在Promise结束时,无论结果是fulfilled,或者是rejected,都会执行执行的回调函数。
这避免同样的语句需要在then方法和catch方法中各写一次。

创建Promise

Promise对象是由关键字new及其构造函数来创建的。
该构造函数会把一个叫做处理器的函数作为它的参数。
处理器函数接受两个函数resolve和reject作为它的参数。
当异步任务顺利完成且返回结果值的时候,会调用resolve函数。
而当异步任务失败且返回失败原因时,会调用reject函数。

实例

//使用Promise 完成读取文件
const fs = require('fs')
// 实例化一个promise对象
const readFilePromise = function(fileName){
	return new Promise((resolve,reject)=>{
  	fs.readFile(fileName,'utf-8',(err,data)=>{
    	if(err) reject(err)
        resolve(data)
    })
    
  })
}
const p1 = readFilePromise('./index.html')
p1.then((data)=>{
    console.log(data)
}).catch(err=>{
    console.log(err)
})

Generator

概念

Generator,生成器。
一个generator function(生成器函数)返回的是一个生成器对象。

语法

代码

// 声明一个generator函数
function* helloWorldGenerator(){
  console.log('1')
	yield 'hello'
  console.log('2')
  yield 'world'
  return 'ending'
}
// 使用 Generator函数生成一个生成器对象
var g = helloWorldGenerator()
// 调用next方法
g.next()
// { value: 'hello', done: false }

g.next()
// { value: 'world', done: false }

g.next()
// { value: 'ending', done: true }

g.next()
// { value: undefined, done: true }

理解

  1. 从形式上,Generator函数是一个普通函数。但是有两个特征。
  2. function关键字和函数名之间有个 * 号
  3. 函数体内使用了yield(产出)关键字
  4. 从语法上,可以理解成 Generator是一个状态机,封装了多个内部状态(有疑问)
  5. 执行Generator函数 会返回一个遍历器对象,可以使用for..of循环,遍历出Genertor函数内的每一个状态。

上面代码中,定义了一个Generator函数,该函数内部有三个状态: hello ,world和return语句( 结束执行)

调用

Generator函数的调用方法和普通函数一样,也是在函数名后面加上一对圆括号。
但是不同的是,Generator函数调用之后,并不会马上执行,返回的也不是函数运行的结果,而是一个指向内部状态的指针对象,也就是遍历器对象。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或者上一次停下来的地方开始执行。直到遇到下一个yield表达式或者return语句为止。
Generator函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

next()方法

调用next方法返回一个对象,包含两个属性,一个是value属性,表示yield后表达式的值,一个是done属性,值为true或者false,表示是否还有下一个状态,生成器函数是否执行完毕。
遇到yield语句,就停下来,去计算yield表达式之后的值,当计算完毕之后,将结果赋值给value属性。
第一次调用,Generator函数开始执行,直到遇到第一个yield表达式为值。

yield 表达式

由于Generator函数返回的是一个遍历器对象。只有调用next方法才会遍历下一个内部状态。
遍历器对象的next方法的运行逻辑如下:

  1. 遇到yield语句,函数就暂停,并将紧跟在yield语句后面的那个表达式的值,作为返回的对象的value属性值。
  2. 下一次调用next方法时,再继续往下执行。直到遇到下一个yield语句
  3. 如果没有遇到新的yield语句,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象value属性值。
  4. 如果函数没有return语句,则返回的对象的value的属性值为undefined。

** 
如果Generator函数没有return 语句,那么Generator最后一次执行,返回的应该是 { value:'undefined',done:'true'}
如果Generator函数有return 语句,那么Generator最后一次执行,返回的应该是 { value:value,done:'true'},这里的value指的是 return语句的值。
**

next方法的参数

yield表达式本身没有返回值,next方法可以带一个参数,该参数就会作为上一个yield表达式的返回值。

作为异步编程解决方案

Generator函数是ES6提供的一种异步编程解决方案,最大的改变,是可以让我们以类似同步的方式编写异步的代码。

为什么

Generator函数可以暂定执行和恢复执行, 这是它能封装异步任务的根本原因。
除此之外,它还有两个特征,使它可以作为异步编程的完美解决方案。

  • 函数体内外的数据传递
  • 错误处理机制

函数体内外数据交换

  1. 向外输出  next方法的返回值的value属性,是Generator函数向外输出数据
  2. next方法会接受参数,向Generator函数体内输入输入数据。
// 声明Generator函数
function* gen(x){
	var y = yield x+2
  return y
}
// 生成器对象
var g = gen()
// 第一次调用next方法
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解决异步读取文件

// 这里要借助Promise
//使用Promise 完成读取文件
const fs = require('fs')
// 实例化一个promise对象\
console.log('fs')
const readFilePromise = function(fileName){
	return new Promise((resolve,reject)=>{
  	fs.readFile(fileName,'utf-8',(err,data)=>{
    	if(err) reject(err)
        resolve(data)
    })
    
  })
}
// 声明 Generator函数
function* gen(){
    const res = yield readFilePromise('./index.html')
    // 请求成功之后 处理数据
    try{
    	res.then(data=>{
        console.log(data)
    	})
    }catch(err){
    	console.log(err)
    }
}

Async/Await

ES2017 引入了async函数,使得异步操作更方便。

概念

async函数是什么,就是Generator函数的语法糖。

和Generator的区别

// 使用Genrator函数 完成文件读取
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/await
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,把yield换成了await/

对Generator的改进

  1. 内置执行器

Generator函数的执行必须依赖co模块。
async函数不需要,调用方式和普通函数一致,后加一个括号即可。调用之后,就会自动执行,输出最后结果。

  1. 更好的语义

async和await,语义更加清楚了。async表示函数内有异步操作。 await 表示紧跟在后面的表达式需要等待结果。

  1. 返回值是Promise

Generator函数的返回值是遍历器对象,async函数的返回值是Promise。

  1. 更广的适用性

co模块规定,yield后只能是Thunk函数,或者Promise对象。
但是await之后可以是Promise和基本类型的值,基本类型的值会自动调用Promise.resolvd()转化为resolved状态的Promise。

基本用法

async函数返回一个Promise对象,可以适用then方法添加回调函数,当函数执行的时候,一旦遇到await就先返回,等到异步操作完成,再接着执行函数体内后面的语句。