web端JavaScript异步浅谈

前言

众所周知,JavaScript是“单线程”(single thread)语言,它跟异步(synchronous)应该是相互矛盾才对?不错,一种语言,要么是单线程,要么是多线程,JS在创造之初被确定为单线程,这个是无法更改的,但是它的宿主环境比如浏览器(Chrome、Firefox、IE)、Node是多线程,我们谈到的异步实际上也是宿主环境通过某种方式(比如Event Loop)调用js,所以使其看起来具有了异步的属性。

同步和异步

下面是当有ABC三个任务需要执行完成时,同步或异步执行的流程图:

同步

thread ->|----A-----||-----B-----------||-------C------|

异步

        A-Start ---------------------------------------- A-End   
           | B-Start ----------------------------------------|--- B-End   
           |   |     C-Start -------------------- C-End      |     |   
           V   V       V                           V         V     V      
  thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--| 

同步(synchronous)和异步asynchronous)的差别就在于这条流水线上各个流程的执行顺序不同

同步任何是串行的执行各个任务,一个任何执行完成时才能执行下一个任务,如果有个很耗时的操作,比如I/O,其他任任务需要等到I/O操作执行完成才能执行接下来的任务并且在这期间不能干任何事情;异步可以“并行”的执行任务,不必等到某个任务执行完成,可以暂缓当前任务,等到某个指令后可以继续进行当前任务;

划重点:JavaScript在设计之初是为浏览器设计的GUI编程语言,GUI编程的特性之一就是保证UI线程一定不能阻塞,否则体验不佳甚至出现白板。

谈那我们日常中使用的异步是怎么回事,下面我们看下浏览器当中是如何做的

浏览器异步

这当中的核心是Event Loop(事件循环)和Task Queue(任务队列)
任务队列:
任务队列实际上就是事件队列,一个事件进入队列方式大致有三种:
1. 用户在页面进行的一些交互行为,比如鼠标点击、页面滚动等等,只要指定回调函数;
2. I/O设备进行一项任务时,此时也会把任务放进队列当中,比如FileReader、XMLHttpRequest等等
3.我们通过通过setTimeout、setInterval、requestAnimationFrame
任务队列是一个先进先出的数据结构,排在前面的事件会优先被主线程来读取,只要当前的执行栈为空,任务队列的任务就会进入到执行栈中,被主线程来调度运行,这里面有一类特殊任务-定时器任务,它需要主线程去根据当前的执行时间去判断是否到了或者超过(由于执行栈执行结束后才会去查看,所以如果执行栈消耗时间比较长时,实际上执行会超过给定的事件才回去执行)指定时间,才会去执行该任务。
备注:需要注意alert和同步的XMLHttpRequest,这些会阻塞主线程。

事件循环:
由于主线程从人物队列中读取事件的过程是循环不断的,所以整个的运行机制又叫做Event Loop。


web端异步常用方式

1. XMLHttpRequest(LV1和LV2)

XMLHttpRequest是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。尽管名字里有 XML,但 XMLHttpRequest 可以取回所有类型的数据资源,并不局限于 XML。而且除了 HTTP ,它还支持 file 和 ftp 协议。在LV2,支持了设置一些新的特性,比如:支持HTTP时限设置防止请求链接时间过长、文件上传、FormData、上传文件、CORS、对二进制数据的获取以及对进度的支持。
简单的用法实例:
var xhr = new XMLHttpRequest();
xhr.open('GET', 'example.php');
xhr.send();
xhr.onreadystatechange = function(){
    if ( xhr.readyState == 4 && xhr.status == 200 ) {
        alert('Success');
    } else {
        alert('Other');
    }
};
// 进度信息
// 下载进度
xhr.onprogress = progressHandler;
// 上传进度
xhr.upload.onprogress = progressHandler;

2. setTimeout、setInterval、requestAnimationFrame

setTimeOut/clearTimeOut、setInterval/clearInterval和requestAnimationFrame/cancelAnimationFrame都属于JavaScriptTimers函数,前两个大家都比较熟悉,基本上可以满足大家大部分场景,但是我们前面提到了在调用栈完成后才会对该方法进行调用,有些时序性比较高的场景有可能会无法满足需要,因此可能会需要requestAnimationFrame,该方法告诉浏览器在下一次屏幕重绘之前调用参数中的callback函数。
简单的用法实例:
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';

function step(timestamp) {
  if (!start) start = timestamp;
  var progress = timestamp - start;
  element.style.left = Math.min(progress / 10, 200) + 'px';
  if (progress < 2000) {
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

3. jQuery.ajax、Fetch、Promise、Deferred

jQuery.ajax是对XMLHttpRequest的封装实现方式,Fetch对XMLHttpRequest的一种更好的替代方式,这里着重提一下两者的差异性:
  • 当接收到一个代表错误的 HTTP 状态码时,从 fetch()返回的 Promise 不会被标记为 reject, 即使该 HTTP 响应的状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
  • 默认情况下,fetch 不会从服务端发送或接收任何 cookies, 如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项)
Fetch的简单的用法示例:
fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象

Promise有以下几种状态:

  • pending: 初始状态,既不是成功,也不是失败状态
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。
从语义上理解,Deferred为延迟对象,Deferred主要用于内部,用于维护异步模型的状态;Promise则作用于外部,通过then()方法暴露给外部以添加自定义逻辑。
贴张图说明一下:

Deferred通过对调用自身的resolve或reject来变更自身的状态为执行状态或者是拒绝状态,并触发then方法的onFulfiled活onRejected方法。
给一段Deferred的简单实现:
function Deferred() {
    /* 状态:默认 等待态 pending */
    this.state = 'pending';
    this.promise = new Promise()
}

Deferred.prototype.resolve = function (obj) {
    this.state = 'fulfilled'
    var handler = this.promise.handler
    if (handler && handler.resolve) {
        handler.resolve(obj)
    }
}

Deferred.prototype.reject = function (obj) {
    this.state = 'rejected'
    var handler = this.promise.handler
    if (handler && handler.reject) {
        handler.reject(obj)
    }
}

4. Generator、async/await
Generator函数是协程(coroutine,多个线程互相协作,完成异步任务)在ES6的实现,它最大的特点是可以将函数的控制权交到外部,由外部逻辑来进行函数内线程的暂停和执行;
async/await是ES7标准引入的,它实际上是Generator函数的语法糖,也是在日常项目中比较推崇的写法,它主要是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。另外,需要注意的是Generator返回的是Iterator,async函数返回的Promise对象。
一段简单的代码示例:
const getData = function () {
  return new Promise(function (resolve, reject) {
    // TODO something
  });
};

const genFun = function* () {
  const f1 = yield readFile('url1');
  const f2 = yield readFile('url2');
  console.log(f1.toString());
  console.log(f2.toString());
};
// 或者是
genFun().next()
genFun().next()

const asyncFun = async function () {
  const f1 = await readFile('url1');
  const f2 = await readFile('url2');
  console.log(f1.toString());
  console.log(f2.toString());
};
一些参考资料:
https://www.cnblogs.com/etoah/p/5863475.html
https://www.cnblogs.com/woodyblog/p/6061671.html
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
http://es6.ruanyifeng.com/#docs/generator-async
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值