0%

(头条、微医)Async/Await 如何通过同步的方式实现异步

个人题解

  • 是promise的语法糖,async 返回的是promise,await 后面的语句相当于放在前一个promise的then 方法内.
  • 个人猜测是这样

最高赞题解

本质是单向链表吧

Async/Await 如何通过同步的方式实现异步
作为前端人员要回答这个问题,需要了解这三个知识点:

  • 同步
  • 异步
  • Async/Await
    首先,js 是单线程的(重复三遍),所谓单线程,
    通俗的讲就是,一根筋(比喻有点过分,哈哈)执行代码是一行一行的往下走(即所谓的同步),
    如果上面的没执行完,就痴痴的等着(是不是很像恋爱中在路边等她/他的你,假装 new 了个对象,啊哈哈哈,调皮一下很开心),
    还是举个 🌰 吧:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 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,例如:
    1
    2
    3
    4
    5
    6
    $.ajax({
    url: 'http://xxx',
    success: function(res) {
    console.log(res);
    },
    });
    success 作为函数传递过去并不会立即执行,而是等请求成功了才执行,即回调函数(callback)
    1
    2
    3
    4
    5
    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 数项目中会有类似这样的代码:

1
2
3
4
5
6
7
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)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序储存数据,而是在每一个节点里存到下一个节点的指针(Pointer)。由于不必须按顺序储存,链表在插入的时候可以达到 o(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要 o(n)的时间,而顺序表响应的时间复杂度分别是 o(logn)和 o(1)。

总结一下链表优点:

无需预先分配内存
插入/删除节点不影响其他节点,效率高(典型的例子:git commit、dom 操作)
单向链表:是链表中最简单的一种,它包含两个域,一个信息域和一个指针域。这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。
20200819112119
一个单向链表包含两个值: 当前节点的值和一个指向下一个节点的链接

单链特点:节点的链接方向是单向的;相对于数组来说,单链表的的随机访问速度较慢,但是单链表删除/添加数据的效率很高。

理解 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 时常会用上面类似的方法,现在根据上面列出的方法进行实现一个单向链:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    // 节点模型
    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

通过以上,我假装你明白什么是单向链表,并且能够用代码实现一个单向链表了,下一步开始说一说迭代器 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 时可省略
根据上面的规则,咱们来自定义一个简单的迭代器:

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]

了解了迭代器,下面可以进一步了解生成器了

3. Generator

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 连带出来这么多知识点,以后面试被问到它的原理时,希望能够帮助到你

相关链接

setTimeout、Promise、Async/Await 的区别

个人题解

  • setTimeout 只是一个异步的定时器,用来执行多少时间后的任务
  • promise 是一个规范,用then,catch,all,race等方法更好的处理异步内容
  • async/await 是一个promise的语法糖,不了解具体的实现

没想到是考察宏任务,微任务的,打扰了.草.

最高赞题解

setTimeout

1
2
3
4
5
6
console.log('script start')	//1. 打印 script start
setTimeout(function(){
console.log('settimeout') // 4. 打印 settimeout
}) // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end') //3. 打印 script start
// 输出顺序:script start->script end->settimeout
  1. Promise
  • Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    console.log('script start')
    let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
    }).then(function () {
    console.log('promise2')
    })
    setTimeout(function(){
    console.log('settimeout')
    })
    console.log('script end')
    // 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout
  • 当JS主线程执行到Promise对象时,

  • promise1.then() 的回调就是一个 task

  • promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue

  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中

  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

async/await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function async1(){
console.log('async1 start');
await async2();
console.log('async1 end')
}
async function async2(){
console.log('async2')
}

console.log('script start');
async1();
console.log('script end')

// 输出顺序:script start->async1 start->async2->script end->async1 end

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

  • 举个例子:
1
2
3
4
async function func1() {
return 1
}
console.log(func1())
  • 在这里插入图片描述
  • 很显然,func1的运行结果其实就是一个Promise对象。因此我们也可以使用then来处理后续逻辑。
