笔记——异步和性能

85 阅读10分钟

分块的程序

可以把JavaScript写在单个.js文件中,但是这个程序是由多个块构成的,这些块中只有一个是现在执行,其余的则会在将来执行,最常见的块单位是函数。

事件循环

一定要清楚,setTimeout()并没有把你的回调函数直接挂在事件循环队列中,它所做的是设定一个定时器,当定时器到时候,环境会把你的回调函数放在事件循环中。这也解释了为什么setTimeout定时器的精度可能不高的原型。

并行线程

异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。 并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行,在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。

非交互

如果进程间没有相互影响的话,不确定性是完全可以接受的。

交互

如果进程之间存在交互,就需要对它们的交互进行协调以避免竞态出现 协调方法:

  1. 上门 if(a&&b)。我们虽然不能确定a和b的到达顺序,但是可以等到它们两个都准备好再进一步打开门
  2. 门闩,只要第一个。

并发协作

要创建一个协作性更强更友好且不会霸占事件循环队列的并发系统,可以异步的批处理这些结果。每次处理之后返回事件循环,让其他等待事件有机会运行。

var res = []

function response(data) {
  var chunk = data.splice(0, 1000)
  res = res.concat(
    chunk.map(val => val*2)
  )

  if(data.length > 0){
    setTimeout(() => {
      response(data)
    }, 0)
  }
}

ajax("url1", response)
ajax("url2", response)

把数据集合放在最多包含1000条项目的块中,这样就确保了“进程”运行时间会很短,即使这意味着需要更多地后续“进程”,因为事件循环队列的交替运行会提高app的响应(性能)

任务

它是挂在事件循环队列的每个tick之后的一个队列。在事件循环的每个tick中,可能出现的异步动作不会导致一个完整的新事件添加到事件循环队列中,而会在当前tick的任务队列末尾添加一个任务。

回调

回调是JavaScript中最基础的异步模式。

//a
ajax("url", () => {
  //c
})
//b
先执行a和c。然后是一段时间不确定的停顿,在未来的某个时刻,如果ajax调用完成,程序就会从停下的位置继续执行后半部分。

顺序的大脑

我们在假装并行执行多个任务时,实际上极有可能是在进行快速的上下文切换。切换的足够快,就能让外界感觉我们在并行的执行所有的任务。

Promise

通过回调表达程序异步和管理并发的两个主要缺陷: 缺乏顺序性和可信任性。

回调未调用解决途径:

function timeoutPromise(delay) {
  return new Promise( function(resolve,reject){
    setTimeout(() => {
      reject('timeout')
    }, delay)
  })
}

Promise.race( [foo(), timeoutPromise(3000)]).then(
  () => {
    //foo()及时完成
  },
  err => {
    //foo被拒绝,或未按时完成
  }
)

链式调用,如果步骤二需要等待步骤一异步完成一些事情的时候:

function delay(time){
  return new Promise((resolve,reject) => {
    setTimeout(resolve, time)
  })
}

delay(50).then(step1 => {
  console.log(1)
  return delay(100)
})
.then(step2 => {
  console.log(2)
  return delay(200)
})
.then(step3 => {
  console.log(3)
})
.then()....

//第 2 步出错后,第 3 步的拒绝处理函数会捕捉到这个错误。拒绝处理函数的返回值(这段 代码中是 42),如果有的话,会用来完成交给下一个步骤(第 4 步)的 promise,这样,这 个链现在就回到了完成状态。

使链式流程控制可行的 Promise 固有特性。
• 调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回。
• 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)
Promise 就相应地决议。
• 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议
值是什么,都会成为当前 then(..) 返回的链接 Promise 的决议值。

术语: 决议(resolve),完成(fulfill)以及拒绝(reject)

错误处理

try...catch无法用于异步。

Promise模式

Promise.all([...])

协调多个并发Promise的运行

Promise.race([...])

只要第一个到达的Promiese,竞态。

  1. 超时竞赛
Promise.race([
  foo(),
  timeoutPromise(3000)
]).then(() => {
  //foo()按时完成
},err => {
  //3s超时报err
})

  1. finally

这个回调在Promise决议后总是被调用,并且允许执行任何必要的清理工作。

