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(中篇)》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容