javascript异步编程

143 阅读6分钟

前言

JavaScript 是单线程执行模型,也就是说,同步代码很有可能会阻塞任务,为了避免这个问题,就出现了异步编程。在javascript中,异步编程分三种表现形式:回调函数、Promise、async/await。

回调函数

在最基本的层面上,JavaScript的异步编程式通过回调实现的。回调的是函数,可以传给其他函数,而其他函数会在满足某个条件时调用这个函数。下面就来看看常见的不同形式的基于回调的异步编程。

定时器

最简单的异步操作就是使用定时器在规定多久时间之后执行注册的回调函数的代码:

    setTimeout(()=>{
        console.log("callback")
    },1000)

事件监听

给目标 DOM 绑定一个监听函数:

    document.getElementById('#myDiv').addEventListener('click', 
        (e) => { 
        console.log('我被点击了')
        }, false);

通过给 id 为 myDiv 的一个元素绑定了点击事件的监听函数,把任务的执行时机推迟到了点击这个目标元素时。

网络请求

另外一种常见的异步操作就是网络请求:

    const SERVER_URL = "/server"; 
    let xhr = new XMLHttpRequest(); 
    // 创建 Http 请求 
    xhr.open("GET", SERVER_URL, true); 
    // 设置状态监听函数 
    xhr.onreadystatechange = function() { 
        if (this.readyState !== 4) return; 
        // 当请求成功时 if (this.status === 200) { 
        handle(this.response); 
        } 
        else 
        { 
        console.error(this.statusText); 
        } 
        }; 
        // 设置请求失败时的监听函数 
        xhr.onerror = function() { console.error(this.statusText); }; 
        // 发送 Http 请求 
        xhr.send(null);

Node中的回调与事件

