深入javascript计划六:深入浅出异步

什么是进程?

进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。

它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

通俗来讲就是:一个进程就是一个程序的运行实例(详细解释就是,启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程)。

进程是系统资源分配的独立实体,每个进程都拥有独立的地址空间。

一个进程无法访问另一个进程的变量和数据结构。

如果想让一个进程访问另一个进程的资源,需要使用进程间通信(IPC)。

IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中 Socket和Streams支持不同主机上的两个进程IPC。

IPC通信介绍

什么是线程?

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

通俗来讲就是:线程是进程的内部的一个执行序列

一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。

单线程,多线程概念

例子:

A = 1 + 2
B = 20 / 5
C = 7 * 8

拆分成4个任务

  • 任务 1 是计算 A=1+2
  • 任务 2 是计算 B=20/5
  • 任务 3 是计算 C=7*8
  • 任务 4 是显示最后计算的结果

单线程是如何执行任务的?

单线程会按照顺序分别执行这四个任务。

多线程是如何执行任务的?

多线程会分两步:

1.使用三个线程同时执行前面三个任务

2.等待第一步全部执行完毕再执行第四个显示任务

从图中可以看到,线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。

这时候就要讲讲线程安全了

简单测试你的线程是否安全:如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的多线程就是安全的。

如果不一致,那就可能是多线程的资源安全有问题,常见的情况如下:

1.临界资源问题(多个线程同时访问相同的资源并进行读写操作)

解决思路:互斥锁(防止多个线程同时读写某一块内存区域)

2.死锁(两个线程相互等待对方释放对象锁)

解决思路:

  • 加锁顺序:线程按照一定的顺序加锁
  • 加锁时限:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁
  • 死锁检测

好了就介绍到这里,想了解更多

面试:史上最全多线程面试题 - (锁&内存模型&线程)

浅谈linux线程模型和线程切换

进程切换与线程切换的区别?

总结进程和线程

1.进程中的任意一线程执行出错,都会导致整个进程的崩溃。

A = 1+2
B = 20/0
C = 7*8

我把上述三个表达式稍作修改,在计算 B 的值的时候,我把表达式的分母改成 0,当线程执行到 B = 20/0 时,由于分母为 0,线程会执行出错,这样就会导致整个进程的崩溃,当然另外两个线程执行的结果也没有了。

2. 线程与线程之间共享进程中的数据。

如下图所示,线程之间可以对进程的公共数据进行读写操作。

从上图可以看出,线程 1、线程 2、线程 3 分别把执行的结果写入 A、B、C 中,然后线程 2 继续从 A、B、C 中读取数据,用来显示执行结果。

3. 当一个进程关闭之后,操作系统会回收进程所占用的内存。

当一个进程退出时,操作系统会回收该进程所申请的所有资源(即使其中任意线程因为操作不当导致内存泄漏,当进程退出时,这些内存也会被正确回收)。

比如之前的 IE 浏览器,支持很多插件,而这些插件很容易导致内存泄漏,这意味着只要浏览器开着,内存占用就有可能会越来越多,但是当关闭浏览器进程时,这些内存就都会被系统回收掉。

4. 进程与进程之间的内容相互隔离。

进程隔离是为保护操作系统中进程互不干扰的技术,每一个进程只能访问自己占有的数据,也就避免出现进程 A 写入数据到进程 B 的情况。正是因为进程之间的数据是严格隔离的,所以一个进程如果崩溃了,或者挂起了,是不会影响到其他进程的。

如果进程与进程之间需要进行数据的通信,这时候,就需要使用用于进程间通信(IPC)的机制了。

白话总结:

进程是运行中的程序,线程是进程的内部的一个执行序列

进程是资源分配的单元,线程是执行行单元

进程间切换代价大,线程间切换代价小

进程拥有资源多,线程拥有资源少

多个线程共享进程的资源

如果还不懂可以在看看   进程与线程的一个简单解释

什么是协程?

协程,英文名是 Coroutine, 又称为微线程,是一种比线程更加轻量级的存在。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

协程不像线程和进程那样,需要进行系统内核上的上下文切换,协程的上下文切换是由程序所控制。

这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

想了解更多?什么是协程

什么是并发?

