同步与异步

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 的回调函数属于异步任务,会在同步任务之后执行(比如说 thencatchfinally


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

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

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.定时触发器线程

    • 传说中的setIntervalsetTimeout所在线程

    • 浏览器定时计数器并不是由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 实例

只要p1p2p3之中有一个实例率先改变状态,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出现得很少,很接近同步代码的写法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值