💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
作为前端人员要回答这个问题,需要了解这三个知识点: * 同步 * 异步 * Async/Await 首先,**js 是单线程的(重复三遍)**,所谓单线程, 通俗的讲就是,一根筋,执行代码是一行一行的往下走(即所谓的**同步**), 如果上面的没执行完,就痴痴的等着(是不是很像恋爱中在路边等她/他的你,假装 new 了个对象,啊哈哈哈,调皮一下很开心), 还是举个 🌰 吧: ~~~javascript // chrome 75 function test() { let d = Date.now(); for (let i = 0; i < 1e8; i++) {} console.log(Date.now() - d); // 62ms左右 } function test1() { let d = Date.now(); console.log(Date.now() - d); // 0 } test(); test1(); ~~~ 上面仅仅是一个 for 循环,而在实际应用中,会有大量的网络请求,它的响应时间是不确定的,这种情况下也要痴痴的等么?显然是不行的,因而 js 设计了异步,即 发起网络请求(诸如 IO 操作,定时器),由于需要等服务器响应,就先不理会,而是去做其他的事儿,等请求返回了结果的时候再说(即**异步**)。 那么如何实现异步呢?其实我们平时已经在大量使用了,那就是`callback`,例如: ~~~javascript $.ajax({ url: 'http://xxx', success: function(res) { console.log(res); }, }); ~~~ success 作为函数传递过去并不会立即执行,而是等请求成功了才执行,即**回调函数**(callback) ~~~javascript const fs = require('fs'); fs.rename('旧文件.txt', '新文件.txt', err => { if (err) throw err; console.log('重命名完成'); }); ~~~ 和网络请求类似,等到 IO 操作有了结果(无论成功与否)才会执行第三个参数:`(err)=>{}` 从上面我们就可以看出,**实现异步的核心就是回调钩子**,将 cb 作为参数传递给异步执行函数,当有了结果后在触发 cb。想了解更多,去看看`event-loop`机制吧。 至于 async/await 是如何出现的呢,在 es6 之前,大多 js 数项目中会有类似这样的代码: ~~~javascript ajax1(url, () => { ajax2(url, () => { ajax3(url, () => { // do something }); }); }); ~~~ 这种函数嵌套,大量的回调函数,使代码阅读起来晦涩难懂,不直观,形象的称之为**回调地狱(callback hell)**,所以为了在写法上能更通俗一点,es6+陆续出现了`Promise`、`Generator`、`Async/await`,力求在写法上简洁明了,可读性强。 \=========================我是分割线========================== 以上只是铺垫,下面在进入正题 👇,开始说道说道主角:`async/await` \=========================我是分割线========================== `async/await`是参照`Generator`封装的一套异步处理方案,可以理解为`Generator`的语法糖, 所以了解`async/await`就不得不讲一讲`Generator`, 而`Generator`又依赖于迭代器`Iterator`, 所以就得先讲一讲`Iterator`, 而`Iterator`的思想呢又来源于单向链表, 终于找到源头了:**单向链表** ## 1. 单向链表 > wiki:链表(Linked list)是一种常见的基础数据结构,是一种[线性表](https://zh.wikipedia.org/wiki/%E7%BA%BF%E6%80%A7%E8%A1%A8),但是并不会按线性的顺序储存数据,而是在每一个节点里存到下一个节点的[指针](https://zh.wikipedia.org/wiki/%E6%8C%87%E6%A8%99_(%E9%9B%BB%E8%85%A6%E7%A7%91%E5%AD%B8))(Pointer)。由于不必须按顺序储存,链表在插入的时候可以达到 o(1)的复杂度,比另一种线性表[顺序表](https://zh.wikipedia.org/wiki/%E9%A1%BA%E5%BA%8F%E8%A1%A8)快得多,但是查找一个节点或者访问特定编号的节点则需要 o(n)的时间,而顺序表响应的时间复杂度分别是 o(logn)和 o(1)。 总结一下链表优点: * 无需预先分配内存 * 插入/删除节点不影响其他节点,效率高(典型的例子:dom 操作) 单向链表:是链表中最简单的一种,它包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。 ![](https://img.kancloud.cn/20/cf/20cf2a60be27c7ccc5750023171e35fa_825x418.png) 一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接 单链特点:节点的链接方向是单向的;相对于数组来说,单链表的的随机访问速度较慢,但是单链表删除/添加数据的效率很高。 理解 js 原型链/作用域链的话,理解这个很容易,他们是相通的。编程语言中,数组的长度是固定的,所以数组中的增加和删除比较麻烦,需要频繁的移动数组中的其他元素,而 js 作为一门动态语言,数组本质是一个类似数组的对象,是动态的,不需要预先分配内存~ 那么如何设计一个单向链表呢?这个取决于我们需要哪些操作,通常有: * append(element):追加节点 * insert(element,index):在索引位置插入节点 * remove(element):删除第一个匹配到的节点 * removeAt(index):删除指定索引节点 * removeAll(element):删除所有匹配的节点 * get(index):获取指定索引的节点信息 * set(element,index):修改指定索引的节点值 * indexOf(element):获取某节点的索引位置 * clear():清除所有节点 * length():返回节点长度 * printf():打印节点信息 看到这些方法是不是有些许熟悉,当你用原生 js 或 jq 时常会用上面类似的方法,现在根据上面列出的方法进行实现一个单向链: ~~~javascript // 节点模型 class LinkNode { constructor(element, next) { this.element = element; this.next = next; } } class LinkedList { constructor() { this._head = null; this._size = 0; this._errorBoundary = this._errorBoundary.bind(this); this._getNodeByIndex = this._getNodeByIndex.bind(this); this.append = this.append.bind(this); this.insert = this.insert.bind(this); this.remove = this.remove.bind(this); this.removeAt = this.removeAt.bind(this); this.removeAll = this.removeAll.bind(this); this.getElement = this.getElement.bind(this); this.setIndex = this.setIndex.bind(this); this.indexOf = this.indexOf.bind(this); this.clear = this.clear.bind(this); this.length = this.length.bind(this); this.printf = this.printf.bind(this); } // 边界检验 _errorBoundary(index) { if (index < 0 || index >= this._size) { throw `超出边界(${0}~${this._size}),目标位置${index}不存在!`; } } // 根据索引获取目标对象 _getNodeByIndex(index) { this._errorBoundary(index); let obj = this._head; for (let i = 0; i < index; i++) { obj = obj.next; } return obj; } // 追加节点 append(element) { if (this._size === 0) { this._head = new LinkNode(element, null); } else { let obj = this._getNodeByIndex(this._size - 1); obj.next = new LinkNode(element, null); } this._size++; } // 在索引位置插入节点 insert(element, index) { if (index === 0) { this._head = new LinkNode(element, this._head); } else { let obj = this._getNodeByIndex(index - 1); obj.next = new LinkNode(element, obj.next); } this._size++; } // 删除第一个匹配到的节点 remove(element) { if (this._size < 1) return null; if (this._head.element == element) { this._head.element = this._head.next; this._size--; return element; } else { let temp = this._head; while (temp.next) { if (temp.next.element == element) { temp.next = temp.next.next; this._size--; return element; } else { temp = temp.next; } } } return null; } // 删除指定索引节点 removeAt(index) { this._errorBoundary(index); let element = null; if (index === 0) { element = this._head.element; this._head = this._head.next; } else { let prev = this._getNodeByIndex(index - 1); element = prev.next.element; prev.next = prev.next.next; } this._size--; return element; } // 删除所有匹配的节点 removeAll(element) { // 创建虚拟头节点, let v_head = new LinkNode(null, this._head); let tempNode = v_head; // let tempEle = null; while (tempNode.next) { if (tempNode.next.element == element) { tempNode.next = tempNode.next.next; this._size--; // tempEle = element; } else { tempNode = tempNode.next; } } this._head = v_head.next; } // 获取指定索引的节点信息 getElement(index) { return this._getNodeByIndex(index).element; } // 修改指定索引的节点值 setIndex(element, index) { this._errorBoundary(index); let obj = this._getNodeByIndex(index); obj.element = element; } // 获取某节点的索引位置 indexOf(element) { let obj = this._head; let index = -1; for (let i = 0; i < this._size; i++) { if (obj.element == element) { index = i; break; } obj = obj.next; } return index; } // 清除所有节点 clear() { this._head = null; this._size = 0; } // 返回节点长度 length() { return this._size; } // 打印节点信息 printf() { let obj = this._head; const arr = []; while (obj != null) { arr.push(obj.element); obj = obj.next; } const str = arr.join('->'); return str || null; } } const obj = new LinkedList(); obj.append(0); obj.append(1); obj.append(2); obj.printf(); // "0->1->2" obj.insert(3, 3); obj.printf(); // "0->1->2->3" obj.remove(3); obj.printf(); // "0->1->2" obj.removeAt(0); obj.printf(); // "1->2" obj.setIndex(0, 0); obj.printf(); // "0->2" obj.indexOf(2); // 1 obj.length(); // 2 obj.clear(); obj.printf(); // null ~~~ [查看源码](https://github.com/Mr-jiangzhiguo/book/code/linked.js) 通过以上,我假装你明白什么是单向链表,并且能够用代码实现一个单向链表了,下一步开始说一说**迭代器**`Iterator` ## 2. Iterator `Iterator`翻译过来就是 **迭代器(遍历器)** 让我们先来看看它的遍历过程(类似于单向链表): * 创建一个**指针对象**,指向当前数据结构的起始位置 * 第一次调用指针对象的`next`方法,将指针指向数据结构的第一个成员 * 第二次调用指针对象的`next`方法,将指针指向数据结构的第二个成员 * 不断的调用指针对象的`next`方法,直到它指向数据结构的结束位置 一个对象要变成可迭代的,必须实现`@@iterator`方法,即对象(或它原型链上的某个对象)必须有一个名字是`Symbol.iterator`的属性(原生具有该属性的有:字符串、数组、类数组的对象、Set 和 Map): | 属性 | 值 | | --- | --- | | [Symbol.iterator]: | 返回一个对象的无参函数,被返回对象符合迭代器协议 | 当一个对象需要被迭代的时候(比如开始用于一个`for..of`循环中),它的`@@iterator`方法被调用并且无参数,然后返回一个用于在迭代中获得值的迭代器 迭代器协议:产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值 当一个对象只有满足下述条件才会被认为是一个迭代器: 它实现了一个`next()`的方法,该方法**必须返回一个对象**,对象有两个必要的属性: * `done`(bool) * true:迭代器已经超过了可迭代次数。这种情况下,value 的值可以被省略 * 如果迭代器可以产生序列中的下一个值,则为 false。这等效于没有指定 done 这个属性 * `value`迭代器返回的任何 JavaScript 值。done 为 true 时可省略 根据上面的规则,咱们来自定义一个简单的迭代器: ~~~javascript const makeIterator = arr => { let nextIndex = 0; return { next: () => nextIndex < arr.length ? { value: arr[nextIndex++], done: false } : { value: undefined, done: true }, }; }; const it = makeIterator(['人月', '神话']); console.log(it.next()); // { value: "人月", done: false } console.log(it.next()); // { value: "神话", done: false } console.log(it.next()); // {value: undefined, done: true } ~~~ 我们还可以自定义一个可迭代对象: ~~~javascript const myIterable = {}; myIterable[Symbol.iterator] = function*() { yield 1; yield 2; yield 3; }; for (let value of myIterable) { console.log(value); } // 1 // 2 // 3 //or console.log([...myIterable]); // [1, 2, 3] ~~~ 了解了迭代器,下面可以进一步了解生成器了 ## 3. Generator `Generator`:生成器对象是生成器函数(GeneratorFunction)返回的,它符合**可迭代协议**和**迭代器协议**,既是迭代器也是可迭代对象,可以调用`next`方法,但它不是函数,更不是构造函数 生成器函数(GeneratorFunction): > function\* name([param[, param[, ... param]]]) { statements } > > * name:函数名 > * param:参数 > * statements:js 语句 调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器对象,当这个迭代器的`next()`方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现`yield`的位置为止(让执行处于**暂停状**),`yield`后紧跟迭代器要返回的值。或者如果用的是`yield*`(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器**暂停执行**),调用`next()`(再启动)方法时,如果传入了参数,那么这个参数会作为**上一条执行的`yield`语句的返回值**,例如: ~~~javascript function* another() { yield '人月神话'; } function* gen() { yield* another(); // 移交执行权 const a = yield 'hello'; const b = yield a; // a='world' 是 next('world') 传参赋值给了上一个 yidle 'hello' 的左值 yield b; // b=! 是 next('!') 传参赋值给了上一个 yidle a 的左值 } const g = gen(); g.next(); // {value: "人月神话", done: false} g.next(); // {value: "hello", done: false} g.next('world'); // {value: "world", done: false} 将 'world' 赋给上一条 yield 'hello' 的左值,即执行 a='world', g.next('!'); // {value: "!", done: false} 将 '!' 赋给上一条 yield a 的左值,即执行 b='!',返回 b g.next(); // {value: undefined, done: false} ~~~ 看到这里,你可能会问,`Generator`和`callback`有啥关系,如何处理异步呢?其实二者没有任何关系,我们只是通过一些方式强行的它们产生了关系,才会有`Generator`处理异步 我们来总结一下`Generator`的本质,暂停,它会让程序执行到指定位置先暂停(`yield`),然后再启动(`next`),再暂停(`yield`),再启动(`next`),而这个暂停就很容易让它和异步操作产生联系,因为我们在处理异步时:开始异步处理(网络求情、IO 操作),然后暂停一下,等处理完了,再该干嘛干嘛。不过值得注意的是,**js 是单线程的(又重复了三遍)**,异步还是异步,callback 还是 callback,不会因为`Generator`而有任何改变 下面来看看,用`Generator`实现异步: ~~~javascript const promisify = require('util').promisify; const path = require('path'); const fs = require('fs'); const readFile = promisify(fs.readFile); const gen = function*() { const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' }); console.log(res1); const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' }); console.log(res2); }; const g = gen(); const g1 = g.next(); console.log('g1:', g1); g1.value .then(res1 => { console.log('res1:', res1); const g2 = g.next(res1); console.log('g2:', g2); g2.value .then(res2 => { console.log('res2:', res2); g.next(res2); }) .catch(err2 => { console.log(err2); }); }) .catch(err1 => { console.log(err1); }); // g1: { value: Promise { <pending> }, done: false } // res1: { // "a": 1 // } // { // "a": 1 // } // g2: { value: Promise { <pending> }, done: false } // res2: { // "b": 2 // } // { // "b": 2 // } ~~~ 以上代码是`Generator`和`callback`结合实现的异步,可以看到,仍然需要手动执行`.then`层层添加回调,但由于`next()`方法返回对象`{value: xxx,done: true/false}`所以我们可以简化它,写一个自动执行器: ~~~javascript const promisify = require('util').promisify; const path = require('path'); const fs = require('fs'); const readFile = promisify(fs.readFile); function run(gen) { const g = gen(); function next(data) { const res = g.next(data); // 深度递归,只要 `Generator` 函数还没执行到最后一步,`next` 函数就调用自身 if (res.done) return res.value; res.value.then(function(data) { next(data); }); } next(); } run(function*() { const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' }); console.log(res1); // { // "a": 1 // } const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' }); console.log(res2); // { // "b": 2 // } }); ~~~ 说了这么多,怎么还没有到`async/await`,客官别急,马上来了(其实我已经漏了一些内容没说:Promise 和 callback 的关系,thunk 函数,co 库,感兴趣的可以去 google 一下,yuanyifeng 老师讲的[es6 入门](http://es6.ruanyifeng.com/)非常棒,我时不时的都会去看一看) ## 4\. Async/Await 首先,`async/await`是`Generator`的语法糖,上面*我是分割线*下的第一句已经讲过,先来看一下二者的对比: ~~~javascript // Generator run(function*() { const res1 = yield readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' }); console.log(res1); const res2 = yield readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' }); console.log(res2); }); // async/await const readFile = async ()=>{ const res1 = await readFile(path.resolve(__dirname, '../data/a.json'), { encoding: 'utf8' }); console.log(res1); const res2 = await readFile(path.resolve(__dirname, '../data/b.json'), { encoding: 'utf8' }); console.log(res2); return 'done'; } const res = readFile(); ~~~ 可以看到,`async function`代替了`function*`,`await`代替了`yield`,同时也无需自己手写一个自动执行器`run`了 现在再来看看`async/await`的特点: * 当`await`后面跟的是 Promise 对象时,才会异步执行,其它类型的数据会同步执行 * 执行`const res = readFile();`返回的仍然是个 Promise 对象,上面代码中的`return 'done';`会直接被下面`then`函数接收到 ~~~javascript res.then(data => { console.log(data); // done }); ~~~ ## 摘自 [第 9 题:Async/Await 如何通过同步的方式实现异步](https://www.muyiy.cn/question/async/9.html)