并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。

并发:是指两个或多个事件可以在同一个时间间隔发生

通俗来讲就是:并发就是A先走了1秒B马上跟上

什么是并行?

并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

并行:是指两个或多个事件可以在同一个时刻发生

通俗来讲就是:并行就是A、B两个人并排走路

异步与同步

异步:不等任务执行完,直接执行下一个任务。

同步:一定要等任务执行完了,得到结果,才执行下一个任务。

阻塞与非阻塞

所谓阻塞,指的是阻碍堵塞。它的本意可以理解为由于遇到了障碍而造成的动弹不得。

所谓非阻塞,自然是和阻塞相对,可以理解为由于没有遇到障碍而继续畅通无阻。

与同步、异步结合

所谓同步/异步,关注的是能不能同时开工。
所谓阻塞/非阻塞,关注的是能不能动。
通过推理进行组合:
同步阻塞,不能同时开工,也不能动。只有一条小道,一次只能过一辆车,可悲的是还TMD的堵上了。
同步非阻塞,不能同时开工,但可以动。只有一条小道,一次只能过一辆车,幸运的是可以正常通行。
异步阻塞,可以同时开工,但不可以动。有多条路,每条路都可以跑车,可气的是全都TMD的堵上了。
异步非阻塞,可以工时开工,也可以动。有多条路,每条路都可以跑车,很爽的是全都可以正常通行。

回到程序里,把它们和线程关联起来

同步阻塞,相当于一个线程在等待。
同步非阻塞,相当于一个线程在正常运行。
异步阻塞,相当于多个线程都在等待。
异步非阻塞,相当于多个线程都在正常运行。

想了解更多?

【面试】迄今为止把同步/异步/阻塞/非阻塞/BIO/NIO/AIO讲的这么清楚的好文章(快快珍藏)

上面说了这么多,就说一个白话版的

打群架

我(CPU)喊了3个人和我一起去打群架(这个 3个人的团伙  就是进程),进程好比是一个上下文,3个人之间 数据共享。

真正打的是谁?是里面的人( 也就是线程),所以线程是最基本的执行单元。

譬如3个人的团伙(这就是进程) 去打架。  对方也是3个人,那么就是1对1干了(这就是多线程操作)。

其实性能很低,完全可以先排一个人(线程)上去, 把对方打趴下后,立刻去打第二个人。 等第一个人站起来了再回过来打。
这种调度机制 ,就是协程(其实还是线程,只不过我加了技巧-调度机制)。

当一个线程(一个打手)打着打着扭在一起了, 那么这叫做阻塞。
我作为头领,可以立刻把这个打手,拉开。让他先去打弱一点的人(这叫做线程切换)。

线程和协程的区别在于:

协程是线程的一个打架技巧(调度策略),真正在干活的 永远是线程(系统根本不知道有协程的存在)。 

并发: 一个人上打A一拳,A还没缓过来,又打B一拳,B懵逼了,又打了C一拳。
看起来 仿佛在同一个时间点我打了三个人(这就是并发)。

并行: 3个人  同时出拳  打ABC ,ABC三个人同时受到一拳(这就是并行)。

同步:一个人打A,必须把A打死,才能打B 。

异步:一个人打A,A只要躺地上, 我就立刻去打B ,等A站起来,我再回头打A。

阻塞:我带3个小弟打架,我就是主线程,我打A ,发现打不过(此时就叫做阻塞)。

如果我硬着头皮打那么所有事就得等待,这叫做同步阻塞。

异步很简单,我打A打不过,我让我小弟去打A,我继续打B,小弟就是处理异步的另外一个线程,而我作为主线程是不能阻塞的,因为要打A打B打C这个过程是预先设定好的(代码里写了),小弟打A,如果打过了,我回头再来扇A一个巴掌  ,小弟打A没打过,我回头过来 向A认错(catch 了)。

好了回到javascript话题

回调函数(Callback)

回调函数应该是大家经常使用到的,以下代码就是一个回调函数的例子:

function ajax(url, succ, fail) {
    if (url === "/test") {
        if (typeof succ === "function") {
            return succ("succ")
        }
    }
    if (typeof fail === "function") {
        return fail("fail")
    }
}