1
2
3
func1().then(res => {
console.log(res); // 30
})

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

更多可见setTimeout、Promise、Async/Await
浏览器的Tasks、microtasks、 queues 和 schedules #21
第 8 题:setTimeout、Promise、Async/Await 的区别 #33

ES5/ES6 的继承除了写法以外还有什么区别?

个人题解

都是基于原型链的写法,别的不知道.

最高题解

class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。(没有变量提升)

1
2
3
4
5
6
7
8
9
10
11
const bar = new Bar(); // it's ok
function Bar() {
this.bar = 42;
}

const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
constructor() {
this.foo = 42;
}
}

class 声明内部会启用严格模式。

1
2
3
4
5
6
7
8
9
10
11
12
// 引用一个未声明的变量
function Bar() {
baz = 42; // it's ok
}
const bar = new Bar();

class Foo {
constructor() {
fol = 42; // ReferenceError: fol is not defined
}
}
const foo = new Foo();

class 的所有方法(包括静态方法和实例方法)都是不可枚举的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 引用一个未声明的变量
function Bar() {
this.bar = 42;
}
Bar.answer = function() {
return 42;
};
Bar.prototype.print = function() {
console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']

class Foo {
constructor() {
this.foo = 42;
}
static answer() {
return 42;
}
print() {
console.log(this.foo);
}
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []

class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Bar() {
this.bar = 42;
}
Bar.prototype.print = function() {
console.log(this.bar);
};

const bar = new Bar();
const barPrint = new bar.print(); // it's ok

class Foo {
constructor() {
this.foo = 42;
}
print() {
console.log(this.foo);
}
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor

必须使用 new 调用 class。

1
2
3
4
5
6
7
8
9
10
11
function Bar() {
this.bar = 42;
}
const bar = Bar(); // it's ok

class Foo {
constructor() {
this.foo = 42;
}
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'

class 内部无法重写类名。

 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

相关链接

第 7 期:ES5/ES6 的继承除了写法以外还有什么区别? #20

介绍下深度优先遍历和广度优先遍历,如何实现?

个人题解

没有做过相关的内容,猜测

深度优先遍历

先找到一个节点, 遍历到当前节点最深的目录

广度优先遍历

优先遍历当前一级的节点,然后再遍历下一层节点

最高赞题

  • 第五题问的是深度优先遍历和广度优先遍历,我是从dom节点的遍历来理解这个问题的
  • html代码
  • 20200807134451
  • 我将用深度优先遍历和广度优先遍历对这个dom树进行查找

深度优先遍历


深度优先遍历DFS 与树的先序遍历比较类似。
假设初始状态是图中所有顶点均未被访问,则从某个顶点v出发,首先访问该顶点然后依次从它的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和v有路径相通的顶点都被访问到。若此时尚有其他顶点未被访问到,则另选一个未被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/*深度优先遍历三种方式*/
let deepTraversal1 = (node, nodeList = []) => {
if (node !== null) {
nodeList.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
deepTraversal1(children[i], nodeList)
}
}
return nodeList
}
let deepTraversal2 = (node) => {
let nodes = []
if (node !== null) {
nodes.push(node)
let children = node.children
for (let i = 0; i < children.length; i++) {
nodes = nodes.concat(deepTraversal2(children[i]))
}
}
return nodes
}
// 非递归
let deepTraversal3 = (node) => {
let stack = []
let nodes = []
if (node) {
// 推入当前处理的node
stack.push(node)
while (stack.length) {
let item = stack.pop()
let children = item.children
nodes.push(item)
// node = [] stack = [parent]
// node = [parent] stack = [child3,child2,child1]
// node = [parent, child1] stack = [child3,child2,child1-2,child1-1]
// node = [parent, child1-1] stack = [child3,child2,child1-2]
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i])
}
}
}
return nodes
}

输出结果

20200807134542

广度优先遍历


