深入理解javascript异步

641 阅读5分钟

异步发展历程

为什么会有异步?

首先我们要简单的了解一下同步和异步的概念

同步:调用一旦开始,调用者必须等到调用方法返回后,才能继续后续的行为。调用者会主动等待调用的结果

异步:当一个异步调用发出后,这个调用就立刻返回了,调用者不会立即得到结果。而是通过某些通知来通知调用者,或者通过回调函数来处理这个调用。

推荐一篇关于同步和异步的文章,感兴趣的同学可以了解一下。

我们来想象一下这样的场景,在你的业务中,需要从服务端去获取http请求信息。这个时候你利用ajax向服务器发了请求。

 $.get(url, function(data) {console.log(data) });

假如这个请求是同步执行的,那么,也就是说你需要等待ajax的响应。但是在网络请求中,请求的速度会有很大的波动,也许对方的服务器会挂掉,这样会导致一直无法得到响应,那么我们后续事情也无法继续。所以为了提高工作效率,我们想要的是,当我们发出请求时,我们只关心响应后对数据的处理,并在等待这段响应的过程中,去执行其他操作。所以,我们需要异步,来提高效率


javascript异步发展经历

那么,我们知道了,在大多数获取资源的操作中,我们需要异步操作。那么javascript异步是怎么一步一步发展的呢?

1.回调函数

我们可以通过回调函数来进行异步操作。

例子:异步读取一个文件

let fs = require('fs') 

fs.readFile('./1.txt','utf8',function(err,data){    
if(err){//如果err有值,就表示程序出错了   
  console.log(err);    
}else{/
/如果error为空,就表示 成功了,没有错误     
 console.log(data);    
} 
})

回调函数有个特点:error first  --> 调用回调函数的时候第一个参数永远是错误对象

有了回调函数似乎问题得到了解决,但是在使用的过程中,我们难免会发现回调函数也有他的不足之处:

  1. 无法错误捕捉 try catch   原因是: 尝试对异步方法进行try/catch操作只能捕获当次事件循环内的异常,对call back执行时抛出的异常将无能为力。也就是说,当你准备捕获异常时,异步错误发生在try catch块结束的时候。
  2. 异步操作不能return  ,实例如下(返回值为undefined):

// 发送ajax请求
$.ajax({
    type: "POST",
    url: "checkName.php",
    data: {
        username: $inputVal
    },
    success: function(responseText) {
        if (responseText === "helloworld") {
                 return false;
        } else {

                return true;
        }

    }
});
// 原因:异步操作,可能无法取到数据就直接返回,所以只等返回undefined。


    3. 如果异步操作过多,会造成回调地狱。

fs.readFile('./template.txt', 'utf8', function (err, template) {
  fs.readFile('./data.txt', 'utf8', function (err, data) {    
    console.log(template + ' ' + data);  
    })
})


2.解决回调嵌套问题

     1.通过事件发布订阅来

    先上代码:

let EventEmitter = require('events');let eventByDep = new EventEmitter();
let html = {};

//监听数据获取成功事件,当事件发生之后调用回调函数

eventByDep.on('ready',function(key,value){ 
     html[key] = value;  
     if(Object.keys(html).length == 3){ 
       //打印出template的值   
      console.log(html); 
       }
});

fs.readFile('./data1.txt', 'utf8', function (err, data1) { 
 //1 事件名 2 参数往后是传递给回调函数的参数  
    eventByDep.emit('ready','data1',data1);
})

fs.readFile('./data2.txt', 'utf8', function (err, data2) {  
    eventByDep.emit('ready','data2',data2);
})

2.通过设置一个哨兵函数来处理

优点:不需要额外引入包

原理:通过调用哨兵函数来监控文件的异步操作,代码如下:

//哨兵函数,用来监听文件异步操作的返回值.
function done(key,value){  html[key] = value; 
 if(Object.keys(html).length == 2){ 
   console.log(html);  
  }
}

fs.readFile('./data1.txt', 'utf8', function (err, data1) { 
    done('data1',data1);
})
fs.readFile('./data2.txt', 'utf8', function (err, data2) { 
 done('data2',data2);
})


//也可以写一个高级函数用来生成哨兵函数:
function render(length,cb){  let html={};  return function(key,value){   
   html[key] = value; 
   if(Object.keys(html).length == length){      
        cb(html);   
     }  
    }
}
let done = render(3,function(html){ 
    console.log(html);
  });


fs.readFile('./data1.txt', 'utf8', function (err, data1) {
     done('data1',data1);
})fs.readFile('./data2.txt', 'utf8', function (err, data2) { 
     done('data2',data2);
})
fs.readFile('./data3.txt', 'utf8', function (err, data3) {  
     done('data3',data3);
})


3.promise的到来

以上的异步处理,都有或多或少的缺点,那么promise的到来,让js的异步处理变得方便,优雅。那么promise是如何实现的呢?

3.1 手动实现一个简单的promise

1. 首先我们创建了三个状态常量分别表示,挂起,失败,成功三种状态。