ajax("/test", (res)=> {
    console.log(res) // succ
})

但是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出如下代码:

function ajax(url, succ, fail) {
    if (url === "/test") {
        if (typeof succ === "function") {
            return succ("succ")
        }
    }
    if (typeof fail === "function") {
        return fail("fail")
    }
}

ajax("/test", (res)=> {
    console.log(res) // succ
    ajax("/test", (res)=> {
        console.log(res) // succ
        ajax("/test", (res)=> {
            console.log(res) // succ
        })
    })
})

以上代码看起来不利于阅读和维护,当然,你可能会想说解决这个问题还不简单,把函数分开来写不就得了。

function ajax(url, succ, fail) {
    if (url === "/test") {
        if (typeof succ === "function") {
            return succ("succ")
        }
    }
    if (typeof fail === "function") {
        return fail("fail")
    }
}

function firstAjax() {
    ajax("/test", (res)=> {
        console.log(res) // succ
        secondAjax()
    })
}
function secondAjax() {
    ajax("/test", (res)=> {
        console.log(res) // succ
    })
}
ajax("/test", (res)=> {
    console.log(res) // succ
    firstAjax()
})

以上的代码虽然看上去利于阅读了,但是还是没有解决根本问题。

回调地狱的根本问题就是:

  1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  2. 嵌套函数一多,就很难处理错误

当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。

Promise

Promise 是 ES6 新增的语法,一定程度解决了回调地狱的问题。

Promise 翻译过来就是承诺的意思,这个承诺会在未来有一个确切的答复,并且该承诺有三种状态,分别是:

 

  1. 等待中(pending)
  2. 完成了 (resolved)
  3. 拒绝了(rejected)

这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变

let promise = new Promise((resolve, reject) => {
    resolve('succ')
    // 无效
    reject('reject')
})
promise.then((res) => {
    console.log(res) // succ
})

  当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的

let promise = new Promise((resolve, reject) => {
    console.log('new Promise')
    resolve('succ')
})
promise.then((res) => {
    console.log(res)
})
// new Promise
// succ

Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装

Promise.resolve(1).then((res) => {
    console.log(res)
    return 2 // 包装成 Promise.resolve(2)
}).then(res => {
    console.log(res)
})
// 1
// 2

前面都是在讲述 Promise 的一些优点和特点,其实它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。

Promise 凭借什么消灭了回调地狱?

什么是回调地狱

  1. 多层嵌套的问题。
  2. 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性。

这两种问题在回调函数时代尤为突出。Promise 的诞生就是为了解决这两个问题。

解决方法

Promise 利用了三大技术手段来解决回调地狱:

  • 回调函数延迟绑定
  • 返回值穿透
  • 错误冒泡

回调函数延迟绑定

function ajax(url) {
    return new Promise((resolve, reject) => {
        if (url === "/test") {
            resolve("succ")
        } else {
            reject("fail")
        }
    })
}
ajax("/test").then((res) => {
    console.log(res) // succ
})

看到没有,回调函数不是直接声明的,而是在通过后面的 then 方法传入的,即延迟传入。这就是回调函数延迟绑定。通俗来讲就是promise有resolved、rejected这两种延迟绑定。

返回值穿透

function ajax(url) {
    return new Promise((resolve, reject) => {
        if (url === "/test") {
            resolve("succ")
        } else {
            reject("fail")
        }
    })
}

let x = ajax("/test").then((res) => {
    console.log(res) // succ
    return ajax("/test")
})
x.then(res => {
    console.log(res) // succ
})

我们会根据 then 中回调函数的传入值创建不同类型的Promise,然后把返回的 Promise 穿透到外层,以供后续的调用。这里的 x 指的就是内部返回的 Promise,然后在 x 后面可以依次完成链式调用。

这便是返回值穿透的效果

这两种技术一起作用便可以将深层的嵌套回调写成下面的形式:

function ajax(url) {
    return new Promise((resolve, reject) => {
        if (url === "/test") {
            resolve("succ")
        } else {
            reject("fail")
        }
    })
}

ajax("/test").then((res) => {
    console.log(res) // succ
    return ajax("/test")
}).then(res => {
    console.log(res) // succ
    return ajax("/test")
}).then(res => {
    console.log(res) // succ
    return ajax("/test")
}).then(res => {
    console.log(res)
})

