Javascript学习笔记-异步和回调

Javascript异步和回调.png

1. 异步

Javascript中程序是分块执行的,块的最常见单位是函数,在Javascript引擎执行的时候,通常最少存在一个现在正在执行的块和一个将要执行的块,对于异步的分块执行,最简单的方式就是回调。

function f1() {
    console.log(1);
}
function f2() {
  console.log(2);
}
// 当开始执行f1的时候,f1就是当前执行块,setTimeout(f1,0)和f2()就是将来要执行的块
f1(); 
// 使用了setTimeout并将f1作为了回调函数,创建了一个异步的块
setTimeout(f1, 0);
f2();

1.1 事件循环

事件循环是Javascript引擎用于处理多块程序执行的机制。主要实现是在Javascript中存在一个事件循环队列,然后会将需要运行的块加入到该队列中,之后等待触发运行,同时存在一个无限循环监听该队列的变化,并触发方法执行。

// 先进先出的队列
var eventLoop = [];
var event;
while(true) {
  if (eventLoop.length > 0) {
      // 获取队列中的某个事件
      event = eventLoop.shift();
      // 事件执行
      try {
        event();
      } catch(e) {
        doError(e);
      }
  }
}

1.2 并行

并行是时间点的概念,是指某个时间点多个事情同时执行,通常多线程才存在并行的能力,事情的运行结果存在不确定性,即两个或者多个线程同时操作同一份数据,那么数据结果就会不确定。

var a = 1;
function f1() {
  a = a + 1;
}
function f2() {
  a = a + 2;
}
// ajax是某个异步函数
ajax(f1);
ajax(f2);
// 如果f1和f2是两个线程同时并行
线程1(X和Y是临时内存地址)
f1:
a. 把a的值保存在X
b. 把1的值保存在Y
c. 执行X+Y
d. 把结果保存到a
线程2(X和Y是临时内存地址)
f1:
a. 把a的值保存在X
b. 把2的值保存在Y
c. 执行X+Y
d. 把结果保存到a
/* 由于多线程并行,所以线程1中和线程2中的步骤是可以按照任意方式组合
    于是对于操作同一份数据a就存在了不确定性
*/
// 例如顺序1:
1a -> 1b -> 1c-> 1d -> 2a -> 2b -> 2c -> 2d 那么最后a的结果是4
// 如果假设顺序2:
1a -> 2a -> 1b -> 1c -> 1d -> 2b -> 2c -> 2d 那么最后a的结果就是3
// 当然还有其他很多顺序组合

由于Javascript是单线程的,所以在函数运行的时候具有原子性和完整性,也就是说在回调函数f1f2执行的时候,如果f1开始执行,那么在f1执行完成之前,f2不会进行执行,所以不存在多线程导致的不确定性。
但是Javascript同样存在不确定性,其不确定性来自异步的回调函数执行时间,这种不确定性也称为竞态条件,也就是说对于上面的例子,这里只存在先执行f1,还是先执行f2导致的结果不确定性。

1.3 并发

并发是时间间隔的概念,是指某个时间间隔内可以处理多件事情的能力。
知乎上有一个很通俗的例子关于并行和并发的区别:

非并发:吃饭的时候接到电话,需要先把饭吃完,才能接电话
并发:吃饭的时候接到电话,中断吃饭接电话,接完电话吃饭
并行:吃饭的时候接到电话,一边吃饭一边接电话(快速切换上下文,其实并不能算是很严格的并行,看似吃饭和打电话同时进行)
并发有三种常见的情况

1.3.1 非交互

两个运行函数之间没有任何关联,独自运行不会对结果产生影响

var a,b;
function f1() {
  a = 1;
}
function f2() {
  b = 1;
}
ajax(f1)
ajax(f2)
1.3.2 交互

两个运行函数之间存在关联,运行顺序对结果会产生影响

// 由于回调的不确定性,所以最后a的值可能是1,可能是2
var a ;
function f1() {
  a = 1;
}
function f2() {
  a = 2;
}
ajax(f1);
ajax(f2);

通常为了控制结果,会设置竞态条件

var a,b ;
function f1() {
  a = 1;
  foo(); // 如果直接输出,可能此时b未被赋值,所以会返回NaN
}
function f2() {
  b = 2;
  foo(); // 如果直接输出,可能此时a未被赋值,所以会返回NaN
}
function foo() {
  console.log(a + b);
}
ajax(f1);
ajax(f2);

