js基础系列(四)——执行机制

1,373 阅读8分钟

1、try-catch-finally内部机制

try语句包含了由一个或者多个语句组成的try块, 和至少一个catch块或者一个finally块的其中一个,或者两个兼有, 下面是三种形式的try声明:

  1. try...catch
  2. try...finally
  3. try...catch...finally

catch子句包含try块中抛出异常时要执行的语句。也就是,你想让try语句中的内容成功, 如果没成功,你想控制接下来发生的事情,这时你可以在catch语句中实现。 如果在try块中有任何一个语句(或者从try块中调用的函数)抛出异常,控制立即转向catch子句。如果在try块中没有异常抛出,会跳过catch子句。

finally子句在try块和catch块之后执行但是在下一个try声明之前执行。无论是否有异常抛出或捕获它总是执行。

你可以嵌套一个或者更多的try语句。如果内部的try语句没有catch子句,那么将会进入包裹它的try语句的catch子句。

function test (){
    try {
        return 0
    } catch (e) {
        return 1
    }
}
test() // 0
function test1 (){
    try {
        throw new Error()
        return 0 // 这里不会执行到
    } catch (e) {
        return 1
    }
}
test1() //1

try执行完后,才执行finally。或者try中产生了异常,会执行catch中的代码,最后执行finally的代码。但是切记:finally的代码,是在try或者catch代码块的return之前执行。

还有一点注意,try和catch块return之前,会执行finally代码。然后执行finally之前,会暂时保存需要return的信息,执行完finally后,再return保存的信息。(如下:)

function testReturn() {
    const i = 1;
    try {
        i++;
        console.log("try:" + i);  //try:2
        return i;
    } catch (e) {
        i++;
       console.log("catch:" + i);        return i;
    } finally {
        i++;
       console.log("finally:" + i);  //finally:3    }
}
testReturn();  //2  
//注意:这里返回i是try中保存的信息,在finally执行完之后直接返回,并不会参与finally中的计算

如果从 finally 块中返回一个值,那么这个值将会成为整个 try-catch-finally 的返回值,无论是否有 return 语句在 try 和 catch 中。这包括在 catch 块里抛出的异常。(如下:)

function test() {
    try {
        throw new Error('can not find it1');
        return 1;
    } catch (err) {
        throw new Error('can not find it2');
        return 2;
    } finally {
        return 3;
    }
}

console.log(test()); // 3 
//finally中有返回值,所以无论try和catch中是否有返回值,会将finally中的返回值返回。

2、宏任务和微任务

众所周知js是单线程,但js是可以执行同步和异步任务的,同步的任务众人皆知是按照顺序去执行的; 而异步任务的执行,是有一个优先级的顺序的,包括了宏任务(macrotasks)和微任务(microtasks)

宏任务: setTimeout, setInterval, setImmediate, I/O, UI rendering
微任务: Promises.(then catch finally), process.nextTick, MutationObserver

宏任务和微任务的区别在于在事件循环机制中,执行的机制不同:每次执行栈执行完所有的同步任务后,会在任务队列中取出异步任务,先将所有微任务执行完成后,才会执行宏任务(微任务会在宏任务之前执行)。


具体的操作步骤如下:

  1. 从宏任务的头部取出一个任务执行;
  2. 执行过程中若遇到微任务则将其添加到微任务的队列中;
  3. 宏任务执行完毕后,微任务的队列中是否存在任务,若存在,则挨个儿出去执行,直到执行完毕;
  4. GUI 渲染;
  5. 回到步骤 1,直到宏任务执行完毕;

setTimeout(() => console.log(1))

new Promise((resolve) => {
    resolve()
    console.log(2)
}).then(() => {
    console.log(3)
})

console.log(4)

先执行同步代码,new Promise在实例化过程中过程中执行的代码是同步的,所以先打印出2,然后是最后一行的4,这时所有同步任务已经执行完成了。接着执行微任务也就是.then()里的回调函数,所以打印4,最后执行宏函数setTimeout,最后打印出1。

所以,上述代码的输出结果为2 4 3 1。

3、事件循环机制EventLoop

在正式说EventLoop之前我先来点铺垫,帮助大家更好的理解。

(1)JS的单线程:
js单线程意思就是同一时间只能做一件事,按照先后顺序执行。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊. 。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? 所以,为了避免复杂性,从一诞生,JavaScript就是单线程。

(2)主线程和任务队列

单线程就意味着,所有任务需要排队。所有任务可以分成两种,一种是同步任务synchronous),另一种是异步任务(asynchronous)。