错误冒泡

function ajax(url) {
    return new Promise((resolve, reject) => {
        if (url === "/test") {
            resolve("succ")
        } else {
            reject("fail")
        }
    })
}

ajax("/test").then((res) => {
    console.log(res)
    return ajax("/test")
}).then(res => {
    console.log(res)
    return ajax("/test")
}).then(res => {
    console.log(res)
    return ajax("/test1")
}).then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

这样前面产生的错误会一直向后传递,被 catch 接收到,就不用频繁地检查错误了。

Promise 手写实现

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回调,只有当 promise
  // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是个 Promise,递归执行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解决以下问题
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 规范 2.2.7,then 必须返回一个新的 promise
  var promise2;
  // 规范 2.2.onResolved 和 onRejected 都为可选参数
  // 如果类型不是函数需要忽略,同时也实现了透传
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 异步执行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考虑到可能会有报错,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 规范 2.3.2
  // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次调用该函数是为了确认 x resolve 的
        // 参数是什么类型,如果是基本类型就再次 resolve
        // 把值传给下个 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 规范 2.3.3.3.3
  // reject 或者 resolve 其中一个执行过得话,忽略其他的
  let called = false;
  // 规范 2.3.3,判断 x 是否为对象或者函数
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 规范 2.3.3.1
      let then = x.then;
      // 如果 then 是函数,调用 x.then
      if (typeof then === "function") {
        // 规范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 规范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 规范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
  }
}

Generator

生成器(Generator)是 ES6 中的新语法,最大的特点就是可以控制函数的执行,相对于之前的异步语法,上手的难度还是比较大的。因此这里我们先来好好熟悉一下 Generator 语法。

  1. 使用 * 表示这是一个 Generator 函数
  2. 内部可以通过 yield 暂停代码
  3. 通过调用 next 恢复执行

简单的例子:

function* foo() {
    yield 'result1'
    yield 'result2'
    yield 'result3'
}

const gen = foo()
console.log(gen.next())
console.log(gen.next())
console.log(gen.next())
console.log(gen.next())
// { value: 'result1', done: false }
// { value: 'result2', done: false }
// { value: 'result3', done: false }
// { value: undefined, done: true }

很好理解每次gen.next(),程序继续执行,直到遇到 yield 程序暂停。

next 方法返回一个对象, 有两个属性: value 和 done。

value 为当前 yield 后面的结果,done 表示是否执行完。

复杂一点的例子:

function *foo(x) {
    let y = 2 * (yield (x + 1))
    let z = yield (y / 3)
    return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}

讲解:

1.当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6。

2.当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined

此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8。

3.当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42。

如果生成器要调用另一个生成器时如何操作?

答案:yield*

看例子:

function* gen1() {
    yield 1;
    yield 4;
}
function* gen2() {
    yield 2;
    yield 3;
}

如果我们想按照1234顺序执行,如何来做?

看例子:

function* gen1() {
    yield 1;
    yield* gen2();
    yield 4;
}
function* gen2() {
    yield 2;
    yield 3;
}

let g1 = gen1()
console.log(g1.next())
console.log(g1.next())
console.log(g1.next())
console.log(g1.next())
console.log(g1.next())

// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// { value: undefined, done: true }

 生成器实现机制——协程

可能你会比较好奇,生成器究竟是如何让函数暂停, 又会如何恢复的呢?这时候就要讲协程了(上文有介绍)。

javascript不是单线程执行嘛?难道可以开多协程一起执行嘛?

答案是:并不能。一个线程一次只能执行一个协程。

比如当前执行 A 协程,另外还有一个 B 协程,如果想要执行 B 的任务,就必须在 A 协程中将JS 线程的控制权转交给 B协程,那么现在 B 执行,A 就相当于处于暂停的状态。

例子:

function* A() {
  console.log("我是A");
  yield B(); // A停住,在这里转交线程执行权给B
  console.log("结束了");
}
function B() {
  console.log("我是B");
  return 100;// 返回,并且将线程执行权还给A
}
let gen = A();
gen.next();
gen.next();

// 我是A
// 我是B
// 结束了

