资讯专栏INFORMATION COLUMN

链式调用与事件循环--一道JavaScript面试题的思考

wow_worktile / 2034人阅读

摘要:最后画几张粗糙的图,简单描述一下这个执行的过程因为是链式调用,所以在链上的都会入栈然后执行,额,执行栈少画了和。。。

前言:昨天在群里讨(jin)论(chui)技(niu)术(pi),有位老铁发了一道他面的某公司面试题,顺手保存了。今早花了一点时间把这题做了出来,发现挺有意思的,决定在今天认真工(hua)作(shui)前,与大家分享我的解题方案和思考过程。

题目如下(可以自己先思考一会,没准可以想出比我更好的方法):

小眼一撇,这几个需求都是要实现链式调用,而链式调用最常见的是 jQuery,还有就是我们非常熟悉的 Promise。

jQuery中链式调用的原理是在函数的末尾return this(即返回这个对象自身),使得对象可以继续调用自身的函数从而达到支持链式调用。

知道了这个套路之后,接下来我们可以按照这个套路飞快的先写出符合第一个小需求的函数。

const LazyMan = function (name) {
  console.log(`Hi i am ${name}`);
}
LazyMan("Tony")
// Hi i am Tony

虽然只有短短三行代码,但没有报一点错,而且运行起来飞快的,完美的实现了第一个小需求。

某路人:“等等,,不就是一个简单的函数,套路用在哪呢?”

啧啧,被你发现了,小伙子不错嘛,好,现在就用链式调把第二个小需求实现了:

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);
    class F {
      sleep(timeout) {
        setTimeout(function () {
          console.log(`等待了${timeout}秒`);
          return this;
        }, timeout)
      };

      eat(food) {
        console.log(`I am eating ${food}`);
        return this;
      }
    }
    return new F();
  }

LazyMan("Tony").sleep(10).eat("lunch")

丢浏览器里面跑一下,一段红条条蹦了出来

Uncaught TypeError: Cannot read property "eat" of undefined

纳尼,eat为什么会在undefined上调用,我不是在sleep中返回了this么!?是不是 Chrome 又偷偷更新,加了一个新 bug,,,

不过 google 工程师应该没有这么不靠谱吧。难道是我写错了?

扫一遍代码,发现return this是在setTimeout中的处理函数返回的,而不是sleep返回的,小改一下。

// ...
sleep(timeout) {
        setTimeout(function () {
          console.log(`等待了${timeout}秒....`);
        }, timeout)
        return this;
      };
// ...

再跑一下,没有红条条了,嘿。

但仔细一看,跟需求中的顺序不一致,我们现在的输出是这样的:

Hi i am Tony
I am eating lunch
等待了10秒

emmmmm,看来,现在得拿出一点 JavaScript 硬本事了。

JavaScript 中有同步任务和异步任务,同步任务就是按照我们编写顺序推入执行栈,一步一步执行;而setTimeout属于异步任务,在浏览器中是由定时触发器线程负责,这个线程会进行计时,当计时完成后将这个事件的handler推入到任务队列中,任务队列中的任务需要等待执行栈中为空时把队列中的任务丢入执行栈中进行执行(从这里也可以知道handler并不能准时执行)。
(随手画了一张草图,有点丑,不过应该不影响我想要表达的意思)

如果不太了解,可以参考这篇文章 这一次,彻底弄懂 JavaScript 执行机制 ,写的非常易懂了

知道了这个知识后,然并卵,它不能帮我们写出所需要的代码。。。

在空气安静了数十分钟后,我还是毫无头绪,只好拿起杯子,准备起身去倒杯水压压惊,突然犹有一道闪电击到了我一般,脑海中浮现了 vue 中实现nextTick这一方法实现的代码,代码虽模糊不清(我根本记不清楚了),但我造这应该可以帮助我解决点什么问题。so,我放下杯子,熟练的打开某 hub,在里面找到了nextTick的实现代码(在这里next-tick.js)。
快速从第一行到最后一行扫了一遍,可以获取到的东东是:它用一个callbacks数组存储需要执行的函数,然后利用micro taskmacro task的优先级特性,从而可以在 DOM 渲染之前执行callbacks中的回调。emmmmm,跟我现在的需求好像扯不上什么关系,并不能给什么帮助。不过我也可以把需要执行的函数加入一个数组中,在最后执行它。说干就干,可以快速写出如下代码:

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);
    function _eat(food){
      console.log(`I am eating ${food}`);
    }
    const callbacks = [];
    class F {
      sleep(timeout) {
        setTimeout(function () {
          console.log(`等待了${timeout}秒....`);
          callbacks.forEach(cb=>cb())
        }, timeout);
        return this;

      };

      eat(food) {
        callbacks.push(_eat.bind(null,food));
        return this;
      }
    }
    return new F();
  };

  LazyMan("Tony").sleep(10).eat("lunch")
  // Hi i am Tony
  // 等待了10秒....
  // I am eating lunch

