9

大前端进击之路(二)JS异步编程

 3 years ago
source link: https://blog.csdn.net/Cjk0620/article/details/112144927
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在这里插入图片描述

打工人!打工魂!前端才是人上人!此系列总结于大前端进击之路过程中的学习,如果文章中有不对的地方,希望大家能进行批评改正,互相进步。

经典面试题

我们先来看一道经典的面试题,让我们的小脑袋瓜子思考起来~如果你对这道题有清晰的思路并且了解背后的原因,那么请直接点赞评论加关注!!!!!

//请写出输出内容
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
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

JS采用单线程模式工作的原因

为了回答这个问题我们首先需要知道JS的执行环境是单线程的,是因为JS语言最早是运行在浏览器端的语言,目的是为了实现页面上的动态交互。实现动态交互的核心就是DOM操作,因此决定了JS必须是单线程模式工作。我们来假设一下如果JS是多线程一起工作的,其中一个线程修改了一个DOM元素,另外的一个线程同时又要删除这个DOM元素,那么此时浏览器就懵逼了,无法明确以哪个工作线程为准。所以为了避免线程同步的问题,JS就被设计成了单线程的工作模式。

注意,我们这里说的单线程是JS的执行环境是单线程,浏览器中是多线程的。

单线程的优势和弊端

采用单线程的工作模式可以节省内存,节约上下文切换时间,没有锁的问题。但弊端也很明显,如果中间有一个任务需要花费大量的时间,那么后面的任务就需要等待这个任务完成后才能执行,就会出现假死的情况,对用户很不友好。为了解决这个问题JS给出了两种执行模式:同步模式(Synchronous)和异步模式(Asynchronous)

同步模式和异步模式

同步模式其实很好理解,举个栗子:

我们如果按照同步模式煮面的话,首先先将锅里装上水,打开火开始烧水,等待水烧开,再将面、鸡蛋、火腿肠等材料拿出,材料准备好后放入锅中进行煮,煮好后开始干饭。

在这里其实我们已经能够看出来问题,我们必须等到水烧开后才去准备要煮的材料。回到概念里就是在同步模式下我们的代码是依次执行,后一个任务必须等待前一个任务结束才能开始执行。程序执行的顺序和代码编写的顺序是完全一致的。在单线程模式下,大多数任务都是以同步模式执行。

上个例子中我们在等待水烧开的过程中什么都没干,很浪费时间,我们可以在烧水的过程中将食材都准备好,等到水烧开后直接放入。

我们在烧水的过程中去干了别的事情,就属于异步模式,异步模式中不会等待异步任务的结束才开始执行下一个同步的任务,都是开启过后就立即执行下一个任务。

异步模式对于JS很重要,没有异步模式的话我们就无法同时处理大量的耗时任务,就会给用户带来卡顿和假死的体验。对于我们开发者来说,会给我们打开代码执行的顺序混乱的问题。

EventLoop事件循环和消息队列

  • EventLoop是一种循环机制,主线程从消息队列中读取任务并按照顺序执行,这个过程是循环不间断的。
  • 消息队列是存放异步任务的地方,当我们的同步任务都执行完毕后,EventLoop会从消息队列中依次取出异步任务放到调用栈中进行执行。

宏任务和微任务

  • 宏任务可以理解为每次执行栈执行的代码就是一个宏任务

    浏览器为了让JS内部宏任务与DOM操作能够有序的执行,会在一个宏任务执行结束后,下一个宏任务执行开始前,对页面进行重新渲染。

    宏任务包括:script整体代码、setTimeout、setInterval、I/O、UI交互事件、MessageChannel等。

  • 微任务可以理解为每个宏任务执行结束后立即执行的任务,发生在宏任务后,渲染之前,执行微任务。

    所以微任务的响应速度相比宏任务会更快,因为无需等待UI渲染

    微任务包括:Promise.then、MutaionObserver、process.nextTick(Node.js环境下)等。

在这里插入图片描述
在这里插入图片描述

图片取自掘金,侵即删

异步编程方案的本质—回调函数

回调函数:由调用者定制,交给执行者执行的函数。

我们通过 callback 回调函数、事件发布/订阅、Promise 等来组织代码,本质都是通过回调函数来实现异步代码的存放与执行。

