本文系你不知道的JS(中册)读书笔记前端
事实上,程序中如今运行的部分和未来运行的部分之间的关系就是 异步编程的核心。ajax
function now() {
return 21;
}
function later() {
answer = answer * 2;//未来执行部分
console.log( "Meaning of life:" answer );//未来执行部分
}
var answer = now();
setTimeout( later, 1000 ); //Meaning of life: 42
复制代码
console.*方法族不是JavaScript正式的一部分,而是**宿主环境**添加到JavaScript中的。
在某些条件下,某些浏览器的console.log(..)并不会把传入的内容当即输出。出现这种状况的主要缘由是,在许多程序(不仅是JavaScript)中,I/O是很是低速的阻塞的部分。因此,(从页面/UI的角度来讲)浏览器在后台异步处理控制台I/O可以提升性能。
复制代码
JavaScript
引擎自己并无时间的概念,只是一个按需执行JavaScript
任意代码片断的环境。“事件”(JavaScrip
t代码执行)调度老是由包含它的环境进行。算法
什么是事件循环?编程
//eventLoop是一个用做队列的数组
//(先进先出)
var eventLoop = [];
var event;
//“永远执行”
while (true) {
//一次tick(循环的每一轮称为一个tick)
if (eventLoop.length > 0) {
event = eventLoop.shift();
//如今,执行下一个事件
try {
event();
}
catch (err) {
reportError(err);
}
}
}
复制代码
必定要清楚,
setTimeout(..)
并无把你的回调函数挂在事件循环队列中。它所作的时设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在将来某个时刻的tick
会摘下并执行这个回调。数组
ES6从本质上改变了在哪里管理事件循环,ES6精确指定了事件循环的工做细节。------> Promise.promise
异步是关于如今和未来的时间间隙,而并行是关于可以同时发生的事情。浏览器
并行计算最多见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不一样的处理器,甚至不一样的计算机上,但多个线程可以共享单个进程的内存。bash
与之相对的是,事件循环把自身的工做分红一个个任务并顺序执行,不容许对共享内存的并行访问和修改。经过分立线程中彼此合做的事件循环,并行和顺序执行能够共存。网络
多线程并行共享内存地址,交错进行,结果不定,但JS不容许(单线程)。多线程
在Javascript的特殊中,函数顺序的不肯定性就是一般所说的竞态条件。
例子:
用户向下滚动加载更多内容至少须要连个独立的“进程”同时运行。“进程为虚拟进程,或者称为任务”,一个“进程”触发onscroll事件并响应,一个“进程”接收Ajax响应。
两个或多个“进程”同时执行就出现了并发,无论组成它们的单个运算是否并行执行(在独立的处理器或处理器核心上同时运行)。能够把并发看做“进程”级(或者任务级)的并行,与运算级的并行相对。
顺序不影响对代码的执行结果。
var res = [];
function response(data) {
res.push( data );
}
//ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
复制代码
协调相互处理竞态
var res = [];
function response(data) {
if( data.url == "http://some.url.1") {
res[0] = data;
}
else if(data.url == "http://some.url.2") {
res[1] = data;
}
}
//ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
复制代码
并发协做是取到一个长期运行的“进程”,并将其分割成多个步骤或多批任务,使得其余并发“进程”有机会将本身的运算插入到事件循环队列中交替运行。
在事件循环的每一个tick中,可能出现的异步动做不会致使一个完整的新事件添加到事件循环队列中,而会在当前tick的任务队列末尾添加一个项目(一个任务)。
事件循环队列和任务队列(插队接着玩)
console.log("A");
setTimeout( function() {
console.log( "B" );
}, 0); //下一个事件循环tick
//理论上的“任务API”
schedule( function(){
console.log( "c" );
schedule(function(){
console.log( "D" );
});
});
// A C D B
复制代码
编译器语句重排几乎就是并发和交互的微型隐喻。
回调函数包裹或者说封装了程序的延续。
listen( "click", function handler(evt) {
setTimeout( function request() {
ajax( "http://some.url.1", function response(text) {
if (text == "hello") {
handler();
}else if (text == "world") {
request();
}
});
}, 500)
});
//也被称为回调地狱或者毁灭金字塔
复制代码
为了不在函数上跳来跳去,可以使用硬编码 ,但 硬编码 确定会使代码更脆弱,由于它并无考虑可能致使步骤执行顺序偏离的异常状况(当其中某个步骤失败报错的状况)。
//A
ajax( "..", function(..) {
//C
});
//B
复制代码
有时候ajax(..)不是你编写的代码,也不在你的直接控制下,多数状况下。它是某个第三方提供的工具。
这时候就会出现控制反转,把本身程序一部分的执行控制交给第三方。
控制反转的修复:
var tracked = false;
analytics.trackPurchase( purchaseData, function() {
if(!tracked) {
tracked = true; //只能回调一次
chargeCreditCard();
displayThankyouPage();
}
})
复制代码
回调表达程序异步和管理并发的两个主要缺陷:缺少顺序性和可信任性。
不把本身的程序的continuation
传给第三方,而是但愿第三方给咱们提供了解其任务什么时候结束的能力,而后由咱们本身的代码来决定下一步作什么。---> Promise
Promise
Promise
就是在快餐店点餐付款以后服务员给你的收据小票,这是一个承诺,你将在后续凭借这张小票拿到你的面包。
从外部看,因为Promise封装了依赖时间的状态 --- 等待底层值的完成或拒绝,因此Promise自己是与时间无关的。一旦Promise
决议,它就永远保持这个状态。
使用回调的话,通知就是任务(foo(..))调用的回调。而使用Promise的话,咱们把这个关系反转了过来,侦听来自Foo(..)的事件,而后在获得通知的时候,根据状况继续。
看一下伪代码
foo(x) {
//开始作点可能耗时的工做
//构造一个listener事件通知处理对象来返回
return listener;
}
var evt = foo( 42 );
evt.on ("completion", function() {
// 能够进行下一步了!
});
evt.on ( "failure", function(err) {
// 啊,foo(..)中出错了
})
复制代码
对回调模式的反转其实就是对反转的反转,或者说反控制反转 --- 把控制还给调用代码
Promise
模式//第一种
function foo(x) {
//开始作一些可能耗时的工做
//构造并返回一个Promise
return new Promise( function(resolve, reject){
//最终调用resolve(..)或者reject(..)
//这是这个promise的决议回调
});
}
var p = foo( 42 );
bar( p );
baz( p );
复制代码
//第二种
function bar() {
// foo(..)确定已经完成,因此执行bar(..)的任务
}
function oopsBar() {
// 啊,foo(..)出错了,因此bar(..)没有执行
}
//对于baz()和oopsBaz()也是同样
var p = foo( 42 );
p.then( bar, oopsBar );
p.then( baz, oopsBaz );
复制代码
要肯定某个值是否是真正的Promise
,用 p instance of Promise
是不够的。由于Promise值多是从其余浏览器窗口(iframe)接收到的。不一样窗口/不一样iframe。此外,库和框架会选择实现本身的Promise
,而不是使用原生的ES6 Promise实现。
识别Promise(或者行为相似于Promise
的东西)就是定义某种称为thenable
的东西,将其定义为任何具备then(..)
方法的对象和函数。
全部值(或其委托),无论是过去的、现存的仍是将来的,都不能拥有
then(..)
函数,无论是有意的仍是无心的;不然这个值在Promise
系统中就会被误认为是一个thenable
,这可能致使难以追踪的bug。
Promise
的信任问题主要是代码是否会引入相似Zalgo(同步异步混乱)这样的反作用。Promise
能够经过回调老是被异步调用来解决这个问题。
Promise
基于任务“插队”
Promise
自己永远不会被决议的解决办法:--- (一种称为竞态的高级抽象机制)
// 用于超时一个Promise工具
function timeoutPromise(delay) {
return new Promise( function(resolve, reject){
setTimeout( function(){
reject( "Timeout" )
}, delay)
})
}
//设置foo()超时
Promise.race([
foo(); //试着开始foo()
timeoutPromise( 3000 ); //给它3秒钟
]).then(
function() {
// foo(..)及时完成
},
function(err){
// 或者foo()被拒绝,或者只是没能按时完成
//查看err来了解是哪一种状况
}
)
复制代码
Promise
将只会接受第一次决议,并默默地忽略任何后续调用。
Promise
至多只能有一个决议值(完成或拒绝)
若是使用多个参数调用resolve(..)
或者reject(..)
,第一个参数以后的全部参数都会被默默忽略。要传递多个值,则须要把它们封装在单个值中传递,好比经过一个数组或对象。
对环境来讲,JavaScript中的函数老是保持其定义所在的做用域的闭包。
Promise
甚至会把JavaScript异常也变成了异步行为,进而极大下降了竞态条件出现的可能,解决潜在的Zalgo风险(同步异步混乱)。
Promise
吗?(可预见/可靠)使用Promise
过滤获得可信任值。
//这么作使得foo(42)返回值可靠
Promise.resolve( foo(42) )
.then( function (){
console.log( V );
} );
复制代码
连石榴能够实现的关键在于如下两个Promise
固有行为特性:
Promise
调用then(..)
,它都会建立并返回一个新的Promise
,能够将其连接起来;Promise
(第一点中的)完成。Promise.resolve(..)
会直接返回接收到的真正Promise
,或展开接收到的thenable
值,并在持续展开thenable
的同时递归地前进。
若是不显式返回一个值,就会隐式返回````undefined,而且这些
promise```仍然会以一样的方式连接在一块儿。
var p = Promise.resolve( 42 );
p.then(
//假设的完成处理函数,若是省略或者传入任何非函数值
//function(v) {
return v;
}
null,
function rejected(err) {
//永远不会到达这里
}
)
复制代码
默认的完成处理函数只是吧接受到的任何传入值传递给下一个步骤(promise
)而已。
then( null, function(err){...} )
只处理拒绝模式 === catch( function(){...} )
;
若是向reject(..) 传入一个Promise/thenable值,它会把这个值原封不动的设置为拒绝理由。后续的拒绝处理函数接收到的是你实际传给reject(..)的那个Promise/thenable,而不是其底层的当即值。
try-catch只能是同步的,没法用于异步代码模式。
Promise采用分离回调风格,一个回调用于完成状况,一个回调用于拒绝状况。
//超时竞赛
//为foo()设定超时
Promise.race([
foo(), //启动foo()
timeoutPromise( 3000 ) //给它三分钟
])
.then(
function() {
//foo()按时完成
},
function(err){
//要么foo()被拒绝,要么只是没能按时完成
//所以要查看err了解具体缘由
}
)
复制代码
//finally
var p = Promise.resolve( 42 );
p.then( something )
.finally( cleanup )
.then( another )
.finally( cleanup );
复制代码
用回调表达异步控制流程的两个关键缺陷:
只有控制生成器的迭代器具备恢复生成器的能力。
生成器为异步代码保持了顺序,同步,阻塞的代码模式。
生成器是一种特殊的函数。
yield..
和next(..)
这一对组合起来,在生成器的执行过程当中构成了一个双向消息传递系统。(启动生成器,即第一个next
,不传值)
每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,经过这个迭代器来控制的是这个生成器的实例。同一个生成器的多个实例能够同时运行,甚至能够彼此交互。
相互交替执行的生成器内部具备同名变量,但这些变量是彼此独立,相互之间没有联系。
生成器做为一种产生值的方式。
var something = (function(){
var nextVal;
return {
//for...of循环须要
//计算属性名:指定一个表达式并用这个表达式的结果做为属性的名称
[Symbol.iterator]: function(){return this;},
//标准迭代器接口方法
next: function() {
if(nextVal === undefined){
nextVal = 1;
}else{
nextVal = (3*nextVal) + 6;
}
return { done: false, value: nextVal };
}
};
})();
something.next().value; //1
something.next().value; //9
something.next().value; //33
something.next().value; //105
复制代码
Object.keys(..)
并不包含来自于[[Prototype]]
链上的属性。而for..in
会包含。
从ES6开始,从一个iterable
中提取迭代器的方法是: iterable
必须支持一个函数,其名称是专门的ES6符号值Symbol.iterator
。调用这个函数时,它会返回一个迭代器。一般每次调用会返回一个全新的迭代器,虽然这一点并非必须的。
严格说来,生成器自己不是iterable
,当你执行一个生成器,就获得了一个迭代器。
function *foo() { .. }
var it = foo()
复制代码
生成器把
while..true
带回了JavaScript
编程的世界。
生成器会在每个yield
处暂停,这意味着不须要闭包也可在调用之间保持变量状态。
生成器内有try..finally
语句,它将老是运行,即便生成器已经外部结束。若是须要清理资源的话,这一点很是有用。
function *something() {
try{
var nextVal;
while (true) {
if(nextVal === undefined){
nextVal = 1;
}else{
nextVal = ( 3*nextVal ) + 6
}
yield nextVal;
}
}
//清理
finally{
console.log( "clean up!");
}
}
复制代码
function foo(x, y) {
ajax(
"http://some.url.1/?x=" + x + "&y=" + y,
function (err, data) {
if(err) {
//向*main()抛出一个错误
it.throw( err );
}else{
//用收到的data回复*main()
it.next();
}
}
);
}
function *main() {
try{
//这里的yield有一个暂停,会等待foo()的完成再赋值给text
var text = yield foo(11, 31);
console.log(text);
}
catch(err) {
console.error( err );
}
}
var it = main();
//这里启动
it.next();
复制代码
把异步做为实现细节抽象了出去,使得咱们能够以同步顺序的形式追踪流程控制:“发出一个Ajax
请求,等它完成以后打印出响应结果。”
生成器的yield
暂停的特性意味着咱们不只可以从异步函数调用获得看似同步的返回值,还能够同步捕获(try..catch
)来自这些异步函数调用的错误。
function *main() {
var x = yield "Hello World";
//永远不会到达这里
console.log( x );
}
var it = main();
it.next();
try{
//*main()会处理这个错误吗?看看吧
it.throw("oops");
}catch(err) {
//不行,没有处理!
console.log( err ); //oops
}
复制代码
Promise
ES6中最完美的世界就是生成器(看似同步的异步代码)和
Promise
(可信任可组合)的结合。
得到Promise
和生成器最大效应的最天然的方法就是yield
出来一个Promise
,而后经过这个Promise
来控制生成器的迭代器。
function foo(x, y){
return request{
"http://some.url.1/?x=" + x + "&y=" + y
};
}
function *main() {
try {
var text = yield foo(11, 31);
console.log( text );
}
catch(err){
console.error( err );
}
}
var it = main();
var p = it.next().value;
//等待Promise p决议
p.then(
function(text) {
console.log( text);
},
function(err) {
it.throw( err )
}
)
复制代码
Promise
的Generator Runner
function run(gen) {
//...
}
function *main() {
//..
}
run( main );
复制代码
ES7: async
与wait
function foo(x, y){
return request{
"http://some.url.1/?x=" + x + "&y=" + y
};
}
//不是生成器函数,而是async函数
async function main() {
try {
//再也不yield出Promise,而是await等待它决议
var text = await foo( 11, 31 );
console.log( text );
}
catch( err ) {
console.error( error );
}
}
main();
复制代码
Promise
并发function *foo() {
//var r1 = yield request( "http://some.url.1" );
//var r2 = yield request( "http://some.url.2" );
<!--并发模式-->
//var p1 = request( "http://some.url.1" );
//var p2 = request( "http://some.url.2" );
//var r1 = yield p1;
//var r2 = yield p2;
var results = yield Promise.all( [
request( "http://some.url.1" );
request( "http://some.url.2" );
] );
//数组结构
var [r1, r2] = results;
var r3 = yield request( "http://some.url.3?v=" + r1 + "&w=" + r2 );
console.log( r3 );
}
//使用前面定义的工具run(..)
run( foo );
复制代码
若是要实现一系列高级流程控制的话,那么很是有用的作法是:把你的Promise
逻辑隐藏在一个只从生成器代码中调用的函数内部。如:
function bar() {
Promise.all( [
baz(..)
.then(..),
Promise.race( [..] )
] )
.then(..)
}
复制代码
yield * 暂停了迭代控制,而不是生成器控制。
yield委托的主要目的是组织代码,以达到与普通函数调用的对称。
yield不仅用于迭代器控制工做,也用于双向信息传递工做。
...
复制代码
和yield委托透明的双向传递信息的方式同样,错误和异常也是双向传递的。
// Request(..)是一个支持Promise的Ajax工具
var res = [];
function *reqData(url) {
var data = yield request( url );
//控制转移
yield;
res.push( data );
}
var it1 = reqData( "http://some.url.1" );
var it2 = reqData( "http://some.url.2" );
var p1 = it1.next();
var p2 = it2.next();
p1.then( function(data){
it1.next(data);
});
p2.then( function(data){
it2.next(data);
});
Promise.all( [p1,p2] )
.then( function(){
it1.next();
it2.next();
} );
复制代码
thunk
)狭义表述: thunk是指一个用于调用另一个函数的函数,没有任何参数。
换句话说,你用一个函数定义封装函数调用,包括须要的任何参数,来定义这个调用的执行,那么这个封装函数就是一个形实转换程序。
function foo(x,y) {
return x + y;
}
function fooThunk() {
return foo( 3, 4 );
}
//未来
console.log( fooThunk() ); //7
复制代码
thunkory (thunk+factory) --- 生成thunk的工厂模式
var fooThunkory = thunkify( foo );
var fooThunk1 = fooThunkory(3,4);
var fooThunk2 = fooThunkory(5,6);
//未来
fooThunk1 (function(sum){
console.log( sum ); //7
})
fooThunk2 (function(sum){
console.log( sum ); //11
})
复制代码
Promise要比裸thunk功能更强,更值得信赖。
JavaScript当前并无任何支持多线程执行的功能。
可是,像浏览器这样的环境,很容易提供多个JavaScript引擎实例,各自运行在本身的线程上,这样你就能够在每一个线程上运行不一样的程序。程序中每个这样独立的多线程部分被成为一个(Web)Worker。这种类型的并行化被成为任务并行,由于其重点在于把程序划分为多个块并发运行。
//专用Worker
var w1 = new Worker( "http://some/url.1/mycoolworker.js" )
复制代码
除了这样指向外部文件的URL的专用Worker,还能够建立一个在想Worker,本质上就是一个存储在单个(二进制)值中的在线文件。
Worker之间以及它们和主程序直接,不会共享任何做用域资源,那会把全部多线程编程的噩梦带到前端领域,而是经过一个基本的事件消息机制相互联系。
w1侦听事件
w1.addEventListener( 'message', function(evt){
//evt.data
})
复制代码
w1发送事件
w1.postMessage( 'sth cool to say');
复制代码
要在建立Worker的程序中终止Worker,能够调用Worker对象上的terminate()。忽然终止Worker线程不会给它任何计划完成它的工做或者清理任何资源。相似于经过关闭浏览器标签页来关闭页面。
在Worker内部是没法访问主程序的任何资源的。这意味着你不能访问它的任何全局变量,也不能访问页面的DOM或者其余资源。但能够执行网络操做(Ajax,WebSockets)以及设定定时器。并且Worker、能够访问几个重要的全局变量和功能的本地复本。如navigator, location, JSON和applicationCache。
能够经过importScripts(..)向Worker同步(阻塞余下的Worker执行,直到文件加载和执行完成)加载额外的JS脚本:
//在Worker内部
importScripts('foo.js', 'bar.js');
复制代码
Web Woker一般应用有:
处理密集型数据计算
大数据集排序
数据处理(压缩,音频分析,图形处理)
高流量网络通讯
复制代码
数据传递
使用解构克隆算法
使用Transferable对象(对象全部权的转移)
复制代码
共享Worker(shareWorker,下降系统的资源使用)
模拟Web Worker(兼容老式浏览器)
单指令数据是一种数据并行方式,与Web Worker的任务并行相对,由于这里的重点实际上再也不是把程序逻辑分红并行的块,而是并行处理数据的多个位。
var v1 = SIMD.float32*4(3.14159, 21.0, 32.3, 55.55);
var v2 = SIMD.float32*4(2.1, 3.2, 4.3, 5.4);
SIMD.float32*4.mul(v1, v2);
//[6.597339, 67.2, 138.89, 299.97]
复制代码
asm.js这个标签是指JavaScript语言中能够高度优化的一个子集。
经过当心避免某些难以优化的机制和模式(垃圾收集、类型强制转换,等),asm.js风格的代码能够被JavaScript引擎识别并进行特别几斤的底层优化。
var a = 42;
//..
var b = a | 0;
//b应该老是被看成32位整型来处理,这样就能够省略强制类型转换追踪。
复制代码
对JavaScript性能影响最大的因素是内存分配,垃圾收集和做用域访问。