讲解:

在这个过程中,A 将执行权交给 B,也就是 A 启动 B,我们也称 A 是 B 的父协程。因此 B 当中最后return 100其实是将 100 传给了父协程。

Generator 手写实现

// cb 也就是编译过的 test 函数
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 如果你使用 babel 编译后可以发现 test 函数变成了这样
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 可以发现通过 yield 将代码分割成几块
        // 每次执行 next 函数就执行一块代码
        // 并且表明下次需要执行哪块代码
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
		// 执行完毕
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}

async/await

async/await被称为 JS 中异步终极解决方案。

什么是async?

async 它就是 Generator 函数的语法糖

async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

async函数对 Generator 函数的改进,体现在以下四点。

  1. 内置执行器
  2. 更好的语义
  3. 更广的适用性
  4. 返回值是Promise

例子:

async function test() {
  return "1";
}
console.log(test());

async function test() {
  return "1";
}
console.log(test());
test().then(res => {
    console.log(res) // 1
})

await

await 只能在 async 函数中使用(正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果(resolve或者reject的值)。如果不是 Promise 对象,就直接返回对应的值),await 表达式会暂停当前 async function 的执行。

例子:

async function f() {
  // 等同于
  // return 123;
  return await 123;
}

f().then(v => console.log(v))
// 123

上面代码中,await命令的参数是数值123,这时等同于return 123

例子:

async function test() {
    console.log("执行了test方法")
    let a = await 200
    // let promise = new Promise((resolve, reject) => {
    // resolve(200)
    // })
    // let a = promise // 会监听promise变化
    // let a = promise.then((res) => {})
    console.log("执行test方法await-", a)
    console.log("----执行完test方法-----")
}
console.log('打印0')
test()
console.log('打印300')
console.log('打印400')
console.log('打印500')

let p = new Promise((resolve, reject) => {
    console.log("执行了p的promise")
    resolve(800)
})
// 监听p的promise状态变化
p.then(res => {
    console.log("监听到p的promise变化-", res)
})
// 打印0
// 执行了test方法
// 打印300
// 打印400
// 打印500
// 执行了p的promise
// 执行test方法await- 200
// ----执行完test方法-----
// 监听到p的promise变化- 800

讲解:

首先代码按顺序执行,先执行(console.log('打印0')),然后将test()压入执行栈,执行(console.log("执行了test方法")),下面遇到关键字await:

1.await内部实现了generators,generators会保留堆栈中东西

2.await是异步操作,遇到await就会立即返回一个pending状态的Promise对象,暂时返回执行代码的控制权

await 200

被js引擎转换成一个promise: 

let promise = new Promise((resolve,reject) => {
   resolve(200);
})
let a = promise // 会监听promise变化

如2说的,所以这时候函数外的代码得以继续执行,所以会按序执行(console.log('打印300')),console.log('打印400'),console.log('打印500'),执行到p的promise内部执行(console.log("执行了p的promise")),遇到resolve,resolve任务进入微任务队列,当前线程的宏任务完成,现在检查微任务队列,先执行test方法里面的(console.log("执行test方法await-", a))、console.log("----执行完test方法-----"),然后执行console.log("监听到p的promise变化-", res)。

在看例子:

async function test() {
    console.log("执行了test方法")
    let a = await 200
    console.log("执行test方法await-", a)
    let b = await 202
    console.log("执行test方法await-", b)
    console.log("----执行完test方法-----")
}
console.log('打印0')
test()
console.log('打印300')
console.log('打印400')
console.log('打印500')

let p = new Promise((resolve, reject) => {
    console.log("执行了p的promise")
    resolve(800)
})
p.then(res => {
    console.log("监听到p的promise变化-", res)
})
// 打印0
// 执行了test方法
// 打印300
// 打印400
// 打印500
// 执行了p的promise
// 执行test方法await- 200
// 监听到p的promise变化- 800
// 执行test方法await- 202
// ----执行完test方法-----

在看例子:

async function test() {
    console.log("执行了test方法")
    let promise = new Promise((resolve, reject) => {
        console.log("执行了test里面的Promise内部")
        resolve(222)
    })
    let a = await promise
    console.log("执行test方法await-", a)
    console.log("----执行完test方法-----")
}
console.log('打印0')
test()
console.log('打印300')
console.log('打印400')
console.log('打印500')

