同步与异步

  • Post author:
  • Post category:其他


1.1 什么是同步/异步任务?

  • 同步任务:指的是在主线程上排队执行的任务,只有当前一个任务执行完毕后才可执行下一个任务。

  • 异步任务:指的是不进入主线程,而进入任务队列的任务。只有当任务队列通知主线程某个异步任务可以执行,方可进入主线程进行执行。

1.2 为什么会出现异步问题?

  • 由于JavaScript是一门

    单线程

    语言,即同一时间只能做一件事;

  • JS本身是同步执行;

  • 但在执行耗时操作时

    为了避免阻塞后续代码的执行

    ,通常采用异步操作;

  • 通过事件循环(event loop)实现异步。

1.3 JS中的任务队列

Javascript 这门脚本语言诞生的使命就是为处理页面中用户的交互,以及操作 DOM 而诞生的。

多线程操作DOM == 乱套

同步任务:简单的任务

异步任务:分为宏任务和微任务

宏任务(macrotask):script, setTimeout ,setInterval ,setImmediate ,I/O,UI rendering

微任务(microtask ):process.nextTick,promise ,MutationObserver


Promise是宏任务(同步执行),但Promise 的回调函数属于异步任务,会在同步任务之后执行(比如说

then



catch



finally


只有当同步任务都执行完毕之后,才会到任务队列里面执行异步任务。


在任务队列中,也分为宏任务和微任务分别在宏任务队列和微任务队列,只有微任务队列中的任务全部执行完毕后,才会执行宏任务队列里面的任务。

console.log(1)
setTimeout(function () {
    console.log(2);
    process.nextTick(function () {
        console.log(3);
    })
})
Promise.resolve().then(function() {
    console.log(4)
}).then(function() {
    console.log(5)
})
结果:
1
4
5
2
3
面试题
async function async1() {
	console.log('async1 start');
	await async2();
	console.log('async1 end');
}
async function async2() {
	console.log('async2');
}
console.log('script start');
setTimeout(function () {
	console.log('setTimeout');
}, 0);
async1();
new Promise(function (resolve) {
	console.log('promise1');
	resolve();
}).then(function () {
	console.log('promise2');
});
console.log('script end');


script start
VM557:2 async1 start
VM557:7 async2
VM557:15 promise1
VM557:20 script end
VM557:4 async1 end
VM557:18 promise2
VM557:11 setTimeout

主线程中的任务结束之后,取出任务队列的第一个任务,推入执行栈中,重复此步骤这就叫做事件循环

1.4 浏览器的线程


浏览器的渲染进程是多线程的


  • 1.GUI渲染线程

    • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。

    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行

    • 注意,

      GUI渲染线程与JS引擎线程是互斥的

      ,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中

      等到JS引擎空闲时

      立即被执行。


    2.JS引擎线程

    • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)

    • JS引擎线程负责解析Javascript脚本,运行代码。

    • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序

    • 同样注意,

      GUI渲染线程与JS引擎线程是互斥的

      ,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。


    3.事件触发线程

    • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)

    • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中

    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理

    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)


    4.定时触发器线程

    • 传说中的

      setInterval



      setTimeout

      所在线程

    • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)

    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)

    • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。


    5.异步http请求线程

    • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求

    • 将检测到状态变更时,如果设置有回调函数,异步线程就

      产生状态变更事件

      ,将这个回调再放入事件队列中。再由JavaScript引擎执行。

1.5 回调地狱

由多层

嵌套

的回调函数组成的代码称为回调地狱。

function callbackFn(callback){    setTimeout(function(){        callback()    },1000)}callbackFn(function(){    callbackFn(function(){        callbackFn(function(){            callbackFn(function(){console.log('回调结束')})        })    })})​

回调地狱就是为是实现代码顺序执行而出现的一种操作,它会造成我们的代码可读性非常差,后期不好维护。(强耦合)

三种方案解决回调地狱 Promise Generator async/await

1.6 Promise

ES6 将其写进了语言标准,统一了用法,原生提供了

Promise

对象。

所谓

Promise

,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

两个特点:

1.对象的状态不受外界影响。

2.一旦状态改变,就不会再变。

三个状态:


pending

(进行中)、

fulfilled

(已成功)和

rejected

(已失败)

pending—–>resolved :成功数据value