广度优先遍历 BFS
从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使得“先被访问的顶点的邻接点先于后被访问的顶点的邻接点被访问,直至图中所有已被访问的顶点的邻接点都被访问到。 如果此时图中尚有顶点未被访问,则需要另选一个未曾被访问过的顶点作为新的起始点,重复上述过程,直至图中所有顶点都被访问到为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let widthTraversal2 = (node) => {
let nodes = []
let stack = []
if (node) {
stack.push(node)
while (stack.length) {
let item = stack.shift()
let children = item.children
nodes.push(item)
// 队列,先进先出
// nodes = [] stack = [parent]
// nodes = [parent] stack = [child1,child2,child3]
// nodes = [parent, child1] stack = [child2,child3,child1-1,child1-2]
// nodes = [parent,child1,child2]
for (let i = 0; i < children.length; i++) {
stack.push(children[i])
}
}
}
return nodes
}

输出结果

20200807134554

相关链接

第 5 题:介绍下深度优先遍历和广度优先遍历,如何实现? #9

介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

个人题解

Set

目前用到Set的功能,是用来去重

Map

跟object很像,比object多了一些实用的操作方法,object的key只能是字符串,这个的key值可以是对象,数字等

WeakSet 和 WeakMap

没用过

最高赞题解

Set

  1. 成员不能重复
  2. 只有健值,没有健名,有点类似数组。
  3. 可以遍历,方法有add, delete,has

weakSet

成员都是对象
成员都是弱引用,随时可以消失。 可以用来保存DOM节点,不容易造成内存泄漏
不能遍历,方法有add, delete,has

Map

本质上是健值对的集合,类似集合
可以遍历,方法很多,可以干跟各种数据格式转换

weakMap

  1. 直接受对象作为健名(null除外),不接受其他类型的值作为健名
  2. 健名所指向的对象,不计入垃圾回收机制
  3. 不能遍历,方法同get,set,has,delete

相关链接

第 4 题:介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
ECMAScript 6 入门 – Set 和 Map 数据结构

什么是防抖和节流?有什么区别?如何实现?

个人题解

防抖

在一个时间段内,如果有触发多次,只执行一次.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const debonce = (fn, timer) => {
let loop = null;
return function () {
if (loop) {
clearTimeout(loop);
}
loop = setTimeout(() => {
fn.apply(this, arguments);
}, timer);
};
};

function demo(arg1, arg2) {
console.log(arg1, arg2);
}

let debonceDemo = debonce(demo, 1000);
let i = 0;
let test = setInterval(() => {
console.log(i);
debonceDemo(1, 2);
if (++i > 100) {
clearInterval(test);
}
}, 10);

节流

不论执行速度有多快,固定时间内执行的次数只有一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const throttle = (fn, timer) => {
let loop = true;
return function () {
if (!loop) {
return;
}
loop = false;
setTimeout(() => {
fn.apply(this, arguments);
loop = true;
}, timer);
};
};

function demo(arg1, arg2) {
console.log(arg1, arg2);
}

let throttleDemo = throttle(demo, 1000);
let i = 0;
let test = setInterval(() => {
console.log(i);
throttleDemo(1, 2);
if (++i > 100) {
clearInterval(test);
}
}, 100);

最高赞题解

防抖

触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间

思路:

每次触发事件时都取消之前的延时调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function debounce(fn) {
let timeout = null; // 创建一个标记用来存放定时器的返回值
return function () {
clearTimeout(timeout); // 每当用户输入的时候把前一个 setTimeout clear 掉
timeout = setTimeout(() => { // 然后又创建一个新的 setTimeout, 这样就能保证输入字符后的 interval 间隔内如果还有字符输入的话,就不会执行 fn 函数
fn.apply(this, arguments);
}, 500);
};
}
function sayHi() {
console.log('防抖成功');
}

var inp = document.getElementById('inp');
inp.addEventListener('input', debounce(sayHi)); // 防抖