Node.js服务端JavaScript环境底层就是异步的,定义了很多使用回调和事件的API。例如读取文件默 认的API就是异步的,它会在读取文件内容之后调用一个回调函数:

    const fs = require('fs'); 
    // 读取配置文件,调用回调函数 
    fs.readFile('config.json', 'utf8', (err, data) => { 
    if(err) { 
    throw err; 
    } 
    else { 
    // todo
    } 

Promise

Promise的概念

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。

Promise解决了什么问题

地狱回调

在实际业务场景中,很容易出现回调嵌套的问题,一旦嵌套层数过多,就会形成地狱回调,代码变得难以阅读,以下是回调嵌套和使用promise处理的对比:

   // 回调嵌套
   setTimeout(()=>{
       console.log('1')
       setTimeout(()=>{
           console.log('2')
           setTimeout(()=>{
               console.log('3')
               setTimeout(()=>{
                   console.log('4')
               },1000)
           },1000)
       },1000)
},1000)
   
   // Promise
  const p1=new Promise((reslove,rejected)=>{
       reslove()
   })
   p1.then(()=>{
       console.log(1)
   }).then(()=>{
       console.log(2)
   }).then(()=>{
       console.log(3)
   }).then(()=>{
       console.log(4)
   })

如何手写一个Promise

promise需要遵循promisesA+规范

Promise的声明

promise是一个类,使用ES6 class来声明。

  • 由于new Promise((resolve, reject)=>{}),所以传入一个参数(函数),规范里叫它executor,立即执行。
  • reslove和reject是函数,使用let声明
    // 构造器
    constructor(executor){

    // 成功
    let resolve = () => {
     };
    // 失败
    let reject = (reason) => { 
    };

    // 立即执行回调函数
    executor(resolve, reject);
    }
}
    

规范对Promise的规定

  • Promise有三种状态(state):pending、fulfilled、rejected
  • 初始状态为pending,可转换为fulfiled和rejected状态
  • fulfilled和rejected不可变更状态
  • executor函数执行报错,直接执行reject
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class Promise {
  // 构造器
  constructor(executor) {
    // 初始化状态
    this.state = PENDING;
    // 初始化成功值
    this.value = undefined;
    // 初始化失败值
    this.reason = undefined;

    // 成功
    let resolve = (value) => {
      // 只有状态为PENDING时才能更改
      if (this.state === PENDING) {
        // 修改为成功状态,并赋值
        this.state = FULFILLED;
        this.value = value;
      }
    };
    // 失败
    let reject = (reason) => {
      // 只有状态为PENDING时才能更改
      if (this.state === PENDING) {
        // 修改为失败状态,并赋值
        this.state = REJECTED;
        this.reason = reason;
      }
    };

    // 立即执行回调函数
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
}

then

规范规定Promise有then方法,里面有两个回调(onFulfilled,onRejected),成功执行onFulfilled(参数为value),失败执行onRejected(参数为reason)

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class Promise {
  // 构造器
  constructor(executor) {...}

  then(onFulFilled,onRejected){
      // 成功
    if(this.state===FULFILLED)
    {
        onFulFilled(this.value)
    }

    // 失败
    if(this.state===REJECTED)
    {
        onRejected(this.reason)
    }
  }
}

支持异步

此时已经可以正常使用了,但是如果resolve是在setTimeOut里面执行的,当then方法执行的时候,state的状态还是pending,所以无法正确执行onFulFilled和onRejected的回调,我们需要把then方法里面的回调使用数组缓存起来,当状态更改的时候才执行

const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

class MyPromise {
  // 构造器
  constructor(executor) {
    // 初始化状态
    this.state = PENDING;
    // 初始化成功值
    this.value = undefined;
    // 初始化失败值
    this.reason = undefined;

    // 成功回调存放的数组
    this.onFulfilledCallbacks = [];
    // 失败回调存放的数组
    this.onRejectedCallbacks = [];

    // 成功
    let resolve = (value) => {
      // 只有状态为PENDING时才能更改
      if (this.state === PENDING) {
        // 修改为成功状态,并赋值
        this.state = FULFILLED;
        this.value = value;
        // 遍历执行回调
        this.onFulfilledCallbacks.forEach(fn=>fn())
      }
    };
    // 失败
    let reject = (reason) => {
      // 只有状态为PENDING时才能更改
      if (this.state === PENDING) {
        // 修改为失败状态,并赋值
        this.state = REJECTED;
        this.reason = reason;
         // 遍历执行回调
         this.onRejectedCallbacks.forEach(fn=>fn())
      }
    };

    // 立即执行回调函数
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled,onRejected){
      // 成功
    if(this.state===FULFILLED)
    {
       onFulfilled(this.value)
    }

    // 失败
    if(this.state===REJECTED)
    {
        onRejected(this.reason)
    }

    if(this.state===PENDING)
    {
        // onFulfilled传入到成功数组
        this.onFulfilledCallbacks.push(()=>{
            onFulfilled(this.value)
        })

         // onRejected传入到成功数组
        this.onRejectedCallbacks.push(()=>{
            onRejected(this.reason)
        })
    }
  }
}

链式调用

平常写代码的时候,经常会new Promise().then().then()这样使用,这就是链式调用,用来解决回调地狱问题

  • 规范规定then方法会返回一个新的Promise,以此来支持链式调用
  • 第一个then方法return出了promise2,promise2的值正是第一个promise的onFulfilled()或onRejected()的值,规定onFulfilled()或onRejected()的值,即第一个then返回的值,叫做x,判断x的函数叫做resolvePromise,x可以是普通的值,也可以是一个Promise,所以需要reslovePromise进行解析
then(onFulfilled, onRejected) {
    let promise2 = new MyPromise((resolve, reject) => {
      // 成功
      if (this.state === FULFILLED) {
        let x = onFulfilled(this.value);
        resolvePromise(promise2, x, resolve, reject);
      }

      // 失败
      if (this.state === REJECTED) {
        let x = onRejected(this.reason);
        resolvePromise(promise2, x, resolve, reject);
      }

      if (this.state === PENDING) {
        // onFulfilled传入到成功数组
        this.onFulfilledCallbacks.push(() => {
          let x = onFulfilled(this.value);
          resolvePromise(promise2, x, resolve, reject);
        });

        // onRejected传入到成功数组
        this.onRejectedCallbacks.push(() => {
          let x = onRejected(this.reason);
          resolvePromise(promise2, x, resolve, reject);
        });
      }
    });
  }

reslovePromise

  • 规范规定x假如是个对象或函数,并且有then方法,就默认是x是promise,进行递归解析
  • x不是promise的话,直接resolve
function reslovePromise(promise2, x, resolve, reject) {
  // 循环引用
  if (x === promise2) {
    return reject(new Error("promise不能循环引用"));
  }
  let called;

  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    try {
      let then = x.then;
      // 有then方法,默认是promise
      if (typeof then === "function") {
        then.called(
          x,
          (y) => {
            if (called) return;
            called = true;
            // 如果是promise,则继续解析
            reslovePromise(promise2, y, resolve, reject);
          },
          (error) => {
            if (called) return;
            called = true;
            // 失败
            reject(error);
          }
        );
      }
      else {
          resolve(x)
      }
    } catch (error) {
      if (called) return;
      called = true;
      reject(err);
    }
  } else {
    resolve(x);
  }
}