异步操作概述

1.单线程模型

  • JS的核心特点就是单线程。
  • JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。
  • 原因:1.不想让浏览器太复杂。多线程要共享资源、且有可能改变彼此的运行结果。
  • 优点:实现简单,执行环境相对单纯。
  • 缺点:任务耗时很长;常见的服务器无响应(假死),就有很大可能是因为某段JS代码进入了死循环,或者等待Ajax请求返回结果很慢,导致页面卡住。(并不是因为CPU忙不过来,而是由于IO操作比较慢)
  • 解决方法:CPU将处于等待中的任务挂起,先运行排在后面的任务。也就是JS内部的事件循环机制(EVENT LOOP)
  • 为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

2.同步任务和异步任务

  • 程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。
  • 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
  • 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。
  • Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。

3.任务队列和事件循环

  • JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
  • 异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。
  • JavaScript 引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。

4.异步操作的模式

4.1 回调函数

  • 回调函数是异步操作最基本的方法。
  • 优点是简单、容易理解和实现,
  • 缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
  • 一个基础的回调函数的例子:可以实现先执行f1,结束后再执行f2
function f1(callback) {
  // ...
  callback();
}

function f2() {
  // ...
}

f1(f2);

(确实会有各种嵌套的话就比较复杂了)

4.2 事件监听

  • 采取事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
  • 优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(decoupling),有利于实现模块化。
  • 缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
  • setTimeout之类的。
  • 也可以用jquery

4.3 发布/订阅

  • 事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。
  • 这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
  • 例子:
jQuery.subscribe('done', f2); //f2订阅

function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done'); //推送
  }, 1000);
}

jQuery.unsubscribe('done', f2);//完成后,f2可以取消订阅

5. 异步操作的流程控制

  • 如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。
  • 如果直接一个个嵌套,麻烦且容易出错,难以维护。
  • 串行执行:编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。
  • 并行执行:所有异步任务同时执行,等到全部完成以后,才执行final函数。效率高。但如果并行的任务较多,就很容易耗尽系统资源,拖慢运行速度。
  • 并行与串行的结合:设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。

(不过,大概知道要这么选,但是写不来……

定时器

  • 定时执行代码的功能,叫做定时器(timer),主要由setTimeout()setInterval()

1.setTimeout()

  • 用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
var timerId = setTimeout(func|code, delay);
  • 接受多个参数,第一个参数func|code是将要推迟执行的函数名或者一段代码,第二个参数delay是推迟执行的毫秒数。
  • 后续的参数将依次传入推迟执行的函数(回调函数)【也即推迟执行的函数的参数】。
  • 注意:如果回调函数是对象的方法,那么setTimeout使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象。【好像就是这样吧,前面提到this的时候就是,容易指向全局环境的说。】
    解决方法1.是将需要运行的函数放在一个匿名函数里2.是使用Bind方法绑定想要的环境。(其实像是call,apply应该也都可以吧)

2.setInterval()

  • 函数的用法与setTimeout完全一致,
  • 区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行
  • 常见用途:实现轮询
  • setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。
  • 为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间。

3.clearTimeout(),clearInterval()

  • setTimeoutsetInterval函数返回的一个整数值(连续的。表示计数器编号)传入clearTimeoutclearInterval函数,就可以取消对应的定时器。
  • 可以写一个函数,取消当前所有的setTimeout定时器
  • 实现方法: 先调用setTimeout,得到一个计算器编号,然后把编号比它小的计数器全部取消。

4.实例:debounce(防抖动) 函数

  • 防止回调函数被频繁调用。
  • 用防抖动函数保证回调函数之间的调用间隔。

5. 运行机制

  • 将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。
  • 这意味着,setTimeoutsetInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。
  • 由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证setTimeoutsetInterval指定的任务,一定会按照预定时间执行。

6.setTimeout(f, 0)

  • setTimeout(f, 0)会在下一轮事件循环一开始就执行。因为setTimeout就已经将代码移到下一轮去了。
  • 这种写法是为了尽可能早的执行f,而不是立即执行。
  • 用途:1、调整事件的发生顺序 。用这个函数可以移动某些函数到后一轮去。

大概原理理解了,但是也是,不会用= =

Promise 对象

  • Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。
  • 它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。
  • Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
    (ES6里有更详细介绍)
  • Promise 是一个对象,也是一个构造函数。
  • 设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数。
var p1 = new Promise(f1);
p1.then(f2);

f1的异步操作执行完成,就会执行f2。

  • Promise 使得f1和f2变成了链式写法。

lua 异步操作 是什么意思_前端

Promise 对象的状态

  • Promise 对象通过自身的状态,来控制异步操作。
  • Promise 实例具有三种状态。
    异步操作未完成(pending)
    异步操作成功(fulfilled)
    异步操作失败(rejected)
    上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。

这三种的状态的变化途径只有两种。

从“未完成”到“成功”
从“未完成”到“失败”

  • 一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次
  • 因此,Promise 的最终结果只有两种。
    异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled。
    异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected。

Promise 构造函数

  • JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例
var promise = new Promise(function (resolve, reject) {
  // ...

  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
});

上面代码中,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。

resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

  • Promise 的回调函数不是正常的异步任务,而是微任务(microtask);正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。
  • 这意味着,微任务的执行时间一定早于正常任务。
    (是不是指异步任务分为正常任务和微任务两种呢)

Promise.prototype.then()

  • Promise 实例的then方法,用来添加回调函数。
  • then方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
  • then方法可以链式使用。
  • Promise 对象的报错具有传递性。
  • then的回调函数属于异步任务,一定晚于同步任务执行。
setTimeout(function() {
  console.log(1);
}, 0);

new Promise(function (resolve, reject) {
  resolve(2);
}).then(console.log);

console.log(3);
// 3
// 2
// 1

上面代码的输出结果是321。
这说明then的回调函数的执行时间,早于setTimeout(fn, 0)。
因为then是本轮事件循环执行,setTimeout(fn, 0)在下一轮事件循环开始时执行。

then() 用法辨析

确实不太会辨别= =

// 写法一
f1().then(function () {
  return f2();
}).then(f3);
f3回调函数的参数,是f2函数的运行结果。

// 写法二
f1().then(function () {
  f2();
  return;
}).then(f3);
f3回调函数的参数是undefined。

// 写法三
f1().then(f2())
  .then(f3);
f3回调函数的参数,是f2函数返回的函数的运行结果。

// 写法四
f1().then(f2)
  .then(f3);
写法四与写法一只有一个差别,那就是f2会接收到f1()返回的结果。