异步和性能
事实上,程序中现在运行的部分和将来运行的部分之间的关系就是异步编程的核心。
我们现在发出的一个异步ajax请求,在将来才能得到返回的结果。
从现在到将来的等待最简单的方法(但绝对不是唯一的,甚至也不是最好的!)是使用一个通常称为回调函数的函数。
任何时候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、ajax响应等)时执行,你就是在代码中创建了一个将来执行的块,也由此在这个程序中引入了异步的机制。
在某些条件下,某些浏览器的console.log(...)并不会把传入的内容立即输出。出现这种情况的主要原因是,在许多程序(不只是JavaScript)中,I/O是非常低速的阻塞部分。所以(从页面/UI 的角度来说)浏览器在后台异步处理控制台I/O能够提高性能,这时用户可能根本意识不到其发生。
如果在调试的过程中遇到对象在console.log(...)语句之后被修改,可你却看到了依赖之外的结果,要意识到这可能是I/O的异步化造成的。
强制快照或使用断点:如果遇到这种很少见的情况,最好的选择是在JavaScript调试器中使用断点,而不是依赖控制台输出。次优的方案是吧对象序列化到一个字符串中(JSON.stringify),以强制执行一次快照。
setTimeout并没有把回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把回调函数放在事件循环中,这样在未来的某个时刻会执行这个回调。
如果这个时候事件循环中已经有n个项目,回调就会等待,它得排在其他项目后面-通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么setTimeout定时器的精度可能不高。
异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。
多线程编程是非常复杂的。因为如果不通过特殊的步骤来防止这种中断和交错运行的话,可能会得到出乎意料的、不确定的行为。
- 如果进程间没有相互影响的话,不确定性是完全可以接受的。
var res = {} function foo(data){ res.foo = data } function bar(data){ res.bar = data } ajax('/zjy/a',foo) ajax('/zjy/b',bar)
- 两个并发的进程通过隐含的顺序相互影响,这个顺序有时会被破坏。这种不确定性很有可能就是一个竞态条件bug。
var res = [] function response(data){ res.push(data) } ajax('/zjy/a',response) ajax('/zjy/b',response)
- 协调交互顺序来处理这样的竞态条件。
var res = [] function response(data){ if(data.url == '/zjy/a'){ res[0] = data }else if(data.url == '/zjy/b'){ res[1] = data } } ajax('/zjy/a',response) ajax('/zjy/b',response)
- 门闩思想
var a,b; function foo(x){ a = x * 2 if(a && b){ baz() } } function bar(y){ b = y * 2 if(a && b){ baz() } } function baz(){ console.log(a + b) } ajax('test/a',foo) ajax('test/b',bar)
- 第一个可以通过,第二个(实际上是任何后续的)调用会被忽略。第二名没有意义。
var a; function foo(x){ if(!a){ a = x * 2; baz() } } function bar(x){ if(!a){ a = x / 2; baz() } } function baz(){ console.log(a) } ajax('/zjy/a',foo) ajax('/zjy/b',bar)
- 如果有1000万条数据处理,这样的进程运行时,页面上的其他代码都不能运行,即使是像滚动、输入、按钮点击这样的用户事件。所以要创建一个协作性更强更友好且不会霸占事件循环队列的并发系统,可以异步的处理这些结果。每次处理之后返回事件循环,让其他等待的事件有机会运行。
let res = [] function response(data){ let chunk = data.splice(0,1000) res = res.concat(chunk.map(()=>{ return val * 2 })) if(data.length > 0){ setTimeout(()=>{ response(data) },0) } } ajax('/zjy/a',response) ajax('/zjy/b',response)
这样处理可以确保进程的运行时间很短,即使这意味着需要更多的后续进程,因为事件循环队列的交替运行会提高站点的响应( 性能)。
代码编写的方式(从上到下的模式)和编译后执行的方式之间的联系之间的联系非常脆弱,理解这一点非常重要。
回调是JavaScript这门语言中最基础的异步模式。
回调函数包裹或者说封装了程序中的延续(continuation)。
我们在假装并行执行多个任务时,实际上极有可能是在进行急速的上下文切换。换句话说,我们是在两个或更多任务之间快速连续地来回切换,同时处理每个任务的微小任务。我们切换的如此之快,以至于对外界来说,我们就像是在并行地执行所有任务。
实际上,回调地狱与嵌套缩进几乎没有什么关系;回调的根本不在于代码嵌套产生的问题,但也是影响理解分析代码的原因之一。
手工硬编码(即使包含了硬编码的出错处理)回调的脆弱本性可就远没有这么优雅了。一旦指定(预先计划)了所有的可能事件和路径,代码就会变得非常复杂,以至于无法维护和更新。这才是回调地狱的真正问题所在,嵌套和缩进基本上只是转移注意力的枝节(把戏)而已。
顺序的人脑计划和回调驱动的异步JavaScript代码之间的不匹配只是回调问题的一部分。
控制反转就是把自己程序一部分的执行控制交给某个第三方。
Promise
通过回调表达程序异步和管理并发的两个主要缺陷:缺乏顺序性和可信任性。
如果我们不把自己程序的continuation传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么,这种范式就称为Promise。
链式流
- 每次对Promise调用then,它都会创建并返回一个新的Promise,我们可以将其链接起来;
- 不管从then调用的完成回调(第一个参数)返回的值是什么,它都会被自动设置为被链接Promise(第一点中)的完成 。
var p = Promise.resolve(1) p.then((res)=>{ console.log(res) return res * 2 }).then((res)=>{ console.log(res) return res * 2 }).then((res)=>{ console.log(res) })
吞并错误或异常
var p = new Promise((res,rej)=>{ res(1) }) p.then((res)=>{ zjy() console.log('zjy') },(err)=>{ console.log(err) }).catch((err)=>{ debugger }) p.then().catch() p.then(()=>{},()=>{})
未完待续...