2. 一开始 Promise 的状态应该是 pending

3.value变量用于保存 resolve 或者 reject 中传入的值

4.resolvedCallbacks 和 rejectedCallbacks 用于保存 then中的回调,因为当执行完 Promise 时状态可能还是等待中,这时候应该把 then中的回调保存起来用于状态改变时使用.

5.规范规定只有等待状态(PENDING)才能改变成其他状态。

6.then方法中有两个函数,分别为失败和成功执行的回调。

// 创建三种状态
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

function Promise(executor){
  const self = this
  self.state = PENDING   //默认为pending态  self.value = null
  self.resaon = reason   //失败的原因
  self.resolvedCallbacks = []  self.rejectedCallbacks = []   
  // 成功态
  function resolve(value){
      if (self.state === PENDING){
         self.state = RESOLVED
         self.value = value 
        //执行回调函数
         self.resolvedCallbacks.forEach(fn=>fn())    
      }
   }
  
  //失败态
  function reject(reason) { 
     if (self.status === 'pending') {    
         self.status = 'rejected';         
         self.reason = reason;        
         self.rejectedCallbacks.forEach(fn=>fn()) 
        }   
    }
   try {
      executor(resolve, reject)}catche(e){  
       // 捕获的时候发生异常,就直接失败了
      reject(e);
   }}


//实现promise.then方法
Promise.prototype.then = function(onFulfilled, onRejected) { 
   const self = this
  //判断onFulfilled,onRejected是否是函数类型,不是则构造函数,并赋值给对应的参数,并实现透传。 
   onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
   onRejected =
    typeof onRejected === 'function'
      ? onRejected
      : r => {
          throw r
        }
   if (self.state === PENDING) {   
     self.resolvedCallbacks.push(onFulfilled)  
     self.rejectedCallbacks.push(onRejected)  }
   if (self.state === RESOLVED) {    
     onFulfilled(self.value) 
  }
  if (that.state === REJECTED) {
     onRejected(self.value)  }
}


//使用
new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 0)
}).then(value => {
  console.log(value)
})


以上只是一个简易版本的promise。

那么promise边解决了异步回调的回调地狱等问题.


4.利用Generator + promise来实现异步

首先我们来认识一下Generator

  1. 我们用*来标识一个Generator函数。
  2. 每次执行都会在yield停止。调用一次next就会继续向下执行。
  3. 返回结果是一个迭代器 迭代器有一个next方法
  4. yield后面跟着的是value的值
  5. yield等号前面的是我们当前调用next传进来的值
  6. 第一次next传值是无效的.

代码实例如下:

function* read() { 
   console.log(1);   
   let a = yield 'hello';    
   console.log(a);    
   let b = yield 9    
   console.log(b);    
   return b;
}
let it = read();
console.log(it.next('')); // {value:'hello',done:false}
console.log(it.next('100')); // {value:9,done:false}
console.log(it.next('200')); // {value:200,done:true
}console.log(it.next('200')); // {value:undefined,done:true}

一般利用Generator + Promise来实现异步:

let bluebird = require('bluebird');let fs = require('fs');

//将文件的读取操作Promise化
  let read = bluebird.promisify(fs.readFile);
  function* r() {    
    let content1 = yield read('./1.txt', 'utf8'); 
    let content2 = yield read(content1, 'utf8'); 
    return content2;
}

//利用next来执行异步
let g = r();
g.next().value.then(function (data) {
  g.next(data).value.then(function (data) {
    g.next(data)
  })
})

接下来我们使用co库(自己实现) 可以自动的将generator进行迭代

function(it){
    return new Promise(function(resolve,reject){
            function next(data){
                let {value,done} = it.next(data);
                if (!done){
                      value.then(function(data){
                        //将值往下传递
                        next(data)
                  },reject)
                }else{
                    resolve(value)           
                }
              }
              next();        
    })
}

//使用
co(r()).then(function (data) {   
   console.log(data)
})


5.async + await

语法如下:

// 只能在async函数内部使用
let value = await promise

关键词await可以让JavaScript进行等待,直到一个promise执行并返回它的结果,JavaScript才会继续往下执行。

也就是说async + await 相当于 co + generator   他实现了一个语法糖.

实例如下:

let bluebird = require('bluebird');
let fs = require('fs');
let read = bluebird.promisify(fs.readFile);
async function r(){ 
  try{     
    let content1 = await read('./text','utf8');
    let content2 = await read(content1,'utf8');       
    return content2;   
 }catch(e){ 
// 如果出错会catch        
  onsole.log('err',e)    
 }
}


// async函数返回的是promise,
r().then(function(data){   
 console.log('flag',data);
},
   function(err){   
    console.log(err);
})

那么async+await有哪些好处呢?

  1. 解决了回调地狱
  2. 并发执行异步,在同一时刻同步返回结果。
  3. 解决了返回值的问题
  4. 可以实现代码的try/catch


以上便是我总结的javascript的发展历程,不足之处,还请各位大佬多多指正。谢谢!