js中的单线程与异步矛盾吗

时间:2022-05-29 11:43:44

1.js的单线程与异步

Javascript语言的执行环境是”单线程”(single thread)。
所谓”单线程”,就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推。

这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段Javascript代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)。

“同步模式”就是上一段的模式,后一个任务等待前一个任务结束,然后再执行,程序的执行顺序与任务的排列顺序是一致的、同步的;”异步模式”则完全不同,每一个任务有一个或多个回调函数(callback),前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行,所以程序的执行顺序与任务的排列顺序是不一致的、异步的。

其实,单线程和异步确实不能同时成为一个语言的特性。js选择了成为单线程的语言,所以它本身不可能是异步的,但js的宿主环境(比如浏览器,Node)是多线程的,宿主环境通过某种方式(事件驱动)使得js具备了异步的属性。

2.javascript的异步编程就是指

// 操作。。。
onxxx = function(){//当发生什么事如(onclick , onmouseover等等。。)
// 做什么操作
}
// 操作

我们要做很多类似这样的监听器,当发生什么事的时候,通知我,我好知道什么时候来做什么事大概就是这样。

浏览器为耗时任务开辟了另外的线程,主要包括http请求线程浏览器定时触发器浏览器事件触发线程,这些任务是异步的。下图描述了异步的流程


js中的单线程与异步矛盾吗

这里waiting queue也就是经常说的 callback queue,excute queue也就是经常说的stack

  • 说说任务队列

异步任务完成后,主线程怎么知道呢?答案就是回调函数,整个程序是事件驱动的,每个事件都会绑定相应的回调函数,举个栗子,有段代码设置了一个定时器

setTimeout(function(){
console.log(time is out);
},50);

执行这段代码的时候,浏览器异步执行计时操作,当50ms到了后,会触发定时事件,这个时候,就会把回调函数放到任务队列里。整个程序就是通过这样的一个个事件驱动起来的。
所以说,js是一直是单线程的,浏览器才是实现异步的那个家伙。

  • 说说主线程

js一直在做一个工作,就是从任务队列里提取任务,放到主线程里执行。图中在waiting queue中进行 event loop,然后将任务放入 excute queue中执行。堆(heap)和栈(stack)共同组成了js主线程,函数的执行就是通过进栈和出栈实现的。

异步就是将代码添加到执行队列末尾。

console.log(1);
setTimeout(function(){
console.log(2);
},0);//注意这里我写了间隔0秒
console.log(3);
//如果按照多线程的话,这样写有可能输出123也有可能输出132.但是js的单线程的,将异步代码放到了末尾执行,所以结果一定是132

3.Javascript异步编程的4种方法

如何利用浏览器的异步机制

我们可以利用浏览器给我们开放的这几个窗口,浏览器定时器线程和事件触发线程是好利用的,网络请求线程不适合我们使用。以下是四种用定时器实现的异步编程

  • a 回调函数

    什么是回调函数

在JavaScript中,回调函数具体的定义为:函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A。我们就说++作为参数的函数A++叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数。

回调函数是一个作为变量传递给另外一个函数的函数,它在主体函数执行完之后执行。

因此callback 不一定用于异步,一般同步(阻塞)的场景下也经常用到回调,比如要求执行某些操作后执行回调函数。
一个同步(阻塞)中使用回调的例子,目的是在func1代码执行完成后执行func2。

var func1=function(callback){
//do something.
(callback && typeof(callback) === "function") && callback();
}

func1(func2);
var func2=function(){
}

回调函数,一般在同步情境下是最后执行的,而在异步情境下有可能不执行,因为事件没有被触发或者条件不满足。

假定有两个函数f1和f2,后者等待前者的执行结果。
  f1();
  f2();
如果f1是一个很耗时的任务,可以考虑改写f1,把f2写成f1的回调函数。
  function f1(callback){
    setTimeout(function () {
      // f1的任务代码
      callback();
    }, 1000);
  }
执行代码就变成下面这样:
 f1(f2);

采用这种方式,我们把同步操作变成了异步操作,f1不会堵塞程序运行,相当于先执行程序的主要逻辑,将耗时的操作推迟执行。

js代码会至上而下一条线执行下去,但是有时候我们需要等到一个操作结束之后再进行下一个操作,这时候就需要用到回调函数。

  • b 事件监听

另一种思路是采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

还是以f1和f2为例。首先,为f1绑定一个事件(这里采用的jQuery的写法)。
  f1.on('done', f2);

上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写:

function f1(){
  setTimeout(function () {
    // f1的任务代码
     f1.trigger('done');
  }, 1000);
}
f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。

当调用的时候,报错:f1没有on方法,为此,为函数添加on方法

var eventMap = {};
Function.prototype.on = function (eventName, callback) {
if (!eventMap[eventName]) {
eventMap[eventName] = [];
}


eventMap[eventName].push(callback);

console.log('callback has been binded');
}
  • c 发布/订阅

我们假定,存在一个”信号中心”,某个任务执行完成,就向信号中心”发布”(publish)一个信号,其他任务可以向信号中心”订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称”++观察者模式++”(observer pattern)。

这个模式有多种实现,下面采用的是Ben Alman的Tiny Pub/Sub,这是jQuery的一个插件。
首先,f2向”信号中心”jQuery订阅”done”信号。

jQuery.subscribe("done", f2);

然后,f1进行如下改写:

 function f1(){
   setTimeout(function () {
     // f1的任务代码
      jQuery.publish("done");
   }, 1000);
 }

jQuery.publish(“done”)的意思是,f1执行完成后,向”信号中心”jQuery发布”done”信号,从而引发f2的执行。
此外,f2完成执行后,也可以取消订阅(unsubscribe)。

jQuery.unsubscribe("done", f2);
  • d Promises对象

简单说,它的思想是,每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数。比如,f1的回调函数f2,可以写成:

f1().then(f2);

f1要进行如下改写(这里使用的是jQuery的实现):

  function f1(){
    var dfd = $.Deferred();
    setTimeout(function () {
      // f1的任务代码
      dfd.resolve();
    }, 500);
    return dfd.promise;
  }

这样写的优点在于,回调函数变成了链式写法,程序的流程可以看得很清楚,而且有一整套的配套方法,可以实现许多强大的功能。
比如,指定多个回调函数:

  f1().then(f2).then(f3);
再比如,指定发生错误时的回调函数:
  f1().then(f2).fail(f3);

而且,它还有一个前面三种方法都没有的好处:如果一个任务已经完成,再添加回调函数,该回调函数会立即执行。所以,你不用担心是否错过了某个事件或信号。这种方法的缺点就是编写和理解,都相对比较难。