Promise 构造函数是同步执行还是异步执行,那么 then 方法呢?
个人题解
构造函数是同步执行,then的时候是异步
1 | const demo = function () { |
一开始底牌尽处的人,注定是输家.
最开始使用的是callback,比如浏览器端的dom事件,ajax.node上的io事件等等,但是有定位问题困难,callback hell,异常处理等问题
1 | // 浏览器端 |
相比callback的回调写法,链式调用的写法更优雅,异常处理变得友好.
1 | const demo = function () { |
用的不多,不做评价.emmm 打扰了
异步终极解决方案,generator/yield语法糖,基于promise,同步的写法,同步代码的异常捕获能力.强!无敌!
1 | const demo = function () { |
JS 异步已经告一段落了,这里来一波小总结
setTimeout(() => {
// callback 函数体
}, 1000)
缺点:回调地狱,不能用 try catch 捕获错误,不能 return
回调地狱的根本问题在于:
1 | ajax('XXX1', () => { |
Promise就是为了解决callback的问题而产生的。
Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装
优点:解决了回调地狱的问题
1 | ajax('XXX1') |
缺点:无法取消 Promise ,错误需要通过回调函数来捕获
特点:可以控制函数的执行,可以配合 co 函数库使用
1 | function *fetch() { |
let a = 0
let b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
}
b()
a++
console.log('1', a) // -> '1' 1
对于以上代码你可能会有疑惑,让我来解释下原因
首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0,因为 await 内部实现了 generator ,generator 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
因为 await 是异步操作,后来的表达式不返回 Promise 的话,就会包装成 Promise.reslove(返回值),然后会去执行函数外的同步代码
同步代码执行完毕后开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 0 + 10
上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator。如果你熟悉 co 的话,其实自己就可以实现这样的语法糖。
本文首发于我的博客:JS异步解决方案的发展历程以及优缺点
文章链接
最高赞作者github
最高赞作者地址 JS异步解决方案的发展历程以及优缺点
阮一峰 Javascript异步编程的4种方法
已知如下数组:
var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
编写一个程序将数组扁平化去并除其中重复部分数据,最终得到一个升序且不重复的数组
递归;
不想写
while循环遍历
不想写
Array.prototype.flat
1 | var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10]; |
骚操作
1 | var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10]; |
第 11 题:将数组扁平化并去除其中重复数据,最终得到一个升序且不重复的数组
Array.prototype.flat() MDN
1 | async function async1() { |
Async/Await 如何通过同步的方式实现异步
作为前端人员要回答这个问题,需要了解这三个知识点:
1 | // chrome 75 |
1 | $.ajax({ |
1 | const fs = require('fs'); |
从上面我们就可以看出,实现异步的核心就是回调钩子,将 cb 作为参数传递给异步执行函数,当有了结果后在触发 cb。想了解更多,去看看 event-loop 机制吧。
至于 async/await 是如何出现的呢,在 es6 之前,大多 js 数项目中会有类似这样的代码:
1 | ajax1(url, () => { |
这种函数嵌套,大量的回调函数,使代码阅读起来晦涩难懂,不直观,形象的称之为回调地狱(callback hell),所以为了在写法上能更通俗一点,es6+陆续出现了 Promise、Generator、Async/await,力求在写法上简洁明了,可读性强。
=========================我是分割线==========================
以上只是铺垫,下面在进入正题 👇,开始说道说道主角:async/await
=========================我是分割线==========================
async/await 是参照 Generator 封装的一套异步处理方案,可以理解为 Generator 的语法糖,
所以了解 async/await 就不得不讲一讲 Generator,
而 Generator 又依赖于迭代器Iterator,
所以就得先讲一讲 Iterator,
而 Iterator 的思想呢又来源于单向链表,
终于找到源头了:单向链表
wiki:链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序储存数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序储存,链表在插入的时候可以达到 o(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 o(n)的时间,而顺序表响应的时间复杂度分别是 o(logn)和 o(1)。
总结一下链表优点:
无需预先分配内存
插入/删除节点不影响其他节点,效率高(典型的例子:git commit、dom 操作)
单向链表:是链表中最简单的一种,它包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接
单链特点:节点的链接方向是单向的;相对于数组来说,单链表的的随机访问速度较慢,但是单链表删除/添加数据的效率很高。
理解 js 原型链/作用域链的话,理解这个很容易,他们是相通的。编程语言中,数组的长度时固定的,所以数组中的增加和删除比较麻烦,需要频繁的移动数组中的其他元素,而 js 作为一门动态语言,数组本质是一个类似数组的对象,是动态的,不需要预先分配内存
那么如何设计一个单向链表呢?这个取决于我们需要哪些操作,通常有:
1 | // 节点模型 |
通过以上,我假装你明白什么是单向链表,并且能够用代码实现一个单向链表了,下一步开始说一说迭代器 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 时可省略
根据上面的规则,咱们来自定义一个简单的迭代器:
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 }
我们还可以自定义一个可迭代对象:
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]
了解了迭代器,下面可以进一步了解生成器了
Generator:生成器对象是生成器函数(GeneratorFunction)返回的,它符合可迭代协议和迭代器协议,既是迭代器也是可迭代对象,可以调用 next 方法,但它不是函数,更不是构造函数
生成器函数(GeneratorFunction):
function* name([param[, param[, … param]]]) { statements } –>
name:函数名
param:参数
statements:js 语句
调用一个生成器函数并不会马上执行它里面的语句,而是返回一个这个生成器的迭代器对象,当这个迭代器的 next() 方法被首次(后续)调用时,其内的语句会执行到第一个(后续)出现 yield 的位置为止(让执行处于暂停状),yield 后紧跟迭代器要返回的值。或者如果用的是 yield*(多了个星号),则表示将执行权移交给另一个生成器函数(当前生成器暂停执行),调用 next() (再启动)方法时,如果传入了参数,那么这个参数会作为上一条执行的 yield 语句的返回值,例如:
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 实现异步:
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} 所以我们可以简化它,写一个自动执行器:
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 一下,ruanyifeng 老师讲的es6 入门非常棒,我时不时的都会去看一看)
4. Async/Await
首先,async/await 是 Generator 的语法糖,上面我是分割线下的第一句已经讲过,先来看一下二者的对比:
// 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 函数接收到
res.then(data => {
console.log(data); // done
});
啊,终于完了,一个 async-await 连带出来这么多知识点,以后面试被问到它的原理时,希望能够帮助到你
没想到是考察宏任务,微任务的,打扰了.草.
1 | console.log('script start') //1. 打印 script start |
Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。
1 | console.log('script start') |
当JS主线程执行到Promise对象时,
promise1.then() 的回调就是一个 task
promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
1 | async function async1(){ |
1 | async function func1() { |
1 | func1().then(res => { |
await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。
更多可见setTimeout、Promise、Async/Await
浏览器的Tasks、microtasks、 queues 和 schedules #21
第 8 题:setTimeout、Promise、Async/Await 的区别 #33
都是基于原型链的写法,别的不知道.
1 | const bar = new Bar(); // it's ok |
1 | // 引用一个未声明的变量 |
1 | // 引用一个未声明的变量 |
1 | function Bar() { |
1 | function Bar() { |
function Bar() {
Bar = 'Baz'; // it's ok
this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}
class Foo {
constructor() {
this.foo = 42;
Foo = 'Fol'; // TypeError: Assignment to constant variable
}
}
const foo = new Foo();
Foo = 'Fol'; // it's ok
没有做过相关的内容,猜测
先找到一个节点, 遍历到当前节点最深的目录
优先遍历当前一级的节点,然后再遍历下一层节点
深度优先遍历DFS 与树的先序遍历比较类似。
假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
1 | /*深度优先遍历三种方式*/ |
广度优先遍历 BFS
从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。 如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。
1 | let widthTraversal2 = (node) => { |
目前用到Set的功能,是用来去重
跟object很像,比object多了一些实用的操作方法,object的key只能是字符串,这个的key值可以是对象,数字等
没用过
成员都是对象
成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
不能遍历,方法有add, delete,has
本质上是健值对的集合,类似集合
可以遍历,方法很多,可以干跟各种数据格式转换
第 4 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
ECMAScript 6 入门 – Set 和 Map 数据结构