js异步处理(一)——理解异步

霸气de小男生 提交于 2020-03-03 00:14:50

简书地址:http://www.jianshu.com/p/f4abe8c4fc2f
我对异步的好奇心起于学习Promise时老是一知半解,最近在看《你所不知道的js(中)》,书中对异步这部分的讲解还是很到位的,所以结合自己的理解整理一下相关知识点。
本文将从是什么、为什么、怎么样这三步式来讲这个问题
一、什么是异步?
  我们一般喜欢把异步和同步、并行拿出来比较,我以前的理解总是很模糊,总是生硬地记着“同步就是排队执行,异步就是一起执行”,现在一看,当初简直就是傻,所以我们第一步先把这三个概念搞清楚,我不太喜欢看网上有些博客里很含糊地说“xxxx是同步,xxxx是异步”,还有举什么通俗的例子,其实对不懂的人来说还是懵逼。
  首先我们要知道这一切的根源都是“Javascript是单线程”,也就是一次只能做一件事,那么为什么是单线程呢?因为js渲染在浏览器上,包含了许多与用户的交互,如果是多线程,那么试想一个场景:一个线程在某个DOM上添加内容,而另一个线程删除这个DOM,那么浏览器要如何反应呢?这就乱套了。
  单线程下所有的任务都是需要排队的,而这些任务分为两种:同步任务和异步任务,同步任务就是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。所以说同步执行其实就是一种只有主线程的异步执行。这里有一个视频关于异步操作是如何被执行的,讲得非常好《what the hack is event loop》,我给大家画个图再来理解一下
最后再来说下并行,并行是关于能够同时发生的事情,是一种多线程的运行机制,而不管同步异步都是单线程的
二、为什么要用异步操作
  这个很好理解,同步下前一个事件执行完了才能执行后一个事件,那么要是遇到Ajax请求这种耗时很长的,那页面在这段时间就没法操作了,更有甚者,万一这个请求由于某种原因一直没有完成,那页面就block了,很不友好。
三、如何实现异步
  我们可以通过回调函数、Promise、生成器、Async/Await等来实现异步。
  今天我们先说最基础的回调函数处理方法来实现,列举几个大家熟悉的使用场景,比如:ajax请求、IO操作、定时器。
   ajax(url, function(){
      //这就是回调函数
   });
回调本身是比较好用的,但是随着Javascript越来越成熟,对于异步编程领域的发展,回调已经不够用了,体现在以下几点:
1、大脑处理程序是顺序的,对于复杂的回调函数会不易理解,我们需要一种更同步、更顺序的方式来表达异步。
举例说明:
//回调函数实现两数相加
function add(getX,  getY, cb){
  var x, y;
  getX(function(xVal){
    x=xVal;
    if(y!=undefined){
      cb(x+y);
    }
  });
  getY(function(){
    y=yVal;
    if(x!=undefined){
      cb(x+y);
    }
  });
}
add(fetchX, fetchY, function(sum){
  console.log(sum);
})

//Promise实现两数相加
function add(xPromise, yPromise){
  return Promise.all([xPromise, yPromise])
  .then(function(values){
    return value[0] + value[1];
  });
}
//fetchX()、fetchY()返回相应值的Promise
add(fetchX(), fetchY())
  .then(function(sum){
    console.log(sum);
  })

只看结构是不是Promise的写法更顺序话一些。

 
2、回调一般会把控制权交给第三方,从而带来信任问题,比如:
  • 调用回调过早
  • 调用回调过晚(或未调用)
  • 调用回调次数过多或过少
  • 未能传递所需的环境和参数
  • 吞掉可能出现的错误和异常
而Promise的特性就有效地解决了这些问题,它是如何解决的呢?

调用回调过早

这种顾虑主要是代码是否会引入类Zalgo效应,也就是一个任务有时会同步完地成,而有时会异步地完成,这将导致竟合状态。
 
Promise被定义为不能受这种顾虑的影响,因为即便是立即完成的Promise(比如 new Promise(function(resolve){ resolve(42); }))也不可能被同步地 监听。也就是说,但你在Promise上调用then(..)的时候,即便这个Promise已经被解析了,你给then(..)提供的回调也将总是被异步地调用。

调用回调过晚

当一个Promise被调用时,这个Promise 上的then注册的回调函数都会在下一个异步时机点上,按顺序地,被立即调用。这些回调中的任意一个都无法影响或延误对其它回调的调用。
 
举例说明:
p.then( function(){
    p.then( function(){
        console.log( "C" );
    } );
    console.log( "A" );
} );
p.then( function(){
    console.log( "B" );
} );
// A B C
 
为什么“C”没有排到“B”的前面?因为因为“C”所处的.then回调函数是在下一个事件循环tick。

回调未调用

这是一个很常见的顾虑。Promise用几种方式解决它。
首先,当Promise被解析后,在代码不出错的情况下它一定会告知你解析结果。如果代码有错误,归类于后面的“吞掉错误或异常”中。
那如果Promise本身不管怎样永远没有被解析呢?那么Promise会用Promise.race来解决。
 
看代码示例:
// 一个使Promise超时的工具
function timeoutPromise(delay) {
    return new Promise( function(resolve,reject){
        setTimeout( function(){
            reject( "Timeout!" );
        }, delay );
    } );
}

// 为`foo()`设置一个超时
Promise.race( [
    foo(),                    // 尝试调用`foo()`
    timeoutPromise( 3000 )    // 给它3秒钟
] )
.then(
    function(){
        // `foo(..)`及时地完成了!
    },
    function(err){
        // `foo()`不是被拒绝了,就是它没有及时完成
        // 那么可以考察`err`来知道是哪种情况
    }
);

调用次数过少或过多

正常是调用一次,“过少”就是未被调用,参考上文;“过多”的情况也很容易理解。Promise的定义方式使得它只能被决议一次,如果出于某种情况决议了多次,Promise也只会接受第一次决议,并忽略后续调用。

未能传递所需的参数/环境值

Promise只会有一个解析结果(完成或拒绝)。如果没有用一个值明确地解析它,它的值就是undefined,就像JS中常见的那样。

吞掉错误或异常

Promise中异常会被捕获,并且使这个Promise被拒绝。
 
举个例子:
var p = new Promise( function(resolve,reject){
    foo.bar();    // `foo`没有定义,所以这是一个错误!
    resolve( 42 );    // 永远不会跑到这里 :(
} );

p.then(
    function fulfilled(){
        // 永远不会跑到这里 :(
    },
    function rejected(err){
        // `err`将是一个来自`foo.bar()`那一行的`TypeError`异常对象
    }
);

 

Promise就先说到这里,关于PromiseAPI及其源码还有生成器、Async/Await 在后续文章中整理报道。
 
【写得不好的地方请大胆吐槽,非常感谢大家带我进步。】
参考资料:
 
 
 
 
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!