执行完,输出跟需求一模一样,嘿嘿嘿。

接着按照第三个小需求执行一下,结果如下:

//...

LazyMan("Tony").eat("lunch").sleep(10).eat("dinner")

// Hi i am Tony
// 等待了10秒
// I am eating lunch
// I am eating dinner

//...

没有报错,很好,但顺序又错了。。。这可不好办。
眼看着空气又要安静下来了,我不能干耗着,决定使用一些常用套路了,比如加个flag,区分是否是需要在 sleep 之后执行的方法,改写后如下:

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);

    function _eat(food) {
      console.log(`I am eating ${food}`);
    }
    const callbacks = [];
    let isNeedSleep = false;
    class F {
      sleep(timeout) {
        setTimeout(function () {
          console.log(`等待了${timeout}秒`);
          callbacks.forEach(cb => cb())
        }, timeout);
        isNeedSleep = true;
        return this;
      };
      eat(food) {
        if (isNeedSleep) {
          callbacks.push(_eat.bind(null, food));
        } else {
          _eat.call(null, food);
        }
        return this;
      }
    }
    return new F();
  };

跑一下,跟第三个小需求输出一模一样,嘿嘿嘿,小菜一碟。

到最后这个小需求中,链式调用中多了一个sleepFirst,其效果是会将sleep提至链式调用的最前端来执行,也就是说sleepFirst的优先级最高。

容我思考一下: 能够根据优先级来操作的数据结构,在我所知的范围内只有优先队列,而优先队列可以用数组来实现,so,是不是说可以用数组来实现优先级callbacks的调用,即用嵌套数组。答曰:你想的没有错啦。

撸起袖子继续干,于是数分钟后有了下面这个函数

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);

    function _eat(food) {
      console.log(`I am eating ${food}`);
    }
    const callbackQueue = [];
    let index = 0;
    class F {
      sleep(timeout) {
        const _callbacks = callbackQueue.shift();
        _callbacks && _callbacks.forEach(cb => cb());
        setTimeout(function () {
          console.log(`等待了${timeout}秒....`);
          const _callbacks = callbackQueue.shift();
          _callbacks && _callbacks.forEach(cb => cb())
        }, timeout);
        index ++;
        return this;
      };
      eat(food) {
        if(!callbackQueue[index]) callbackQueue[index] = [];
        callbackQueue[index].push(_eat.bind(null, food));
        return this;
      };
      sleepFirst(timeout){
        setTimeout(function () {
          console.log(`等待了${timeout}秒....`);
          const _callbacks = callbackQueue.shift();
           _callbacks && _callbacks.forEach(cb => cb())
        }, timeout);
        index ++;
        return this;
      }
    }

    return new F();
  };

我的想法是 每经过一次sleep后,index会+1,表示有新的一组callback,当执行eat时,判断是否存在当前index对应的数组,不存在则创建一个对应的空数组,然后把对应需要调用的函数添加入这个数组中,最后把这个数组存到callbackQueue中,当添加完成后,会按照顺序一步一步从callbackQueue中取出并执行。

虽然我思路这思路应该是对的,但我还是隐隐约约感觉到了里面蕴含的红条条,先丢浏览器中跑一下试试。

结果如下:

Hi i am Tony
I am eating lunch
I am eating dinner
等待了5秒....
等待了10秒....

果然,没有按照所需的顺序执行,因为这里还是没有能够处理sleepFirst优先级的这个根本问题。。。

等等。。。我刚刚说了啥,"优先级",咱们往上翻,我前面好像提到过这个词!

没错,vue中的nextTick中就用到了,我们可以参考它,利用Event Loopmicro taskmacro task执行的优先级来解决这个问题。

  const LazyMan = function (name) {
    console.log(`Hi i am ${name}`);

    function _eat(food) {
      console.log(`I am eating ${food}`);
    }

    const callbackQueue = [];
    let index = 0;

    class F {
      sleep(timeout) {
        setTimeout(() => {
          const _callbacks = callbackQueue.shift();
          _callbacks && _callbacks.forEach(cb => cb());
          setTimeout(function () {
            console.log(`等待了${timeout}秒....`);
            const _callbacks = callbackQueue.shift();
            _callbacks && _callbacks.forEach(cb => cb())
          }, timeout);
        })
        index++;
        return this;
      };

      eat(food) {
        if (!callbackQueue[index]) callbackQueue[index] = [];
        callbackQueue[index].push(_eat.bind(null, food));
        return this;
      };

      sleepFirst(timeout) {
        Promise.resolve().then(() => {
          const _callbacks = callbackQueue.shift();
          setTimeout(function () {
            console.log(`等待了${timeout}秒....`);
            _callbacks && _callbacks.forEach(cb => cb())
          }, timeout);
        })
        index++;
        return this;
      }
    }

    return new F();
  };