let p = new Promise((resolve, reject) => {
    console.log("执行了p的promise")
    resolve(800)
})
p.then(res => {
    console.log("监听到p的promise变化-", res)
})

// 打印0
// 执行了test方法
// 执行了test里面的Promise内部
// 打印300
// 打印400
// 打印500
// 执行了p的promise
// 执行test方法await- 222
// ----执行完test方法-----
// 监听到p的promise变化- 800

想了解更多?

https://yuchengkai.cn/docs/frontend/#async-%E5%92%8C-await

https://es6.ruanyifeng.com/#docs/async

好了让我们实现一个sleep

function sleep(n) {
    return new Promise(resolve => {
        setTimeout(() => {resolve(n)}, n);
    });
}
async function test() {
    console.log("------开始----")
    await sleep(1800);
    console.log("----结束----");
}
test()

 

热门文章

暂无图片
编程学习 ·

环型链表

环型链表 题目描述:给定一个链表,判断链表当中有没有环解体思路: 思路一:可以利用快慢指针的思路,给定两个指针,让两个指针一开始都位于链表头部的位置,然后开始走起来,一个指针每次走一步,一个指针每次走2步,如果说链表是有环的话,那么走的快的链表,就会先进入到环…
暂无图片
编程学习 ·

C语言 介绍

一.C的历史 编程语言的发展过程: 第1代语言 机器语言↓ 第2代语言 汇编语言↓ 第3代语言 高级语言——结构化:C,Fortran,Basic,Pascal↓分界线:1980s面向对象(OO):Algo,Simula67,Ada,SmallTalkC++,Java,C#结构化语言的缺陷: 操作和数据是分离的C语言的起源: 1969…
暂无图片
编程学习 ·

Java 中三个修饰符及相关概念

abstract(抽象的) 1.修饰类 --> 抽象类抽象类 不能创建对象,可以声明引用 抽象类可以定义属性和方法以及构造方法。 构造方法是在创建子类对象时使用(创建子类对象 先创建父类对象)2.修饰方法 --> 抽象方法语法:public abstract 返回值类型 方法名(参数列表);注意: …
暂无图片
编程学习 ·

【论文阅读报告】Visualizing and Understanding Convolutional Networks

背景 众所周知,卷积神经网络在图像处理方面表现突出,但是在很多情况下,我们在调神经网络的参数时只是依靠运气,并不知道自己对参数和网络结构的调整会影响神经网络的哪一部分。因此这篇文献的目的就是让我们通过一种可视化的方法来了解卷积神经网络如何工作,以及每一层的特…
暂无图片
编程学习 ·

Java语言基础之基础语法&流程控制语句&数组

Java语言概述 Java语言是一款面向对象的高级语言,是由Sun Microsystems公司(现已被oracle公司收购)。由James Gosling和同事们共同研发,并在1995年正式推出,据oracle官方数据指数,目前全球已有上亿的系统是使用Java开发的。Java是一门面向对象编程语言,不仅吸收了C++语言…
暂无图片
编程学习 ·

Qt编写安防视频监控系统30-GPS运动轨迹

一、前言 此功能是一个客户定制的,主要是需要在地图上动态显示GPS的运动轨迹,有个应用场景就是一个带有监控的车子,实时在运动中,后台可以接收到经纬度信息,需要绘制对应的轨迹,相当于这些摄像机点位是动态移动的,这样就可以观测到摄像机的实时位置信息,双击摄像机还可…
暂无图片
编程学习 ·

抖音上卖什么最热销?抖音上最热销的产品是什么?

抖音带货卖什么类型产品热门,抖音带货做哪个领域好自去年六月第一批100个内测账号入驻以来,抖音购物车至今已运营一年有余。随着这一年来功能打磨、生态打通等不断完善,抖音购物车已成为KOL带货变现的绝佳途径之一。第一种,在抖音里卖减肥产品。现在的人生活条件都比较好,…
暂无图片
编程学习 ·

直播带货平台开发公司哪家强?