pending—–>rejected :失败原因reason

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise新建后会立即执行

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

Promise.all


Promise.all()

方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

全部变成

fulfilled

,或者其中有一个变为

rejected

,才会调用

Promise.all

方法后面的回调函数。

Promise.race

const p = Promise.race([p1, p2, p3]);


Promise.race()

方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例

只要

p1



p2



p3

之中有一个实例率先改变状态,

p

的状态就跟着改变

Promise.allSettled

全部Settled(包含fulfilled、rejected)完成后回调

Promise.any

只要参数实例有一个变成

fulfilled

状态,包装实例就会变成

fulfilled

状态;如果所有参数实例都变成

rejected

状态,包装实例就会变成

rejected

状态。

Promise.resolve

Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

1.7 Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。


  1. function

    关键字和函数之间有一个星号(

    *

    ),且内部使用yield表达式,定义不同的内部状态。


  2. 调用Generator函数后,该函数并不执行

    ,返回的也不是函数运行结果,而是一个指向内部状态的指针对象

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

Generator的用途

在JavaScript中,一个函数一旦被执行,就会执行到最后或者被

return

,运行期间

不会被外部所影响打断

,而Generator的出现就

打破了这种函数运行的完整性

1.8 async、await

async 函数是 Generator 函数的语法糖。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};

等价于

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};


async

函数的

await

命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。Promise.resolve(1)

async的返回值是Promise ,进一步说,

async

函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而

await

命令就是内部

then

命令的语法糖。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);
//指定50毫秒后 输出hello world


await

命令后面是一个

thenable

对象(即定义了

then

方法的对象),那么

await

会将其等同于 Promise 对象。

实现原理:

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      if(next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() { return gen.next(v); });
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
    step(function() { return gen.next(undefined); });
  });
}

async 函数可以保留运行堆栈。

const a = () => {
  b().then(() => c());
};

函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。等到b()运行结束,可能a()早就运行结束了,b()所在的上下文环境已经消失了。如果b()或c()报错,错误堆栈将不包括a()

const a = async () => {
  await b();
  c();
};


b()

运行的时候,

a()

是暂停执行,上下文环境都保存着。一旦

b()



c()

报错,错误堆栈将包括

a()

。(方便查找报错)

async function foo() { 
 console.log(await Promise.resolve('foo')); 
} 
async function bar() { 
 console.log(await 'bar'); 
} 
async function baz() { 
 console.log('baz'); 
} 
foo();
bar(); 
baz(); 
// baz 
// bar 
// foo

async/await 中真正起作用的是 await。async 关键字,无论从哪方面来看,都不过是一个标识符。

毕竟,异步函数如果不包含 await 关键字,其执行基本上跟普通函数没有什么区别。

1.9 横向对比三种方法

Promise :

优点:

回调函数的改进。使用then方法。异步任务的多段执行更清楚。

缺点:

Promise的最大问题是代码冗余,请求任务多时,一堆的then,也使得原来的语义变得很不清楚

Generator :

优点:

分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。虽然Generator将异步操作表示得很简洁,但是流程管理却不方便

缺点:

流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)

async:

优点:

1.

方便级联调用

:即调用依次发生的场景。

2.

同步代码编写方式

: Promise使用then()函数进行链式调用,一直点点点,是一种从左向右的横向写法;async/await从上到下,顺序执行,就像写同步代码一样,更符合代码编写习惯。

3.

多个参数传递

:Promise的then()函数只能传递一个参数,虽然可以通过包装成对象来传递多个参数,但是会导致传递冗余信息,频繁的解析又重新组合参数,比较麻烦;async/await没有这个限制,可以当做普通的局部变量来处理,用let或者const定义的块级变量想怎么用就怎么用,想定义几个就定义几个,完全没有限制,也没有冗余工作。

4.

同步代码和异步代码可以一起编写

: 使用Promise的时候最好将同步代码和异步代码放在不同的then()节点中,这样结构更加清晰;async/await整个书写习惯都是同步的,不需要纠结同步和异步的区别,当然,异步过程需要包装成一个Promise对象放在await关键字后面。

5.

sync/await是对Promise的优化

: async/await是基于Promise的,是进一步的一种优化,不过在写代码时,Promise本身的API出现得很少,很接近同步代码的写法。



版权声明:本文为qq_38885990原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。