节流

高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率

思路:

每次触发事件时都判断当前是否有等待执行的延时函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function throttle(fn) {
let canRun = true; // 通过闭包保存一个标记
return function () {
if (!canRun) return; // 在函数开头判断标记是否为true,不为true则return
canRun = false; // 立即设置为false
setTimeout(() => { // 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。当定时器没有执行的时候标记永远是false,在开头被return掉
canRun = true;
}, 500);
};
}
function sayHi(e) {
console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

相关链接

第 3 题:什么是防抖和节流?有什么区别?如何实现?

React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

题解

个人猜想是diff算法的优化办法,固定key以后,如果更新数据,可以确定需要替换的数据,减少dom替换的开销

赞同最高题解

受楼下答案的一些特殊情况影响,导致很多人都认为key不能”提高”diff速度。在此继续重新梳理一下答案。

在楼下的答案中,部分讨论都是基于没有key的情况diff速度会更快。确实,这种观点并没有错。没有绑定key的情况下,并且在遍历模板简单的情况下,会导致虚拟新旧节点对比更快,节点也会复用。而这种复用是就地复用,一种鸭子辩型的复用。以下为简单的例子:

1
2
3
4
5
6
7
8
9
<div id="app">
<div v-for="i in dataList">{{ i }}</div>
</div>
var vm = new Vue({
el: '#app',
data: {
dataList: [1, 2, 3, 4, 5]
}
})

以上的例子,v-for的内容会生成以下的dom节点数组,我们给每一个节点标记一个身份id:

1
2
3
4
5
6
7
[
'<div>1</div>', // id: A
'<div>2</div>', // id: B
'<div>3</div>', // id: C
'<div>4</div>', // id: D
'<div>5</div>' // id: E
]

改变dataList数据,进行数据位置替换,对比改变后的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vm.dataList = [4, 1, 3, 5, 2] // 数据位置替换

// 没有key的情况, 节点位置不变,但是节点innerText内容更新了
[
'<div>4</div>', // id: A
'<div>1</div>', // id: B
'<div>3</div>', // id: C
'<div>5</div>', // id: D
'<div>2</div>' // id: E
]

// 有key的情况,dom节点位置进行了交换,但是内容没有更新
// <div v-for="i in dataList" :key='i'>{{ i }}</div>
[
'<div>4</div>', // id: D
'<div>1</div>', // id: A
'<div>3</div>', // id: C
'<div>5</div>', // id: E
'<div>2</div>' // id: B
]

增删dataList列表项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vm.dataList = [3, 4, 5, 6, 7] // 数据进行增删

// 1. 没有key的情况, 节点位置不变,内容也更新了
[
'<div>3</div>', // id: A
'<div>4</div>', // id: B
'<div>5</div>', // id: C
'<div>6</div>', // id: D
'<div>7</div>' // id: E
]

// 2. 有key的情况, 节点删除了 A, B 节点,新增了 F, G 节点
// <div v-for="i in dataList" :key='i'>{{ i }}</div>
[
'<div>3</div>', // id: C
'<div>4</div>', // id: D
'<div>5</div>', // id: E
'<div>6</div>', // id: F
'<div>7</div>' // id: G
]

从以上来看,不带有key,并且使用简单的模板,基于这个前提下,可以更有效的复用节点,diff速度来看也是不带key更加快速的,因为带key在增删节点上有耗时。这就是vue文档所说的默认模式。但是这个并不是key作用,而是没有key的情况下可以对节点就地复用,提高性能。

这种模式会带来一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。VUE文档也说明了 这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出

楼下 @yeild 也提到,在不带key的情况下,对于简单列表页渲染来说diff节点更快是没有错误的。但是这并不是key的作用呀。

但是key的作用是什么?
我重新梳理了一下文字,可能这样子会更好理解一些。

key是给每一个vnode的唯一id,可以依靠key,更准确, 更快的拿到oldVnode中对应的vnode节点。

  1. 更准确
    因为带key就不是就地复用了,在sameNode函数 a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。

  2. 更快
    利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。(这个观点,就是我最初的那个观点。从这个角度看,map会比遍历更快。)

原答案 ———————–

vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中(建议先了解一下diff算法过程)。
在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点(这里对应的是一个key => index 的map映射)。如果没找到就认为是一个新增节点。而如果没有key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个map映射,另一种是遍历查找。相比而言。map映射的速度更快。
vue部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vue项目  src/core/vdom/patch.js  -488行
// 以下是为了阅读性进行格式化后的代码

// oldCh 是一个旧虚拟节点数组
if (isUndef(oldKeyToIdx)) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
// map 方式获取
idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
// 遍历方式获取
idxInOld = findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
}