直播带货正在我们的生活中大放异彩,直播带货的用户却日益增多,甚至成为了一种刚需,未来的发展必然一片光明,在各大商场店铺实时播放,与实体店销售员一样承担着重要的角色,它战胜了传统的广告,正昂首阔步向我们走来。直播的背后,也凸显出社会的进步与电子商务格局的变化…
暂无图片
编程学习 ·

程序员翻车时的 30 种常见反应!

**软件开发工作充满了挑战性。人无完人,对于程序员来说,写出有 bug 的代码是在所难免的。有些人很淡定,也有一些人会感到生气、沮丧、不安或气馁。在修复 bug 的过程中我们都经历了什么?这个值得我们一探究竟。 本文列出了程序员在修复 bug 时可能会说的一些话或者想法。我…
暂无图片
编程学习 ·

Layui实现动态加载Tree

目录前言实现步骤初步准备构建data数据源 前言有空研究了一下Layui,感觉相对于EasyUI来说,美观了不少,结合后台加载动态Tree带大家初步了解一下这个框架实现步骤 初步准备 Layui官网 去官网下载好Layui,里面有示例和css、js等文件具体使用步骤: 要使用Layui,必须引入css文件…
暂无图片
编程学习 ·

docker安装卡死在boot2docker.iso的下载

安装docker遇到的问题,转载记录一下。 方法一: docker安装需要最新的boot2docker.iso,从docker的安装界面复制网址,我安装的日期是20180823,此时的boot2docker.iso地址为boot2docker.iso,这个地址貌似被墙了,需要翻墙下载,将下载好的文件放到对应文件夹下,我的电脑上为…
暂无图片
编程学习 ·

C#判断端口是否被占用

public static bool PortInUse(int port){bool inUse = false;IPGlobalProperties ipProperties = IPGlobalProperties.GetIPGlobalProperties();IPEndPoint[] ipEndPoints = ipProperties.GetActiveTcpListeners();//IP端口foreach (IPEndPoint endPoint in ipEndPoints){if (…
暂无图片
编程学习 ·

RocketMQ 5:消息重试

1.创建消费者RetryConsumer,使用consumer.setMaxReconsumeTimes()方法可以设置重试次数,默认15次,返回ConsumeConcurrentlyStatus.RECONSUME_LATER;消费失败后,先会进入%RETRY%group1中,再到这个ConsumerGroup。而如果一直这样重复消费都持续失败超过重试次数,就会投递到D…
暂无图片
编程学习 ·

ant design of vue,form自定义校验

<a-form-item label="产品特性" class="am-enter_form_item"><a-select mode="tags"class="am-enter-select"placeholder="选择或填写2~4个标签(限制2~4个字)"showArrow:max-tag-count="4":max-tag-te…
暂无图片
编程学习 ·

SpringBoot+Mybatis实现简单的增删改查

SpringBoot+Mybatis实现简单的增删改查 首先是在Springboot项目中整合Mybatis 先导入依赖 <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.3.2</version></dependen…
暂无图片
编程学习 ·

Java实现文件浏览器下载

前言:先说下需求,项目需求是用户一点击一个前端页面的链接就可以下载一个压缩包.因为就1个文件,使用文件管理系统像fastDSF,阿里云的OSS这种没必要,直接放在nginx服务器上的怕不好管理,于是给我限定了把文件打包到部署时候的jar包中并实现浏览器下载. 废话不多说,直接上代码! 1…
暂无图片
编程学习 ·

CQF笔记M1L3泰勒级数和转移概率密度函数

CQF笔记M1L3泰勒级数和转移密度函数Module 1 Building Blocks of Quant FinanceLecture 2 Taylor Series and Transition Density Functions泰勒级数期权价格的泰勒级数trinomial random walk和转移密度函数Similarity solutions 求解过程 Module 1 Building Blocks of Quant F…
暂无图片
编程学习 ·

Java开发面试知识点-长期更新

前言:本节内容长期更新,专门为了扫清盲点复习。采取链接前置,内容后置。内容可能较为杂碎。 参考链接: Java基础知识面试题(2020最新版) 1、Java开发基础面试知识点 2、equals和HashCode深入理解以及Hash算法原理 长期更新1、String、StringBuilder、StringBuffer区别2、…