同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。


在了解了上面的知识以及宏任务和微任务之后,我们就可以来看EventLoop啦~

EventLoop

JS的执行机制是:

  首先判断JS是同步还是异步,同步就进入主进程,异步就进入eventtable

  异步任务在eventtable中注册函数,当满足触发条件后,被推入eventqueue

  同步任务进入主线程后一直执行,直到主线程空闲时,才会去eventqueue中查看是否有可执行的异步任务,如果有就推入主进程中

  以上三步循环执行,这就是eventloop。

我们来看个例子:

console.log(1);
setTimeout(function() {
    console.log(2);
}, 0);
new Promise(function(resolve) {
    console.log(3);
    resolve(Date.now());
}).then(function() {
    console.log(4);
});
console.log(5);
setTimeout(function() {
    new Promise(function(resolve) {
        console.log(6);
        resolve(Date.now());
    }).then(function() {
        console.log(7);
    });
}, 0);

1)执行 log(1),输出 1;

2)遇到 setTimeout,将回调的代码 log(2)添加到宏任务中等待执行;

3)执行 console.log(3),将 then 中的 log(4)添加到微任务中;

4) 执行 log(5),输出 5;

5)遇到 setTimeout,将回调的代码 log(6, 7)添加到宏任务中;

6)宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,存在一个微任务 log(4)(在步骤 3 中添加的),执行输出 4; 

7)取出下一个宏任务 log(2)执行,输出 2;

8)宏任务的一个任务执行完毕,查看微任务队列中是否存在任务,不存在;

9)取出下一个宏任务执行,执行 log(6),将 then 中的 log(7)添加到微任务中;

10)宏任务执行完毕,存在一个微任务 log(7)(在步骤 9 中添加的),执行输出 7;

 因此,最终的输出顺序为:1, 3, 5, 4, 2, 6, 7;  

4、Node与浏览器EventLoop的差异

在说差异之前还是先来了解一下Node中的EventLoop

NodeJs中的eventloop

执行机制:

1)times(计时期)

2)I/O callbacks处理流、网络、tcp错误callback

3)idle、prepare node内部使用

4)poll轮询,执行poll中的I/O队列,检查定时器是否到时

5)check(检查)存放setImmediate回调

6)close callbacks关闭回调 socket.on('close')

执行过程:

1)执行js中的同步代码

2)执行microtask微任务,先执行NextTickQueue中的所有任务,在执行Other Microtask Queue中的所有任务。

3)开始执行macrotask宏任务,共6个阶段,从第1个阶段开始,执行相应的每个阶段macrotask中的所有任务。注意:这里是所有每个阶段宏任务队列的所有任务(在浏览器中的eventloop只取宏任务中的第一个任务出来执行),每一个阶段的macrotask任务执行完毕后,开始执行微任务,也就是步骤2

4)Times queue -> 步骤2 -> I/O queue -> 步骤2 -> check queue -> 步骤2 -> close callback queue -> 步骤2 -> timers queue

5)这就是node的eventloop的简化版


浏览器和Node中的eventloop的区别

1)实现机制不同

2)nodejs可以理解成4个宏任务和2个微任务队列,但执行宏任务时,有6个步骤

3)Nodejs中,先执行全局的js代码,执行完同步代码调用栈,清空后,先从微任务队列NextTick queue中依次取出所有的任务,放入调用栈执行;再从微任务队列中的other microtask queue中依次取出所有任务放入调用栈执行。然后开始宏任务的6个阶段,每个阶段都将该宏任务队列中的所有任务都取出来执行,每个宏任务阶段执行完毕后,开始执行微任务,在开始执行下一阶段的宏任务,以此构建事件循环。

4)Macrotask包括:setTimeout\setInterval\setImmediate\requestAnimation\I/O\UI rendering

5)Microtask包括:process.nextTick(Node)\promise.then()\Object.observe\Mutation.observer

5、如何在保证页面运行流畅的情况下处理海量数据

1)借用事件循环机制,用异步操作实现

2)使用web worker开辟一条独立的子线程(这个后面会讲到,不急哈)

结语

关于JavaScript的基础系列就总结到这里,系列中没有提及语法和API,关于这些都可以在官方文档中直接查阅学习,这里就不把API拿出来一一去说了。

完结,撒花~~~