分块的程序
JavaScript中的程序是由多个块组成的
程序执行时,只有一个块是现在执行,其他的块都是将来再执行
最简单常见的块单位是函数
我们在写程序的时候可能会经常遇到一个问题
程序中某个块我们并不希望在现在执行的块结束后就立刻执行
比如获取后台的数据的块执行完之后,我们希望当后台的数据接收完毕之后再执行打印出数据的块
而不是一发送完请求就立刻打印
回调函数是最简单的实现方式,但不是唯一的实现方式
Ajax也是个很好的实现方式,ajax发送一个异步请求,然后在将来获取到结果了之后再执行打印数据的块
(尽可能的不要使用同步Ajax请求,因为这样会锁定浏览器UI(按钮,菜单,滚动条等),并阻塞所有的用户交互)
任何时候只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器,鼠标点击,Ajax响应等)时执行,就等于在代码中创建了一个将来的块,也由此引入了异步机制
异步控制台
console.*方法组没有什么规范或者需求来指定它们如何工作
控制台并不是JavaScript的一部分,而是由宿主环境(比如说浏览器)添加到JavaScript中的
因此不同的浏览器和JavaScript环境可以根据自己的意愿来实现
尤其要提出,console.log(..)并不会将传入的内容立即输出
因为I/O是非常低速的阻塞部分,浏览器是在后台异步处理控制台I/O的
但是大部分情况下并不会有什么影响
当你发现控制台输出的语句并不是你期望的时候
你可能需要考虑下是不是因为这个原因导致console.log(..)推迟执行了
最好的选择是使用断点来调试,或者用JSON.stringify(..)进行一次"快照"
事件循环
JavaScript引擎并不是独立运行的,它允许在宿主环境中(如web浏览器或者Node.js等)
这些环境都提供了一种机制来处理程序中多个块的执行,而且执行每块时调用JavaScript引擎
这种机制就是事件循环
也就是说JavaScript引擎本身没有时间概念,只是一个按需执行JavaScript代码的环境
事件调度是由包含它的环境(比如浏览器)来进行的
那么,什么是事件循环
简单的来说,可以把事件循环看做一个队列(先进先出)
当一个事件执行完后,从队列摘下一个事件并执行,这些事件就是你的回调函数
setTimeout(..)定时器的精度可能不是很高,正是因为setTimeout(..)函数并没有将回调函数挂在事件队列中
而是设定了一个定时器,当定时器到时后,环境会把你的回调函数放在事件循环中
当这时事件循环中有很多个事件在排队等待的时候,这个回调函数得排在其他项目后面
因此执行时间可能会更长
事实上,直到ES6,JavaScript才真正内建有直接的异步概念,在此之前都是由宿主环境来管理的
这个改变一个主要原因是,ES6中Promise的引入精确指定了事件循环的工作细节
并行线程
异步和并行经常会混为一谈,但事实上它们的意义完全不同
异步是关于现在和将来的时间间隙,而并行是能够同时发生的事情
并行最常见的计算工具就是进程和线程
进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上
但多个线程能够共享单个进程的内存
而事件循环不允许对共享内存的并行(同时)访问和修改
完整运行
由于JavaScript的单线程特性
事件中的代码具有完整性
一旦某个事件开始运行了,这个事件中所有的代码都会在下一个事件中任意代码运行之前完成
这称为完整运行特性
竞态条件
JavaScript是单线程编程(不跨线程共享数据),而且有完整运行的特性
这意味着不会出现多线程编程中交错运行导致的不确定行为(语句(表达式运算)顺序级别的不确定性)
但这不表示JavaScript总是确定性的
比如两个ajax请求的回调函数的先后执行顺序
这种函数的不确定性就被称为竞态条件(函数(事件)级别的不确定性)