You Don't Know JS: Async & Performance(第一章, 异步:now & later)


Chapter 1: Asynchrony: Now & Later

 

在一门语言中,比如JavaScript, 最重要但仍然常常被误解的编程部分是如何在一个完整的时间周期表示和操作程序行为。

 

这是关于当你的程序正在部分运行,其他部分等待运行。--这之间的gap。

 

mind the gap!(比如在subway door and the platform)

 

异步编程就是这个核心: now and later parts of your program

 

在JS的发展初级, callback function足够用了。但是JS继续在scope和complexity方向成长,为了满足不断扩展的要求(作为第一类编程语言,它运行在浏览器,服务器,已经每个它们之间的设备),开发者需要更强大的并且合理的功能。

 

后几章我们会探索各种异步的JS技术。

但此时我们将不得不更深度地理解什么是异步asynchrony, 它如何在JS操作!

 


 

A Program in Chunks

可以把JS程序 写在一个.js文件内,不过程序是由不同的部分组成,有的部分现在执行,有的则等待执行。

常见的chunk单位是函数。

 

比如发送请求并⌛️处理响应的数据。这之间有一个gap, 最简单的等待的方式是使用一个函数,即回调函数:

ajax( "http://some.url.1", function myCallbackFunction(data){

    console.log( data ); // Yay, I gots me some `data`!

} );

 

 

再看一个:

function now() {
    return 21;
}

function later() {
    answer = answer * 2;                //黄色部分是later
    console.log( "Meaning of life:", answer );
}

var answer = now();

setTimeout( later, 1000 ); // Meaning of life: 42

 

分为立即执行的now chunk, 和1000毫秒(1秒)后的later chunk

setTimeout()建立了一个事件(a timeout)在1秒之后发生。所以later()函数在1秒之后执行。

 

当你把一段代码放入一个函数并直到它被执行,用来响应某个事件,你正在创建一个later chunk。

因此asynchrony来到了你的程序!

 

Async Console

注意console.log也是异步的,不同的浏览器consol I/O可能不同导致不同的输出console.log(..)。

所以在debugging时,需要当心!! 

var a = {
    index: 1
};

// later
console.log( a ); // ??

// even later
a.index++;

这个例子: 有可能console.log(a) 在a.index++执行后,才执行!

 

注意:debug最好是用断点来代替console.log输出。 或者把问题对象转化为JSON格式(JSON.stringify)


  

Event Loop

尽管JS允许异步代码,但直到ES6,JS本身没有任何直接的异步概念内建在JS。

The JS engine itself has never done anything more than execute a single chunk of your program at any given moment, when asked to.

The JS engine doesn't run in isolation. It runs insidehosting environment, which is for most developers the typical web browser.

Over the last several years (but by no means exclusively), JS has expanded beyond the browser into other environments, such as servers, via things like Node.js. 

In fact, JavaScript gets embedded into all kinds of devices these days, from robots to lightbulbs. 

But the one common "thread" (that's a not-so-subtle asynchronous joke, for what it's worth) of all these environments is that they have a mechanism in them that handles executing multiple chunks of your program over time, at each moment invoking the JS engine, called the "event loop."

so, 比如,当你的JS程序发出Ajax请求向服务器取数据,你在一个函数内建立响应代码(callback),并且JS engine 告诉hosting environment:“喂,我将暂停执行,但是当你完成网络请求,并有数据,请调用回调函数”。

浏览器于是建立监听从网络来的响应,并当它有something给你,它根据时间表安排回调函数,把它插入event loop中执行。

 

什么是event loop?

伪代码演示:

// `eventLoop` is an array that acts as a queue队列,先排队的先办理
var eventLoop = [ ];
var event;

// keep going "forever"
while (true) {
    // perform a "tick"
    if (eventLoop.length > 0) {
        // get the next event in the queue
        event = eventLoop.shift();

        // now, execute the next event
        try {
            event();
        }
        catch (err) {
            reportError(err);
        }
    }
}

 

持续的运行♻️, 每次迭代(iteration重复)循环被称为,tick!

每一个tick, 如果一个事件在queue中等待,执行它,拿出队列。这些事件就是你的函数回调。

 

需要重点⚠️:

setTimeout()不会把你的回调函数放到event loop queue中。它只是建立一个timer。当timer expires, 环境会把你的回调放入event loop,等待, 当tick到它,就会执行它。

所以,真实的回调函数发生的时间比setTimeout()设置的时间要多一个等待时间。

 

⚠️!

ES6改变了event loop queue被管理的模式。ES6明确了event loop 如何工作。这意味着技术上它属于JS engine 的 范围, 而不是hosting environment。

这么做的主要原因在Promises内有介绍。(见第3章),因为需要直接的管理在event loop queue的时间表操作。

 


 

 

Parallel Threading 平行线程

async和parallel是 2个不同的事情。

记住,async是关于现在后之后的这个缺口gap。而parallel是关于事情能够被同步发生。

 

最普通的parallel计算工具是processes 和 threads。Processes and threads 可以独立地执行,也可以同步地执行。on separate processors, or even separate computers, 但是多个线程可以共享一个单独进程的内存

An event loop, by contrast, breaks its work into tasks and executes them in serial, disallowing parallel access and changes to shared memory. Parallelism and "serialism" can 共存 in the form of cooperating event loops in separate threads.

一个event loop, 不允许parallel access 和改变共享的内存。平行和序列可以在event loop这个合作的event loops形式下在各自的线程内同时存在。

我的理解:

同时执行2个线程,那么共享的内存的数据被这2个线程使用会导致数据混乱,造成结果的不确定。

所以,JS不会共享data across 线程。 

 

Run-to-Completion

JS是单线程的,所以可能是先执行foo,等foo执行完成后,再执行bar。也肯能相反。

var a = 20;

function foo() {
    a = a + 1;
}