all和race的变体

  1. none([]),类似all,不过完成和拒绝情况互换了。拒绝转换为完成值。
  2. any([]),只需要完成一个而不是全部。
  3. first([])
  4. last([])

并发迭代

有时候可以利用同步数组的方法(如forEach(), map(),some())对Promise任务进行同步迭代。但如果这些Promise任务是异步的,我们就需要异步工具来进行并发执行。

if(!Promise.map){
  Promise.map = function(vals, cb) {
    //一个等待所有map的promise的新promise
    return Promise.all(
      //map把值数组转换为promise数组
      vals.map(val => {
        //用val异步map之后决议的新promise替换val
        return new Promise( resolve => {
          cb(val, resolve)
        })
      })
    )
  }
}

var p1 = Promise.resolve( 21 );
var p2 = Promise.resolve( 42 );
var p3 = Promise.reject( "Oops" );
// 把列表中的值加倍,即使是在Promise中 
Promise.map( [p1,p2,p3], function(pr,done){
// 保证这一条本身是一个Promise 
  Promise.resolve( pr ).then(
// 提取值作为v 
    function(v){
// map完成的v到新值
      done( v * 2 );
    },
// 或者map到promise拒绝消息
      done );
})
.then(
   function(vals){
        console.log( vals );         // [42,84,"Oops"]
    } );

Promise.resolve()和Promise.reject()

创建一个已被拒绝的Promise的快捷方式是Promise.reject()

var p1 = new Promise((resolve, reject) => {
  reject('oo')
})
//等价于
var p2 = Promise.reject('oo')

生成器

一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入期间。不过ES6引入的生成器却打破了这个假定。

var x = 1
function *foo() {
  x++
  yield //暂停
  console.log('x:',x)
}
function bar() {
  x++
}

//构造一个迭代器来控制这个生成器
var it = foo()

//启动foo()
it.next()
x   //2
bar()
x   //3
it.next()   //x:3

解析:

  1. it=foo()运算并没有执行生成器*foo(),只是构造了一个迭代器iterator
  2. 第一个it.next()起订了生成器*foo(),并运行了x++
  3. *foo()在oyield语句处暂停,第一个it.next()调用结束
  4. 调用bar(),x++
  5. 最后的it.next()调用了从暂停处恢复了生成器*foo()的执行。

输入和输出

  1. 迭代消息传递
function *foo(x) {
  var y = x * (yield)
  return y
}
var it = foo(6)
it.next()
var res = it.next(7)   //把7传回为被暂停的yield表达式的结果
res.value     //42

多个迭代器

同一个生成器的多个实例可以同时运行,甚至可以彼此交互:

function *foo() {
  var x = yield 2
  z++
  var y = yield (x * z)
  console.log(x,y,z)
}
var z = 1

var it1 = foo()
var it2 = foo()
var val1 = it1.next().value     //2
var val2 = it2.next().value     //2

val1 = it1.next(val2 * 10).value    //40    x:20   z:2
val2 = it2.next(val1 * 5).value     //600   x:200  z:3

it1.next(val2 / 2)        //y: 300    20   300 3
it2.next(val1 / 4)        //y: 10     200  10  3

构建一个控制迭代器的辅助函数:

function step(gen) {
  var it = gen()
  var last
  return function(){
    //不过yield出来的是什么,下一次都把它原样传回去。
    last = it.next(last).value
  }
}

生产者与迭代器

产生一系列值,其中每个值与前面一个有特定的关系 使用函数闭包的版本:

var gimmeSomething = (function(){
  var nextVal

  return function(){
    if(nextVal === undefined){
      nexVal = 1
    }else{
      nextVal = 3 * nextVal
    }
    return nextVal
  }
})()

gimmeSomething()   //1
gimmeSomething()   //3
gimmeSomething()   //9

通过迭代器解决:

var something = (function(){
  var nextVal
  return {
    [Symbol.iterator]: function(){return this},

    next: function(){
      if(nextVal === undefined){
        nextVal = 1
      }else{
        nextVal = 3 * nextVal
      }
      return {done: false, value: nextValue}
    }
  }
})()
something.next().value      //1
something.next().value      //3
something.next().value      //9

