异步操作参考问题

172 阅读7分钟

1 如何理解JS的单线程模型?单线程模型优劣势?

理解: 单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。

注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。

JavaScript 之所以采用单线程,而不是多线程,跟历史有关系。JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。 如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制? 所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变。 优点: 这种模式的好处是实现起来比较简单,执行环境相对单纯; 缺点: 坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。 JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞。

2 JavaScript 引擎都有哪些线程?

js引擎是单线程的。为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质。

web worker示例:
    <script>
    let w;
    function startWorker() {
        if(typeof(Worker) !== "undefined") {
            if(typeof(w) == "undefined") {
                w = new Worker("demo_workers.js");
            }
            //从 web worker 发生和接收消息。向 web worker 添加一个 "onmessage" 事件监听器接收 web worker返回的信息
            w.onmessage = function(event) {   
                document.getElementById("result").innerHTML = event.data;
            };
        } else {
            document.getElementById("result").innerHTML = "Sorry! No Web Worker support.";
        }
    }
    function stopWorker() { 
        w.terminate();//如需终止 web worker,并释放浏览器/计算机资源,请使用 terminate() 方法
        w = undefined;//复用Web Worker。如果您把 worker 变量设置为 undefined,在其被终止后,可以重复使用该代码
    }
    </script>

由于 web worker 位于外部文件中,它们无法访问window对象。

3 什么是同步任务和异步任务?

同步任务: 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

异步任务: 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。 排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。

4 如何理解JS引擎提供的任务队列?

任务队列(task queue)里面是各种需要当前程序处理的异步任务。 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。 排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。 之所以提供任务队列,是因为 IO 操作(输入输出)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。但是CPU基本是空闲状态,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。 等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。

5 什么是事件循环?

引擎在不停地检查异步任务有没有结果,能不能进入主线程,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。

6 异步操作都有哪些模式?

回调函数
事件监听 发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern) 事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。

7 什么是串行执行和并行执行?

串行执行
编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。

并行执行
流程控制函数也可以是并行执行,即所有异步任务同时执行。

8 如何理解观察者模式?

事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执。

9 JavaScript 提供定时执行代码功能的相关函数有哪些?

setTimeout
setInterval
clearTimeout
clearInterval

10 setTimeout 和 setInterval 的区别?

setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。

11 实现一个 debounce 函数?

function debounce(callback, delay) {
    let timer;
    return function () {
        const that = this;
        const args = arguments;
        clearTiemout(timer);
        timer = setTimeout(() => {
            callback.apply(that, args)
        }, delay)
    }
}

12 setTimeout(f, 0) 的用途有哪些?

setTimeout(f, 0)会在下一轮事件循环一开始就执行。

用途:调整事件发生顺序

用户自定义的回调函数,通常在浏览器的默认动作之前触发

`document.getElementById('input-box').onkeypress = function (event) {
  this.value = this.value.toUpperCase();
}`

上面代码想在用户每次输入文本后,立即将字符转为大写。 但是实际上,它只能将本次输入前的字符转为大写,因为浏览器此时还没接收到新的文本,所以this.value取不到最新输入的那个字符。只有用setTimeout改写,上面的代码才能发挥作用。

document.getElementById('input-box').onkeypress = function() {
  var self = this;
  setTimeout(function() {
    self.value = self.value.toUpperCase();
  }, 0);
}

setTimeout(f, 0)实际上意味着,将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被放到几个小部分,分别放到setTimeout(f, 0)里面执行。

优劣比较:

var div = document.getElementsByTagName('div')[0];
// 写法一
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
  div.style.backgroundColor = '#' + i.toString(16);
}
// 写法二
var timer;
var i = 0x100000;
function func() {
  timer = setTimeout(func, 0);
  div.style.backgroundColor = '#' + i.toString(16);
  if (i++ == 0xFFFFFF) clearTimeout(timer);
}
timer = setTimeout(func, 0);

同样是改变背景颜色,写法一会阻塞浏览器,写法二不会

13 Promise 对象 与普通对象的区别与联系?

联系: 都是对象。
区别:

promise对象:
    对象的状态不受外界影响。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。原型上有then方法和catch方法。
普通对象:
    没有特别限制。

14 Promise 实例具有哪三种状态?

fulfilled
rejected
pending

15 then() 用法有哪些?

用来添加回调函数。
then方法可以链式使用。

16 Promise 的优点和缺点?

优点: 让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数; 再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。 所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。

缺点: 编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。

17 如何理解JS中的微任务和宏任务?

事件循环是通过任务队列进行协调的。js分为同步任务和异步任务,同步任务在主线程上执行,形成一个执行栈。异步任务放在任务队列,分为宏任务和微任务。 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

宏任务:(macro)task 可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。 浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...

常见宏任务:
    setTimeout
    setInterval
    setImmediate//在浏览器完成后面的其他语句后,就立刻执行这个回调函数

微任务:(micro)task 可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染。 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)。

常见微任务:
    Promise.then
    Object.observe
    MutaionObserver
    process.nextTick(Node.js 环境)

经典题目:

    setTimeout (() => { console.log(4)})
    new Promise (resolve => {
        resolve()
        console.log(1)
    }).then (() => {
       console.log(3)
    })
     console.log(2)
 //输出1234

参考地址:wangdoc.com/javascript/…