创建map函数

1
2
3
4
5
6
7
8
9
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}

遍历寻找

1
2
3
4
5
6
7
8
// sameVnode 是对比新旧节点是否相同的函数
function findIdxInOld (node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]

if (isDef(c) && sameVnode(node, c)) return i
}
}

链接

写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?

安装hexo

1
npm install -g hexo-cli

mac电脑可能需要使用sudo权限

1
sudo npm install -g hexo-cli

初始化项目,安装依赖

1
2
3
$ hexo init <folder>
$ cd <folder>
$ npm install

各类配置项参考hexo中文文档

发布到github pages

  1. 创建一个github仓库<yourname>.github.com,比如我的lampofaladdin.github.io
  2. 在_config.yml中增加配置项
    1
    2
    3
    4
    deploy:
    type: git
    repo: <github git地址>
    # 例如git@github.com:lampofaladdin/lampofaladdin.github.io.git
  3. 发布到github hexo deploy

使用阿里云oss静态页关联自己域名

  1. 创建阿里云Bucket,不开通版本控制,读写为公共读,其他默认
  2. 创建ram用户,给用户oss的控制权限
  3. 创建ram用户的AccessKey,获取到AccessKeyID,AccessKeySecret
  4. 进入bucket,基础设置,静态页面,默认首页为index.html,子目录首页开通
  5. 传输管理,绑定你已经购买备案好的域名。
  6. 安装hexo-deployer-ali-oss,npm i hexo-deployer-ali-oss --save
  7. 在_config.yml中增加配置
    1
    2
    3
    4
    5
    6
    deploy:
    type: ali-oss
    region: <regionName> # oss-cn-shanghai oss-cn-beijing oss-cn-guangzhou 等
    accessKeyId: <AccessKeyID>
    accessKeySecret: <AccessKeySecret>
    bucket: <bucketName> # shendengjun-blog
  8. npm deploy

同时发布在githubPage 与 阿里云oss

1
2
3
4
5
6
7
8
deploy:
- type: git
repo: <github git地址>
- type: ali-oss
region: <regionName> # oss-cn-shanghai oss-cn-beijing oss-cn-guangzhou 等
accessKeyId: <AccessKeyID>
accessKeySecret: <AccessKeySecret>
bucket: <bucketName> # shendengjun-blog

TODO

  • git push的时候depoly
  • 标签分类怎么做

相关链接

第 160 题:输出以下代码运行结果,为什么?如果希望每隔 1s 输出一个结果,应该如何改造?注意不可改动 square 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const list = [1, 2, 3]
const square = num => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}

function test() {
list.forEach(async x=> {
const res = await square(x)
console.log(res)
})
}
test()

题解

因为foreach是并行执行的,并不支持异步,需要改成for或者 for of

1
2
3
4
5
6
7
8
9
10
11
12
13
async function test() {
for(let i = 0 ;i < list.length ; i++){
const res = await square(list[i])
console.log(res)
}
}

async function test() {
for(let i of list){
let res = await square(i)
console.log(res)
}
}

聊聊生活,写写技术.

欢迎加入前端交流群·VUE|JS|TS|全栈|前端交流·群号:318195769