// callback就是回调函数
// 就是把函数作为参数传递,缺点是不利于阅读,执行顺序混乱。
function foo(callback) {
    setTimeout(function(){
        callback()
    }, 3000)
}

foo(function() {
    console.log('这是回调函数')
    console.log('调用者定义这个函数,执行者执行这个函数')
    console.log('其实就是调用者告诉执行者异步任务结束后应该做什么')
})

更优异步编程统一方案——Promise

Promise概述

Promise概念MDN传送门

关于Promise概念性内容就不在赘述了,可直接点击传送门前往MDN查看。简单来说如果我们是用传统的回调函数方式来完成复杂的异步流程,就会无法避免大量的回调函数嵌套,产生回调地狱的问题。为了避免回调地狱让我们开始愉快的Promise的学习时光吧!

// 我们想要执行完第一个再执行第二个再执行第三个
// 虽然我们使用同步的方式将异步的代码学出来了,但是这样的回调是不是让我们的小脑袋瓜子嗡嗡的?
setTimeout(() => {
    console.log('执行第一个');
    setTimeout(() => {
        console.log('执行第二个');
        setTimeout(() => {
            console.log('执行第三个');
            setTimeout(() => {
                console.log('执行第四个');
                setTimeout(() => {
                    console.log('执行第五个');
                }, 2000);
            }, 2000);
        }, 2000);
    }, 2000);
}, 2000);

回调地狱图示,取自网络,侵即删。

Promise基本用法

// Promise 基本示例
// promise的英文意思是承诺
// 在JS中Promise是一个对象,接收一个函数作为参数
const promise = new Promise(function (resolve, reject) {
  // 这里用于“兑现”承诺
  // resolve(100) // 承诺达成
  reject(new Error('promise rejected')) // 承诺失败
})

promise.then(function (value) {
  // 即便没有异步操作,then 方法中传入的回调仍然会被放入队列,等待下一轮执行
  console.log('resolved', value)
}, function (error) {
  console.log('rejected', error)
})

Promise案例

我们用Promise来封装一个AJax

function ajax (url) {
  return new Promise((resolve, rejects) => {
    // 创建一个XMLHttpRequest对象去发送一个请求
    const xhr = new XMLHttpRequest()
    // 先设置一下xhr对象的请求方式是GET,请求的地址就是参数传递的url
    xhr.open('GET', url)
    // 设置返回的类型是json,是HTML5的新特性
    // 我们在请求之后拿到的是json对象,而不是字符串
    xhr.responseType = 'json'
    // html5中提供的新事件,请求完成之后(readyState为4)才会执行
    xhr.onload = () => {
      if(this.status === 200) {
        // 请求成功将请求结果返回
        resolve(this.response)
      } else {
        // 请求失败,创建一个错误对象,返回错误文本
        rejects(new Error(this.statusText))
      }
    }
    // 开始执行异步请求
    xhr.send()
  })
}

ajax('/api/user.json').then((res) => {
  console.log(res)
}, (error) => {
  console.log(error)
})

Promise的链式调用

  • 嵌套使用的方式是使用Promise最常见的误区。我们要使用promise的链式调用的方法尽可能保证异步任务的扁平化。
// 嵌套使用 Promise 是最常见的误区
ajax('/api/urls.json').then(function (urls) {
  ajax(urls.users).then(function (users) {
    ajax(urls.users).then(function (users) {
      ajax(urls.users).then(function (users) {
        ajax(urls.users).then(function (users) {
        })
      })
    })
  })
})

链式调用的理解

  • promise对象then方法,返回了全新的promise对象。可以再继续调用then方法,如果return的不是promise对象,而是一个值,那么这个值会作为resolve的值传递,如果没有值,默认是undefined
  • 后面的then方法就是在为上一个then返回的Promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束

Promise的异常处理

  • then中回调的onRejected方法
  • .catch()
ajax('/api/user.json')
  .then(function onFulfilled(res) {
    console.log('onFulfilled', res)
  })
  .catch(function onRejected(error) {
    console.log('onRejected', error)
  })
  
// 相当于
ajax('/api/user.json')
  .then(function onFulfilled(res) {
    console.log('onFulfilled', res)
  })
  .then(undefined, function onRejected(error) {
    console.log('onRejected', error)
  })

简单来说.catch是给整个promise链条注册的一个失败回调,推荐使用。

拉勾大前端训练营


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK