异步
异步控制台
- 对于 consloe 方法簇的工作原理是没有规定的,它也不是 js 语言的一部分,而是由浏览器等宿主环境添加到语言中的。
- 某些条件下 I/O 操作是非常低速的操作,他们会阻塞页面运行,所以有时会遇到 consloe 异步执行的情况。
- 如果遇到这种很罕见的情况,最好是使用断点来代替控制台,或者保存一个序列化副本到字符串然后打印字符串。
事件循环
- js 引擎并没有时间的概念,只是一个按需执行代码块的环境。而事件与调度是由调用 js 引擎的环境决定的,例如 web 浏览器等。
- es6 开始才本质的改变了 js 压根没有异步的情况,主要是因为 promise 的引入(因为 promise 要求对事件循环队列有精细的控制),这才使得代码的调用进入了 js 引擎的管理范围而不是全权让宿主环境来控制。
并行线程
不是并行多任务,而是快速的上下文切换。
- 完整运行,由于 js 的单线程特性,函数的同步代码具有原子性,在这些代码完成之前不会被其他任务打断。
- js 一次只能处理一个事件,多个“线程”的各个事件循环执行是并发的一种形式(这里的线程不是计算机意义上的线程,是两个逻辑线,例如一个页面交互线和一个请求数据线)
- 非交互线程,两个线程没有相互影响,他们处于竞争态造成执行顺序的不确定性是可以接受的
- 交互线程,两个线程有交流关系,则需要进行协调来避免竞态的出现
- tips:通过吧一个大任务拆分成小任务再使用 setTimeout(.. 0) 来加入到任务队列来避免一个任务占有太长时间而阻塞页面。要注意的是 setTimeout 是在有机会的时候插入时间,连续的 setTimeout 插入的任务顺序可能并不是预想的。
任务
es6中出现了新概念建立在事件循环队列上,任务队列|job queue (其实就是俗称的微任务)
回调、promise 等其他
详见文档promise手撕
生成器
- 打破完整性运行的新东西!
通过 function *fun(){} 来定义一个生成器,在 functuon 关键字和函数名之间加星号。
通过 yield 来暂停执行并返回,通过调用 next 方法来从上一次暂停的地方继续执行。
生成器函数的调用顾名思义是返回一个迭代器对象,可以被赋值给一个变量名。
next 返回一个对象,具有 value 属性为返回的值。
迭代消息传递
function *foo(x){
const y = x * (yield);
return y;
}
const it = foo( 6 );
it.next();
const res = it.next( 7 );
res.value; // 42
第一个 next 是启动这个生成器,当运行到 yield 时会暂停,等待第二个 next 被调用并传入参数作为 yield 的值。
function *foo(x){
const y = x * (yield "hellow");
return y;
}
const it = foo( 6 );
let res = it.next();
res.value; // hellow
res = it.next( 7 );
res.value; // 42
与第一个例子到区别为,第一次 next 进行了一次从迭代器向外的信息传递。
生成器产生值
- 这是他本来的用途,顾名思义生成器。
- array 有默认的生成器来生成一个迭代器,next 会依次返回其中的元素
iterable
- 可迭代的,即一个对象,其中一个值是可以迭代的迭代器对象
- 从一个可迭代对象提取迭代器的方法是在内部实现一个函数,名称是专用的符号值 Symbol.iterator。调用这个函数时返回一个迭代器,可以是全新的页可以是单例的。
const a = [1,2,3];
const it = a[Symbol.iterator]();
it.next().value;...
- 用 for ..of 来遍历一个迭代器,或者 of 后面调用一个生成器
生成器把 while..true 带回了编程
Web Worker
- 这是宿主环境的功能,与 js 语言几乎没有关系。js 语言本身当前没有任何支持多线程的功能。
- 使用 new Worker('url') 来实例化一个 webWorker,url 是一个 js 文件。浏览器会启动一个独立的线程让这个文件在这个线程中作为独立的程序运行。
- woker 和主程序之间没有任何的共享作用域和资源(防止多线程噩梦进入前端领域),而是通过一个很基本的消息机制相互联系。
- woker 对象是一个事件监听者和触发者,通过 addEventListener 和 postMessage 来收发。
- 通过 worker 实现的拒绝服务攻击:生成上百个 worker!系统可以控制你创建多少个进程,但是无法预测和保证你可以开多少个线程。
w1.addEventListener( "message", function(evt){
// evt.data
} );
w1.postMessage( "something cool to say" );
- worker 环境
- woker 不能访问主程序的任何资源,但是可以访问一些重要的全局变量和功能的本地副本例如 location。也可以执行网络操作。
- 有一些讨论已经涉及吧 canvas 实例暴露给 worker,让 worker 执行一些图形处理,这对高性能游戏和其他类似应用是很有用的。(目前浏览器不支持,但可能以后会有)
- worker 数据传递
- 早期的 worker 线程之间使用事件监听机制传递信息可能是双向的,唯一的方法是将数据序列化为字符串,这样传递之前转字符串,接受后在转回原数据意味着两杯的内存使用和性能损耗。
- 现在有了结构克隆算法,甚至可以处理要复制的对象有循环引用的情况。虽然仍然要使用双倍内存但是性能不需要双倍字符串转换损失,大部分主流浏览器都支持这种方案。
- 对于大数据集而言,可以使用 Transferable 对象,这种对象可以转移他的所有权到另一个 worker。任何实现了这个接口的数据结构都支持这种传输方式。
- 共享 worker
- new SharedWorker("url");
- 需要用唯一标识符端口(port)来做消息发出方标识|w1.port.postMessage| w1.port.start()
- 共享 worker 必须处理“connect”事件,这个事件就是端口对象的来源。
addEventListener( "connect", function(evt){ // 这个连接分配的端口 var port = evt.ports[0]; port.addEventListener( "message", function(evt){ // .. port.postMessage( .. ); // .. } ); // 初始化端口连接 port.start(); } );
性能测试与调优
性能测试
- 传统的 new date 方式|完全错误
- 精度:报告的时间为 0,可能是运行时间小于 1ms,但是有些平台更新定时器是以更长的时间作为单位比如 15ms|比如早期 IE 版本。
- 稳定:影响执行时间的因素太多了,环境因素完全处于 js 不可控的领域,获取 date 可能也需要时间。
- 通过重复|不是太捞
- 通过重复来求均值稳定数据
- 重复次数不是固定的,运行足够多的数据直到执行时间趋紧某个稳定的值
- Benchmark.js
- 自己查怎么用,这里举个例子
function foo(){}
let bench = new Benchmark('foo test',foo,{config});
//bench包含每秒运算数,出错边界和样本方差
关注测试环境
- 确定性能优化的初心,不是玩文字游戏
- 人脑处理信息最快轮转速度大概是 13ms,++a 和 a++ 相差几 ns 的原因就得出拿快几纳秒 A 代替 B 是不成立的。不要执迷于微观性能。
- 引擎优化
- 一个处理语句,比如 Number('12'),引擎可能因为后面没有用到这个结构而压根没运行,可能在上一次转换字符串“12”时有一些副本记录,鉴于现代引擎的复杂程度这并不在我目前的考虑范围内。
jsPerf
在控制变量中,性能测试需要在各种不同的环境下得到结果,jsPerf 应运而生,你可以把测试内容放在一个 url 然后让别人打开,此时测试结果会被收集并持久化图形化。(众包测试)
不是所有的引擎都类似
- 曾经社区有一场研究 v8 工作模式的运动,通过便携裁剪过的代码来最大程度利用 v8 的工作模式,通过这样的努力可能会获得令人吃惊的性能优化。
- 不要从一个函数到另一个函数传递 arguments | 把 try..catch 分离到单独的函数,引擎优化 try..carch 有一些困难,分离出去可以让其包含的代码可以优化。
- 但是对具体技巧的关注缺陷是明显的,你无法预测你的代码是否会在另一种环境下运行,而这种环境你的优化技巧反而是很不好的行为,又或是引擎的发展改变,所以在针对具体引擎的细节优化时要万分小心。
所以我们应该更加关注优化的大局,而不是担心细微的性能差别。
- 防止过早优化
- 非关键路径上的优化是一件耗时耗力但是成效并不显著的行为,性价比不高
- 但是对于关键路径上,甚至是一个阻塞 UI 的任务,对他的优化就是值得的
尾调用优化
ES6引入了一个性能领域的特殊要求:尾调用优化。也就是一个函数中调用另一个函数,这个调用应该出现在函数的结尾,在这个调用结束后除了可能返回一个值之外没有其他事情做。
因为调用一个新函数需要一块额外的内存来管理调用栈,支持 TCO 的引擎如果意识到是一个尾调用,则不会新调用的函数闯进新的栈帧,而是直接使用父级函数的栈帧,不仅快,也节省内存。
尤其是递归!(很好理解吧)