由一道'恶搞'排序引发的思考

440 阅读4分钟

作者:随便@毛豆前端

背景

​ 偶然在技术群里看到一个恶搞的排序。第一眼看着觉得这个排序算法是恶搞的,但是恶搞的排序算法却给出了正确的排序结果。不禁让人想去进一步了解它的运行原理。

Event Loop介绍

​ 我们知道JavaScript是单线程的,程序运行时,只有一个线程存在。单线程的JavaScript一段一段的执行,前面的执行完了,再执行后面的。但是如果遇到一个耗时很久的任务,比如接口请求、I/O操作,此时后面的任务如果一直等待,不仅浪费资源,并且页面会卡住,交互也很差。为了解决这个问题,JavaScript将任务分为同步和异步任务,来进行不同的处理。

​ JavaScript在执行时,会将同步任务在执行栈(execution context stack)中,按照顺序在主线程上执行,前面的任务执行完了,再执行后面的。遇到异步任务不会停下来等待,而是将其挂起,继续执行当前栈中的同步任务。当异步任务有返回结果时,将异步任务执行完成后的结果加入任务队列(task queue),通常任务队列中存放的都是异步完成后的回调函数。

​ 当执行栈中的任务完成后,空闲的主线程就会读取任务队列中是否有任务。如果有,主线程就会把最先进入任务队列的任务加入到执行栈中,执行栈中的任务执行完成后,主线程便又会去查询任务队列中的任务,并读取到执行栈中去执行。这个过程是循环往复的,这便是Event Loop,事件循环。

​ 网上有一张流传很广的图对这一过程进行了总结:

​ 由图可知,JavaScript在运行时产生堆和栈,ajax、setTimeout等异步任务被挂起,异步任务返回结果加入到任务队列,主线程会循环往复的读取任务队列中的任务,并加入执行栈中执行。

"排序"代码分析

​ OK,说了那么多,我们终于看到跟这个排序算法有关系的关键词了:setTimeout。作为一个异步任务,在循环数组的过程中,每次根据当前值,延时往任务队列添加回调函数,填充数据到新的数组中。由于值有大小的分别,小的值延时时间短,就会被先添加到任务队列中,最终利用Event Loop的时间差,达到了排序的目的。

​ 不考虑延时的问题,单纯从代码来看,完成整个排序只是用了一遍循环,这个排序代码的时间复杂度其实是O(n)的。并且从目前的例子来看:排序过程中,相同大小的数据在排序前后不会改变顺序,是稳定的排序算法。我们甚至可以把这段代码封装修改一下,通过一个回调来得到排序后的数组:

function eventLoopSort(list, callback) {
  var newList = [];
  var sum = 0;
  list.forEach(item => {
    setTimeout(() => {
      newList.push(item);
    }, item * 100)
    sum += item;
  });
  setTimeout(() => {
    callback && callback(newList);
  }, sum * 100);
}

var list = [1, 1, 4, 6, 2, 3, 9, 8, 7];
eventLoopSort(list, data => {
  console.log(data) // [1, 1, 2, 3, 4, 6, 7, 8, 9]
})

问题总结

​ 一本正经的讲了这么多,还是改变不了这个排序是恶搞,不可以应用到实际代码中的现实。因为在面对大量数据时,除了setTimeout延时较长之外,这个排序还是会出错的。考虑到代码本身执行也是需要耗时的,在面对大量数据时,前面数据执行时间较长,长到可以抵消延时的时间差的时候,排序就会完全出错了。

​ 最后,第一次写出这段"玩笑"代码的人,一定对浏览器的运行机制非常的了解。这也是我想通过这篇文章表达的意思,希望能通过这个恶搞,有趣的形式,对你的学习有所帮助。