晴明的博客园 GitHub      CodePen      CodeWars     

[js] event loop

并发模型(Concurrency model) 、运行时(runtime)

栈(stack)

函数调用形成了一个栈帧(stack of frames)。

        function foo(b) {
            var a = 10;
            return a + b + 11;
        }

        function bar(x) {
            var y = 3;
            return foo(x * y);
        }

        console.log(bar(7));

当调用bar时,创建了第一个帧(frame),帧中包含了bar的参数和局部变量。当bar调用foo时,第二个帧就被创建,并被压到第一个帧之上,帧中包含了foo的参数和局部变量。当foo返回时,最上层的帧就被弹出栈(剩下bar函数的调用帧 )。当bar返回的时候,栈就空了。

堆(heap)

对象被分配在一个堆中,用以表示一个大部分非结构化的内存区域。

队列(queue)

一个 JavaScript 运行时包含了一个待处理的消息队列。每一个消息都与一个函数相关联。当栈拥有足够内存时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧stack frame)。当栈再次为空的时候,也就意味着消息处理结束。

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。
"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。
所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。而"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

事件循环(event loop)

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

如果当前没有消息queue.waitForMessage会同步地(synchronously )等待消息到达。

执行至完成(Run-to-completion)

每一个消息完整的执行后,其它消息才会被执行。每当一个函数运行时,它就不能被抢占,并且在其他代码运行之前完全运行(且可以修改此函数操作的数据)。
javascript是单线程(single thread)。
这与C语言不同,例如,如果函数在线程中运行,则可以在任何位置终止然后在另一个线程中运行其他代码。

这个模型的一个缺点在于当一个消息需要太长时间才能完成,Web应用无法处理用户的交互,例如点击或滚动。浏览器用“程序需要过长时间运行”的对话框来缓解这个问题。
一个很好的做法是使消息处理缩短,如果可能,将一个消息裁剪成几个消息。

添加消息(Adding messages)

在浏览器里,当一个事件出现且有一个事件监听器被绑定时,消息会被随时添加。如果没有事件监听器,事件会丢失。所以点击一个附带点击事件处理函数的元素会添加一个消息。其它事件亦然。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数,即指定某个任务在主线程最早可得的空闲时间执行。
setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。
因此第二个参数仅仅表示最少的时间,而非确切的时间。

零延迟(Zero delays)

零延迟并不是意味着回调会立即执行。在零延迟调用 setTimeout 时,其并不是过了给定的时间间隔后就马上执行回调函数。其等待的时间基于队列里正在等待的消息数量。在下面的例子中,"this is just a message" 将会在回调 (callback) 获得处理之前输出到控制台,这是因为延迟是要求运行时 (runtime) 处理请求所需的最小时间,但不是有所保证的时间。

        (function () {

            console.log(1);

            setTimeout(function cb() {
                console.log(2);
            });

            console.log(3);

            setTimeout(function cb1() {
                console.log(4);
            }, 0);

            console.log(5);

        })();
        // 1
        // 3
        // 5
        // 2
        // 4

多个运行时互相通信

一个 web worker 或者一个跨域的iframe都有自己的栈,堆和消息队列。两个不同的运行时只能通过 postMessage方法进行通信。如果后者侦听到message事件,则此方法会向其他运行时添加消息。

永不阻塞(Never blocking)

主线程的绿色部分,还是表示运行时间,而橙色部分表示空闲时间。
每当遇到I/O的时候,主线程就让Event Loop线程去通知相应的I/O程序,然后接着往后运行,所以不存在红色的等待时间。
等到I/O程序完成操作,Event Loop线程再把结果返回主线程。
主线程就调用事先设定的回调函数,完成整个任务。

nodejs里的event loop

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
  • timers: 这个阶段执行setTimeout()和setInterval()设定的回调。
  • I/O callbacks: 执行几乎所有的回调,除了close回调,timer的回调,和setImmediate()的回调。
  • idle, prepare: 仅内部使用。
  • poll: 获取新的I/O事件;node会在适当条件下阻塞在这里。
  • check: 执行setImmediate()设定的回调。
  • close callbacks: 执行比如socket.on('close', ...)的回调。

process.nextTick方法可以在当前"执行栈"的尾部,下一次Event Loop(主线程读取"任务队列")之前触发回调函数。即它指定的任务总是发生在所有异步任务之前。

process.nextTick(function A() {
    console.log(1);
    process.nextTick(function B() { 
        console.log(2); 
    });
});

setTimeout(function timeout() {
    console.log('timeout');
}, 0)
  // 1
  // 2
  // timeout

setImmediate方法则是在当前"任务队列"添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行。

setImmediate()被设计在 poll 阶段结束后立即执行回调;
setTimeout()被设计在指定下限时间到达后执行回调。

setImmediate(function A() {
    console.log(1);
    setImmediate(function B() {
        console.log(2);
    });
});

setTimeout(function timeout() {
    console.log('timeout');
}, 0);
//有可能是
//timeout
//1
//2
//也可能是
//1
//timeout
//2

当多次调用 setImmediate() 时,callback 函数会按照它们被创建的顺序依次执行。 每次事件循环迭代都会处理整个回调队列。 如果一个立即定时器是被一个正在执行的回调排入队列的,则该定时器直到下一次事件循环迭代才会被触发。
如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,即随机。
如果两者都不在主模块调用(即在一个 IO circle 中调用),那么setImmediate的回调永远先执行。

task

这里的task是指event loop里的task ,也就是queue里的task。
一般task被称为macrotask(宏任务),
一般包含:

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • postMessage
  • I/O
  • UI rendering

除了macrotask还有microtask(微任务),每一个event loop都有一个microtask队列。microtask也被称作job。
一般包含:

  • process.nextTick
  • Promise.then
  • Object.observe
  • MutationObserver

process.nextTick有的人觉得有异议,node的event loop不能算作microtasks。只从执行顺序来看是microtasks的效果。

Promise.then在不同的浏览器表现可能会有不同,但是普遍的共识是Promise.then属于microtasks队列。

Promise.then是异步执行的,而创建Promise实例是同步的(在stack里)执行的。

        setTimeout(function () {
            console.log(1)
        }, 0);
        new Promise(function executor(resolve) {
            console.log(2);
            for (var i = 0; i < 10000; i++) {
                i == 9999 && resolve();
            }
            console.log(3);
        }).then(function () {
            console.log(4);
        });
        setTimeout(function setTimeout1() {
            console.log(5)
            Promise.resolve().then(function promise2() {
                console.log(6);
            })
        }, 0)
        setTimeout(function () {
            console.log(7)
        }, 0);
        console.log(8);
        //2 3 8 4 1 5 6 7

  1. 处理 HTML 标记并构建 DOM 树。
  2. 处理 CSS 标记并构建 CSSOM 树, 将 DOM 与 CSSOM 合并成一个渲染树。
  3. 根据渲染树来布局,以计算每个节点的几何信息。
  4. 将各个节点绘制到屏幕上。

  • 在一轮event loop中多次修改同一dom,只有最后一次会进行绘制。
  • 渲染更新(Update the rendering)会在event loop中的tasks和microtasks完成后进行,但并不是每轮event loop都会更新渲染,这取决于是否修改了dom和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧(60hz)的时间内(时间并不确定,因为浏览器每秒的帧数总在波动,16.7ms只是估算并不准确)修改了多处dom,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。
  • 如果希望在每轮event loop都即时呈现变动,可以使用requestAnimationFrame。
posted @ 2017-10-20 19:35  晴明桑  阅读(146)  评论(0编辑  收藏  举报