function bar() {
    a = a * 2;
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

 

 这样的不确定问题无法解决!

因此,ES6介绍了一个thing,可以指定先执行哪个。

 

Concurrency并发

an example of event, actions etc happening at the same time!

让我们想象一个网页,它显示一个更新状态的列表,当用户滚动列表时,加载这个列表。为了让这样的功能实现,至少需要2个独立的'processes'被同步地执行

 

当用户滚动页面到底部时, 激活第一个‘process1’响应 onscroll事件(发出Ajax请求,请求新的list内容)。

第二个‘process2’将会接收Ajax 响应(用于把数据渲染到网页)。

 

当用户的滚动足够快,你会看到2个以上的onscroll事件在刚完成第一个响应的返回和处理时就fire了。

比如: 滚动请求4和5,6,发生的足够块,以至于响应4和滚动请求6同时激活。

onscroll, request 1
onscroll, request 2          response 1
onscroll, request 3          response 2
response 3
onscroll, request 4
onscroll, request 5
onscroll, request 6          response 4
onscroll, request 7
response 6
response 5
response 7

 

并发就是2个或更多的‘processes’在相同的时间段同步的执行,不考虑是否他们的内部的event操作在平行(在相同的一瞬间)发生你可以认为并发是在‘process’层次的平行(task-level),不是操作层次的平行 (operation-level)。

 

本例子:

process1在请求1开始,在请求7结束。

process2在响应1开始,在响应7结束。它们是同步执行的(并发concurrency)。

 

因此就造成了以下可能:

一个滚动时间和一个Ajax响应事件可能在相同的时刻等待被处理。比如请求2和响应1。

 

但是,JS是单线程的,在event loop queue中,只能一次处理一个事件。

这就造成了不确定性nondeterminism。

于是event loop queue可能是这样排队的:(也可能是另外的排序)

onscroll, request 1   <--- Process 1 starts
onscroll, request 2
response 1            <--- Process 2 starts
onscroll, request 3
response 2
response 3
onscroll, request 4
onscroll, request 5
onscroll, request 6
response 4
onscroll, request 7   <--- Process 1 finishes
response 6            //出错❌了!!!
response 5
response 7            <--- Process 2 finishes

 

2个process并发运行(task-level parallel),但它们内部的独立事件需要在event loop queue中排队处理。

 

Noninteracting 不交互

如果两个'process'不会产生交互(互相不干扰),则nondeterminism非确定性可以完全接受。

var res = {};

function foo(results) {
    res.foo = results;
}

function bar(results) {
    res.bar = results;
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );

 

foo, bar互不干扰,谁在event loop queue的前面,无所谓。

 

Interaction

更常见的,并发"processes"是必须要交互的, 非直接的通过作用域和/或者 DOM。

当如此交互发生 , 你需要coordinate协调这些交互,防止"race conditions",

看👆👆前面案例标记黄色的部分!! 

 

这个例子就是在函数作用域内的代码res.push,导致res存入的数据的顺序不确定。

var res = [];

function response(data) {
    res.push( data );
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

 

 

还有多个并发函数调用,并且通过共享的DOM互相影响.比如2个process,一个更新<div>的内容,一个更新style和attributes of the div。一旦内容更新,就更新这个元素的样式。这2个process,就是交互的,必须有先后处理的顺序。

 

Cooperation协调 cooperative concurrency.协调性的并发

目标是把一个长process分成多step/块 ,以便其他并发进程有机会把他们的操作插入event loop queue.

 例子:

一个Ajax响应处理,它需要计算一个非常大的数组并返回结果:

var res = [];

// `response(..)` receives array of results from the Ajax call
function response(data) {
    // add onto existing `res` array
    res = res.concat(
        // make a new transformed array with all `data` values doubled
        data.map( function(val){
            return val * 2;
        } )
    );
}

// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );

 

假如ajax( "http://some.url.1", response )得到的结果首先返回。这是非常庞大的记录,需要花费数秒甚至更长时间。

当这个process运行时,网页上不会发生任何事情,其他的响应不会调用, UI不会更新,甚至事件如滚动,点击等等。这很糟糕!

所以,制造一个更协调的并发系统,友好的不全占住event loop queue。

你可以把结果使用异步批次处理:asynchronous batches。

修改:

function response(data) {
    // let's just do 1000 at a time,splice()切割数据,得到前1000条记录。
    var chunk = data.splice( 0, 1000 );

    // add onto existing `res` array
    res = res.concat(
        // make a new transformed array with all `chunk` values doubled
        chunk.map( function(val){
            return val * 2;
        } )
    );

    // anything left to process?
    if (data.length > 0) {
        // async schedule next batch
        setTimeout( function(){
            response( data );
        }, 0 );
    }
}

 

这样响应函数执行的时间就缩短了。

 


 

Jobs

As of ES6, there's a new concept layered on top of the event loop queue, called the "Job queue.

The most likely exposure you'll have to it is with the asynchronous behavior of Promises (see Chapter 3).

So, the best way to think about this that I've found is that the "Job queue" is a queue hanging off the end of every tick in the event loop queue.

一个队列,这个队列放置到每一次tick的event loop queue的后面。

暗含异步的行动,发生在a tick of the event loop期间, 不会让一个全新的时间添加到event loop queue, 而是添加一个item(job)到当前的tick's Job queue。

我的理解:

假设有一个event loop queue. a1-b1-a2-b2-b3。有一个job, 保证这个job在当前这个queue的b3执行完成后,立即执行job。而不是添加新的event: a3, b4等。

console.log( "A" );

setTimeout( function(){
    console.log( "B" );     这是later,不是当前的event loop tick
}, 0 );

// theoretical "Job API"
schedule( function(){
    console.log( "C" );

    schedule( function(){
        console.log( "D" );
    } );
} );

 

执行结果是A C D B

 


 

Statement Ordering

未理解。

 


Review

一个Js程序总是会分成多块chunks, 第一个chunk 现在运行,下一个chunk稍后运行,在响应一个事件。

无论何时有事件运行, event loop 运行直到queue变空。每个事件循环的迭代是一个tick.

用户交互行为, I/O, timers入event队。

 

single-threaded(one-at-a-time) event loop queue

任何时刻queue中每次只处理一个event。 当一个事件执行时,它可能直接或间接引起一个或多个后续的事件。

 

并发Concurrency是当2个或多个事件链积极的插入event loop queue, 从一个高层次看,它们看起来是同步运行的。

 

因此需对并发的process做一些对交互行为的协调。 例如确保顺序,或者防止race conditions。

这些进程也可以通过把它们分成更小的chunks, 来允许其他的process插入event loop queue!

 

posted @ 2018-10-10 10:26  Mr-chen  阅读(354)  评论(0编辑  收藏  举报