丢浏览器执行一下,完全冇问题,丢 node 中也一样,欧耶,完美。

最后画几张粗糙的图,简单描述一下这个执行的过程:

因为是链式调用,所以在链上的都会入栈然后执行,额,执行栈少画了 sleep 和 sleepFirst。。。

Hi i am Tony

其中 setTimeout 的 handler 为宏任务,加入marco task队列中;Promise.resolve().then的回调为微任务,加入micro task队列中

然后执行栈被清空,micro task中未清空的任务加入执行栈中被执行,

因为其中有一个 setTimeout,所以把其 handler 加入macro task

前面的微任务执行完就出栈了,这时候macro task中第一个任务入执行栈中进行执行

这个时候如果有 callbacks 就会执行

因为函数内部又有一个 setTimeout,于是把它的 handler 加入macro task

然后清空执行栈,继续执行下一个宏任务

等待了5秒....
I am eating lunch
I am eating dinner

执行栈为空,把最后一个宏任务丢进栈中执行

等待了10秒....
I am eating junk food

最后总结一下,这道题的难点是能否想到用event loop来解决,如果能往这方向去想了,做起来就很简单了。

还有平时不怎么动笔的(比如我),一开始写起文章来就会如鲠在喉,许多内容都写漏了。所以平时有时间就要多动动笔,写写文章,但也不是说东拼西凑一篇,而是真的要有自己的思考和感悟。

最最最后给各位看官老爷多添加一个小需求练练手:

 LazyMan("Tony").eat("lunch").eat("dinner").sleepFirst(5).sleep(10).eat("junk food").eat("healthy food")
 // Hi i am Tony
 // 等待了5秒
 // I am eating lunch
 // I am eating dinner
 // 等待了10秒
 // I am eating junk food
 // I am eating healthy food

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101460.html

相关文章

  • 一道面试题引发的思考 --- Event Loop

    摘要:想必面试题刷的多的同学对下面这道题目不陌生,能够立即回答出输出个,可是你真的懂为什么吗为什么是输出为什么是输出个这两个问题在我脑边萦绕。同步任务都好理解,一个执行完执行下一个。本文只是我对这道面试题的一点思考,有误的地方望批评指正。 想必面试题刷的多的同学对下面这道题目不陌生,能够立即回答出输出10个10,可是你真的懂为什么吗?为什么是输出10?为什么是输出10个10?这两个问题在我脑...

    betacat 评论0 收藏0
  • 「今日头条」前端面试题和思路解析

    摘要:一篇文章和一道面试题最近,有篇名为张图帮你一步步看清和的执行顺序的文章引起了我的关注。作者用一道年今日头条的前端面试题为引子,分步讲解了最终结果的执行原因。从字面意思理解,让我们等等。当前的最新版本,在这里的执行顺序上,的确存在有问题。 一篇文章和一道面试题 最近,有篇名为 《8张图帮你一步步看清 async/await 和 promise 的执行顺序》 的文章引起了我的关注。 作者用...

    宠来也 评论0 收藏0
  • 一道js闭包面试题的学习

    摘要:然后最外层这个函数会返回一个新对象,对象里面有一个属性,名为,而这个属性的值是一个匿名函数,它会返回。 最近看到一条有意思的闭包面试题,但是看到原文的解析,我自己觉得有点迷糊,所以自己重新做一下这条题目。 闭包面试题原题 function fun(n, o) { // ① console.log(o); return { // ② fun: function(m) ...

    plus2047 评论0 收藏0
  • [Java] 关于一道面试题的思考

    摘要:对于这种会退出的情况,数组显然不能像链表一样直接断开,因此采用标记法先生成一个长度为的布尔型数组,用填充。中对整个进行遍历才能得到此时数组中的数量。 文中的速度测试部分,时间是通过简单的 System.currentTimeMillis() 计算得到的, 又由于 Java 的特性,每次测试的结果都不一定相同, 对于低数量级的情况有 ± 20 的浮动,对于高数量级的情况有的能有 ± 10...

    rozbo 评论0 收藏0
  • JavaScript中的算法(附10道面试常见算法题解决方法和思路)

    摘要:中的算法附道面试常见算法题解决方法和思路关注每日一道面试题详解面试过程通常从最初的电话面试开始,然后是现场面试,检查编程技能和文化契合度。值得记住的数组方法有和。一个好的解决方案是使用内置的方法。 JavaScript中的算法(附10道面试常见算法题解决方法和思路) 关注github每日一道面试题详解 Introduction 面试过程通常从最初的电话面试开始,然后是现场面试,检查编程...

    Cruise_Chan 评论0 收藏0

发表评论

0条评论

wow_worktile

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<