js 线程 之 异步编程(1)
js 单线程
相信大家玩 js 很久了,Js语言的执行环境是 “ 单线程 ”。什么是单 线程,就是当一个任务完成之后才会有下一个任务 。 举个栗子,你在奶茶店买奶茶 ,只有一个 奶茶妹妹,客人们都在排队,问你喝什么,你告诉她,她下好单再叫下一个,以此类推,这就是 单线程的同步。再比如到你了,你没选好喝什么,她先问下一位喝什么,等你想好了,再问你喝什么,这就是 单线程的异步。多个奶茶妹妹同事问客人 ,就是多线程。
js 为什么是单线程的。
为什么 JavaScript 不能有多个 线程 呢?这样能提高效率啊。办事快啊, 我能撩到好几个奶茶妹妹,但这个和 js 的作用有关 ,当初 js 只是为了服务于 游览器 ,用户交互,所以设计了单 线程 , 如果多 线程 你想有多么麻烦,你操作了 dom 是修改 ,同时另外的线程执行了 删除 dom 这时候游览器怎么选择呢?为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主 线程 控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。所以我们任然 坚持说 js 是单 线程 的。
异步同步
既然 js 是 单线程 的,那么按照我们的之前的栗子, 你去买奶茶,你没想好要喝什么,一直占着位置,估计后面的兄弟们对你肯定不爽了,开始骂娘了 ,所以 这时候我们的 奶茶妹 就很机智,然你在后面等等,先问下一个要喝什么。于是,所有任务可以分成两种。
同步任务(synchronous)
在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
执行过程大概是这样:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)一旦"执行栈"中的当前任务执行完毕,就会执行下一个任务,进入执行栈,开始执行。
(3)主线程不断重复上面的(2)
异步任务(asynchronous)
不进入主线程、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
执行过程大概是这样:
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。开始执行。
(4)主线程不断重复上面的(3)
浏览器不是单线程的
虽然JS运行在浏览器中,是单线程的,每个window一个JS线程,但浏览器不是单线程的,可能有如下线程:
javascript引擎线程
这个就是我们常说的单线程
界面渲染线程
渲染dom页面的进程
浏览器事件触发线程
有可能触发多个事件。例如 keyup ,clickup 等。
Http请求线程
你可以发起多个请求,至于最多能发送多少请求,我们后面会提到。
如果js是单线程的,那么谁去轮询大的Event loop事件队列?答案是浏览器会有单独的线程去处理这个队列。
js 的 异步编程
异步编程有这么几种方式:
ES 6以前:
- 回调函数
- 事件监听(事件发布/订阅)
ES 6:
- Generator函数(协程coroutine)
- Promise对象
ES 7:
- async和await
* 回调函数
//一个定时器
function timer(time, callback){
setTimeout(function(){
callback();
}, time);
};
timer(3000, function(){
console.log(123);
})
这个最常见,也是我们经常写的。非常简单。一个 setTimeout 然后执行后面的函数。至于 setTimeout 的话 ,我们后面再讲。
* 事件监听(发布/订阅)
发布—订阅模式可以广泛应用于异步编程中,这是一种替代传递回调函数的方案。比如,我们可以订阅 ajax 请求的 error 、 succ 等事件。 或者如果想在动画的每一帧完成之后做一些事情,那我们可以订阅一个事件,然后在动画的每一帧完成之后发布这个事件。在异步编程中使用发布—订阅模式,我们就无需过多关注对象在异步运行期间的内部状态,而只需要订阅感兴趣的事件发生点。
这里不详细说了,因为后面我们会把这种设计模式单独拿出来讲。这里举例一个事件监听,其实是一样的:
element.addEventListener("click",function(){
alert("clicked");
})
* Generator函数
这个函数我用的很少,所以我打算后面专门也来研究一下这个东西。
形式上,Generator 函数是一个普通函数,但是有两个特征。
function关键字与函数名之间有一个星号;
函数体内部使用yield语句,定义不同的内部状态
我们来大概看看这个东西
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,我们可以通过调用 next 方法,使得指针移向下一个状态。
* Promise对象
这个就比较熟了,之前有提到。不太熟悉的童鞋们可以看这篇文章: 你就需要这篇文章,带你搞懂实战的 Promise 对象 ; 后面我们也会将他拿出来,看看我们的 JavaScript 原生如何实现 promise 对象。
* async和await
这个也比较熟悉,之前的博客也提到过 ,了解 JavaScript ES7 的 async / await , ; 后面我们也会将他拿出来,看看我们的 JavaScript 原生如何实现 async / await 对象。
结语
以上就是我们的异步编程方式,我们后面会慢慢将这些东西拿出来讲解 :
- setTimeout
- 发布订阅
- 原生(es2015)实现 promiss
- Generator
- 原生(es2015)实现 async / await