// 使用竞态条件确保输出结果
var a,b ;
function f1() {
  a = 1;
  // 添加竞态条件
  if(a&&b) {
    foo();
  }
}
function f2() {
  b = 2;
  // 添加竞态条件
  if(a&&b) {
    foo();
  }
}
function foo() {
  console.log(a + b);
}
ajax(f1);
ajax(f2);
1.3.3 协作

Javascript的单线程操作具有原子性,那么当某个方法执行可能会持续占用引擎,于是我们通常会考虑执行一部分以后释放资源,使事件循环队列中的其他内容可以先执行,不断切换执行上下文。利用setTimeout函数,可以让我们实现这样的效果

function res(datas) {
  for(let i = 0; i< datas.length; i++) {
      if (i === 1000) {
          // 释放当前资源,尝试将回调重新加入事件循环队列尾部
          setTimeout(_ => {res(datas.slice(0,1000))} , 0);
          break;
      }
  }
}

1.4 任务队列

任务队列是建立在事件循环队列基础上,区别在于事件循环队列每次只能将事件添加到队列的尾部;任务队列则是在一个事件循环队列触发下一次tick前执行,可以不断在插入内容,从而使得事件循环的下一次tick延迟。

2. 回调

2.1 回调的执行

Javascript中实现异步分块执行的最简单的方式就是回调。当异步操作结束的时候,回调函数会被放到事件循环队列中,注意,这里不是异步操作结束的时候执行回调函数,而是将其放到事件循环队列中等待执行。
也就是说,当异步结束的时候,回调函数并非是立即执行,而是根据Javascript事件循环机制来进行执行,其具体运行时间不可预知。

function f1() {
  console.log(1);
}
// 这里设置1000ms是指在1000ms后将f1方法放到事件队列中,并不是1000ms后就立即执行回调方法
setTimeout(f1, 1000);

2.2 缺陷

回调很简单,但是回调在处理Javascript操作的时候存在一些缺陷

2.2.1 回调地狱

由于异步的不确定性,当我们需要依次使用回调结果的时候,不可避免的就必须要使用嵌套的方式

ajax(url1, function(url2){
  ajax(url2, function(url3)){
    ajax(url3, function(data){
      // 做某些内容
    })
  })
});

这种嵌套一方面带来的问题是代码上阅读的困难,另一方面没办法对某些操作进行统一的处理,例如:异常处理,日志操作等

2.2.2 信任问题

在使用第三方异步方法的时候,由于只能进行回调函数的传递,那么我们不能确保第三方异步方法如何对回调进行调用,可能存在多次调用回调的情况,从而导致结果和预期不相符

function f() {
  console.log(1);
}
ajaxF(f);
// 第三方提供的ajaxF,可能我们并不知道第三方调用回尝试重连
// 所以可能导致我们的回调调用多次,当然这个问题可以通过沟通解决
var i = 0;
function ajaxF(fn) {
  ajax(data=>{
    // 失败时且请求次数小于3尝试重新请求
    if (data.fail && i<3){
      i++;
      // 多次调用回调
      fn(data.fail);
      ajaxF(fn);
    } else {
        fn(data.success);
    }
  })
}

2.3 优化

回调自身存在一些缺陷,我们通过一些处理手段可以提升回调的可读性,但是这种优化并不能从根本上解决目前回调带来的困境,我们需要使用新的异步方案来替代回调,当然回调仍然是最简单的Javascript异步处理方法
优化一:将异常和成功回调分离

function fail() {
  console.log('fail')
}
function succ() {
  console.log('succ');
}
ajax(succ, fail);

优化二:first-error风格回调

function foo(err, data) {
  if(err) {
      // 处理错误逻辑
  } else {
      // 处理正常逻辑
  }
}
ajax(foo);

3. 小结

Javascript异步最基础的内容是事件循环,所以最基本的是了解事件循环机制,在了解的基础上再去处理所有和异步相关的问题都会变的简单。
回调是最简单的Javascript异步解决方案,但是其自身存在一些不方便使用的地方,在较复杂的时候,我们需要更好的异步方案来替代。

4. 参考

《你不知道的Javascript(中篇)》

你可能感兴趣的:(Javascript学习笔记-异步和回调)