//ES6新增的for...of循环可以自动迭代标准迭代器
for(var v of something){
  console.log(v)
  if(v > 10){
    break
  }
}
//1   3   9

iterable(可迭代)

从一个iterale中提取迭代器的方法:iterable必须支持一个函数,器名称是专门的ES6符号值Symbol.iterator。调用这个函数时,它会返回一个迭代器。

var a = [1,3,5,7,9]   //a就是一个iterable

var it = a[Symbol.iterator]()

it.next().value   //1
it.next().value   //3
it.next().value   //5

生成器迭代器

可以把生成器看做一个值的生产者,我们通过迭代器接口的next()调用一次提取出一个值。

function *something() {
  var nextVal
  while(true){
    if(nextVal === undefined){
      nextVal = 1
    }else{
      nextVal = 3 * nextVal
    }
    yield nextVal
  }
}
for(var v of something()){  //调用something()生成器得到它的迭代器
  console.log(v)
  if(v > 10){
    break
  }
}

如果生成器内有try...finally语句,它将总是运行,即使生成器已经外部结束,如果需要清理资源很有用:

 function *something() {
    try {
      var nextVal;
      while (true) {
        if (nextVal === undefined) {
          nextVal = 1;
        }else {
          nextVal = (3 * nextVal) + 6;
        }
        yield nextVal;
      }
    }
  // 清理子句 
    finally {
        console.log( "cleaning up!" );
    }
  }

调用 it.return(..) 之后,它会立即终止生成器,这当然会运行 finally 语句。另外,它 还会把返回的value设置为传入return(..)的内容,这也就是"Hello World"被传出 去的过程。现在我们也不需要包含 break 语句了,因为生成器的迭代器已经被设置为 done:true,所以 for..of 循环会在下一个迭代终止。

异步迭代生成器

Web Worker

把你的程序分为两个部分:一部分运行在主UI线程下,另外一部分运行在另一个完全独立的线程中 这样的架构可能引出的问题:

  1. 需要了解在独立的线程运行是否意味值它可以并行运行。
  2. 这两个部分能否共享作用域和资源?如果能的话,你会想要知道它们将如何实现通信。

Web Worker是浏览器(即宿主环境)的功能,JavaScript当前并没有任何支持多线程执行的功能。

从JavaScript主程序中,可以这样实例化一个Worker

var w1 = new Worker("http://xxx/xxx.js")

这个url应该指向一个js文件的位置。这个文件将被加载到一个worker中,然后浏览器启动一个独立的线程,让这个文件在这个线程中作为独立的程序运行。

Worker环境

在Worker内部是无法访问主程序的任何资源的。这意味着你不能访问它的任何全局变量,也不能访问页面的DOM或者其他资源,这是一个完全独立的线程!

可以通过importScripts('xxx.js')来向Worker加载额外的JS脚本,这些脚本加载是同步的。

Web Worker通常应用场景:

  1. 处理密集型数学计算
  2. 大数据集排序
  3. 数据处理(压缩,音频分析,图像处理等)
  4. 高流量网络通信

数据传递

上述的应用场景有个共同点就是需在线程之间通过事件机制传递大量的信息,可能是双向的。

共享Worker

SIMD

单指令多数据(SIMD)是一种数据并行的方式,与Web Worker的任务并行相对,这里的重点不再是把程序逻辑分成并行的块,而是并行处理数据的多个位。

性能测试与调优

性能测试

var start = (new Date()).getTime()   //或者Date.now()
var end = (new Date()).getTime()

console.log("duration:", end - start)

这是一种常用的方案,但是有些问题,可信度低,精度低。

Benchmark.js

环境为王

引擎优化

尾递归优化

function foo(x){
  return x
}

function bar(y){
  return foo(y+1)     //尾调用
}

function baz(){
  return 1 + bar(40)   //非尾调用
}

baz()

调用一个新的函数需要额外的一块预留内存来管理调用栈,称为栈帧。所以前面的代码一般会为上面每个函数保留一个栈帧。 然鹅,如果支持TCO的引擎能够意识到foo(y+1)调用位于尾部,这意味着bar()基本完成了,那么在调用foo()时,它就不需要创建一个新的栈帧,而是可以重用已有的bar()的栈帧,这样不仅速度更快,也更节省内存。