js 线程 之 异步编程(1)

221 阅读6分钟

----欢迎查看我的博客----

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