当前位置: 首页 > article >正文

JavaScript 系列之:Ajax、Promise、Axios

前言

  • 同步:会阻塞。同步代码按照编写的顺序逐行依次执行,只有当前的任务完成后,才会执行下一个任务。

  • 异步:异步代码不会阻塞后续代码的执行。当遇到异步操作时,JavaScript 会将该操作放入任务队列中,继续执行后续的同步代码,直到同步代码执行完毕,再从任务队列中取出异步操作的结果进行处理。

Ajax、Promise、Axios 他们都是异步相关的技术。

  • Ajax 用来发起网络请求;

  • Promise 是 JavaScript 的一种异步编程解决方案(不仅限于异步网络请求),可以使用 Promise 来封装 Ajax 网络请求;

  • Axios 就是使用 Promise 来封装 Ajax 请求的。

Ajax

Ajax(Asynchronous Javascript And XML),翻译为异步的 Javascript 和 XML,它不是编程语言,是一项 Web 应用程序技术。能够在不重新加载整个页面的情况下更新部分网页内容。

Ajax 通过 XMLHttpRequest 对象来向服务器发出异步请求,从服务器获得数据,然后用 JavaScript 来操作 DOM 从而更新局部页面。

特点:

  • 异步通信

    • 发送请求后,程序并不会等待响应结果,而是会继续往下运行

    • 所以,必须要在 Ajax 状态监听的回调函数中,才能保证获取响应数据

  • 刷新数据而不会加载整个页面

    • 不用 Ajax:更新或提交内容——需要重新加载整个网页

    • 使用 Ajax:更新或提交内容——只更新部分网页

  • 无需插件

    • 使用纯粹的 JavaScript 和浏览器内置的 XmlHttpRequest 对象

缺点:

  • Ajax 不能使用 back 和 history 功能,即对浏览器机制的破坏。

  • 安全问题:Ajax 暴露了与服务器交互的细节

XMLHttpRequest 对象

XMLHttpRequest 对象是由浏览器提供的,它是浏览器的内置对象,而不是 JavaScript 的内置对象。可以通过 window.XMLHttpRequest 得到。

常用的方法:

  • open(get/post, url, 是否异步默认true):创建 http 请求

  • send():发送请求给服务器

  • setRequestHeader():设置头信息(使用 post 才会用到,get 并不需要调用该方法)

  • onreadystatechange:用于监听 ajax 的工作状态(readyState 变化时会调用此方法)

常用的属性:

  • readyState:用来存放 XMLHttpRequest 的状态,监听从0-4发生不同的变化

    • 0: 还未创建请求,即未调用 open() 方法

    • 1: 已调用 open() 方法,但未发送 send() 方法

    • 2: 已调用 send() 方法,但未接收到响应

    • 3: 已接收到部分响应

    • 4: 已接收到全部的响应

  • status:服务器返回的状态码

  • responseText:服务器返回的文本内容

Ajax 如何解决浏览器缓存问题

  • 在 Ajax 发送请求前加上 anyAjaxObj.setRequestHeader("If-Modified-Since", "0")

  • 在 Ajax 发送请求前加上 anyAjaxObj.setRequestHeader("Cache-Control", "no-cache")

  • 在 URL 后面加上一个随机数:"fresh=" + Math.random()

  • 在 URL 后面加上时间戳:"nowtime=" + new Date().getTime()

手写 Ajax

Ajax 的基本流程:创建 XMLHttpRequest 对象 => 发送数据 => 接收数据

发送 get 请求代码示例:

// 创建一个新的 XMLHttpRequest 对象
var xhr = new XMLHttpRequest();

// 配置 GET 请求,URL 后面加上参数
xhr.open('GET', 'https://example.com/api?name=John&age=30', true);

// 设置请求头 (可选)
xhr.setRequestHeader('Content-Type', 'application/json');

// 监听请求完成后的回调
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log('GET 请求成功:', xhr.responseText);
    } else {
        console.log('GET 请求失败:', xhr.status);
    }
};

// 发送请求
xhr.send();

发送 post 请求代码示例:

// 创建一个新的 XMLHttpRequest 对象
var xhr = new XMLHttpRequest();

// 配置 POST 请求
xhr.open('POST', 'https://example.com/api', true);

// 设置请求头
xhr.setRequestHeader('Content-Type', 'application/json');

// 请求完成后的回调
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        console.log('POST 请求成功:', xhr.responseText);
    } else {
        console.log('POST 请求失败:', xhr.status);
    }
};

// 准备 POST 请求数据
var data = {
    name: 'John',
    age: 30
};

// 发送 POST 请求,并传递数据
xhr.send(JSON.stringify(data));

Promise

ES6 新特性,Promise 是 JavaScript 中用于处理异步操作的一种方案。本质是一个构造函数,参数是一个执行器函数。

// 创建实例
var promise = new Promise((resolve, reject)=> {
  console.log(1)
  // 异步操作
  setTimeout(() => {
    const success = true; // 模拟异步操作成功
    if (success) {
      console.log(2)
      resolve('操作成功');
    } else {
      reject('操作失败');
    }
  }, 2000);
}).then(res =>{
  // 成功时的回调函数,即 resolve 被调用时执行。
  console.log(3, res) // 3 操作成功
}).catch(err =>{
  // 失败时的回调函数,即 reject 被调用时执行。
});

// 立即输出 1,等待两秒钟后输出 2,再立即输出 3

初学者可能不太理解这段代码,我们将它拆分开:

// 定义一个执行异步操作的函数  
function executorFunction(successMethod, errorMethod) {  
  console.log(1); // 异步操作前的日志  
  // 使用 setTimeout 模拟异步操作  
  setTimeout(() => {  
    const success = true; // 模拟异步操作成功  
    if (success) {  
      console.log(2); // 异步操作成功时的日志  
      successMethod('操作成功'); // 调用 successMethod,表示操作成功  
    } else {  
      errorMethod('操作失败'); // 调用 errorMethod,表示操作失败  
    }  
  }, 2000); // 模拟异步操作耗时 2 秒
}  
  
// 定义成功时的回调函数  
function onSuccess(res) {  
  console.log(3); // 异步操作成功后的日志  
  console.log(res); // 打印 successMethod 传递的值  
}  
  
// 定义失败时的回调函数  
function onError(err) {  
  console.log('发生错误:', err); // 打印 errorMethod 传递的错误信息  
}  
  
// 使用构造函数创建 Promise 对象,构造函数接受一个执行器函数作为参数
var promise = new Promise(executorFunction);  
  
// 使用 .then() 和 .catch() 链接 Promise  
promise.then(onSuccess).catch(onError);

在上面的示例中,function executorFunction(successMethod, errorMethod) 就是执行器函数,执行器函数在创建 Promise 实例时立即被调用,此时会将 PromiseState(即 Promise 的状态值) 初始化为 pending

执行器函数接受两个回调函数作为参数,调用第一个参数时表示成功,会将 PromiseState 改为 fulfilled,并将成功的结果传递给后续的 then 方法;调用二个参数时表示失败,会将 PromiseState 改为 rejected,并将错误的结果传递给后续的 catch 方法。

什么是执行器函数?

执行器函数就是那些被设计来执行特定任务或操作的函数,它与普通函数并没有什么区别,只是在叫法上更加细致。

为什么要有执行器函数?为什么不直接使用 resolve 和 reject 作为参数?

其实就是为了封装和控制。其实执行器函数内部封装了很多隐藏的方法。例如 Promise 的状态转换,是怎么从 pending 转为 fulfilled 或 rejected 的呢?还有调用 resolve('操作成功') 方法时,Promise 链是怎么拿到结果的呢?等等这些都是执行器函数在发挥作用。没有执行器函数是无法做到这些的。

为什么要在 .then() 里操作成功时的回调函数,而不是直接在 resolve 方法之前操作?

这是一个强烈的建议,但并不是一个唯一标准。

  • 异步性:Promise 的主要目的是处理异步操作。如果你在 resolve 之前直接进行操作,那么这些操作就会是同步的,而不是异步的。

  • 链式调用:Promise 支持链式调用,允许你通过 .then() 方法将多个异步操作连接在一起,如果你在 resolve 之前直接进行操作,这种链式调用就无法实现。

  • 错误处理:Promise 通过 .catch() 方法提供了集中的错误处理机制。如果你在 resolve 之前直接进行操作,并且这些操作可能抛出错误,那么你就需要使用传统的 try-catch 语句来捕获这些错误。

  • 解耦:将操作放在 .then() 中可以将这些操作与 Promise 的创建和解析解耦。

Promise 特点

  • 对象的状态不受外界影响

    • 这个好理解,每一个 Promise 对象都是通过构造函数 new 出来的,状态只在该构造函数内部有效

    • 有三种状态:pending(等待状态)、fulfilled(成功/已解决状态)、rejected(失败/被拒绝状态)

  • 有 PromiseState 属性和 PromiseResult 属性

    • PromiseState 值是当前状态值(pending、fulfilled、rejected 中的一种),

    • PromiseResult 值是当前结果值(resolve 或 reject 中的值)。

  • 一旦状态改变,就不会再变

    let p = new Promise((rosolve, reject)=> {
        rosolve('成功') // 第一次状态改变,一旦状态被改变,后面的代码就都不执行了
        reject('失败') // 再改变无效
        consoloe.log('我不会被执行')
    })
    console.log(p)
    

在这里插入图片描述

Promise 的 .then() 和 .catch()

  • .then() 接收 1-2 个回调函数作为参数:

    • 第一个参数是当 Promise 成功(fulfilled)时调用的函数。

    • 第二个参数可选,是当 Promise 失败(rejected)时调用的函数。

  • .catch() 接收 1 个回调函数作为参数

    • 参数是当 Promise 失败(rejected)时调用的函数。

    • 如果 .then() 定义了第二个参数,那么当 Promise 被拒绝时会调用 then 的第二个参数函数,而不是 catch 中的参数函数

let p1 = new Promise((rosolve, reject) => {
    setTimeout(() => {
        throw new Error('出错了'); 
    })
})
.then(data => {
    console.log('then data:', data);
})
.catch(error => {
    console.error('catch error:', error);
});
// 没有调用 rosolve 或 reject,then 和 catch 不执行,哪怕是抛出异常也不会执行。
// .then() 接收一个参数
let p1 = new Promise((rosolve, reject) => {
    setTimeout(() => {
        rosolve('成功') // rosolve
    })
})
.then(data => {
    console.log('then data:', data); // then data: 成功
})
.catch(error => {
    console.error('catch error:', error);
});

let p2 = new Promise((rosolve, reject) => {
    setTimeout(() => {
        reject('失败') // reject
    })
})
.then(data => {
    console.log('then data:', data);
})
.catch(error => {
    console.error('catch error:', error); // catch error: 失败
});
// .then() 接收两个参数
let p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        reject('失败') // reject
    })
})
.then(
    data => {
        console.log('then data:', data);
    },
    error => {
        console.error('then error:', error); // then error: 失败
    }
)
.catch(error => {
    console.error('catch error:', error);
});

既然 then 方法的第二个参数也可以用于处理 reject 的情况,那为什么还要有 catch 方法呢?为什么要有两个重复的功能?

从下面要介绍的 Promise 链式调用中你会找到答案。

Promise 链式调用

链式调用的原理

  • then 方法接收两个可选的回调函数作为参数,这两个参数是有返回值的,它们的返回值被用作下一个 then 方法的参数(如果不定义返回值就是 return undefined)

  • then 方法的返回值其实是一个 Promise 对象,如果 then 中 return 的是一个非 Promise 类型的值(如字符串、undefined 等),就会自动将这个值包裹成一个 PromiseStatus 为 fulfilled、PromiseResult 为这个值的 Promise 对象。例:return 1 会变成 return Promise.resolve(1)。

// then 方法的返回值其实是一个 Promise 实例
const p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        rosolve('成功') // rosolve
    }, 2000)
})

const p1 = p.then(
    data => {
        console.log('then 1 data:', data);
        return 1
    },
    error => {
        console.error('then 1 error:', error);
    }
)

console.log('p1:', p1);

/*
then 1 data: 成功
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 1
*/
// 返回值被用作下一个 then 方法的参数
const p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        rosolve('成功') // rosolve
    }, 1000)
})

p.then(
    data => {
        console.log('then 1 data:', data); // then 1 data: 成功
        return 1
    },
    error => {
        console.error('then 1 error:', error);
    }
)
.then(data => {
    console.log('then 2 data:', data); // then 2 data: 1
});

链式调用的规则

1、如果在上一个 then 或 catch 中捕获了 reject,那么最终都会被下一个 then 的成功回调函数捕获:

// 情况 1:then 的失败回调捕获 reject
const p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        reject('失败') // reject
    }, 1000)
})

p.then(
    data => {
        console.log('then 1 data:', data);
        return 1
    },
    error => {
        // then 的失败回调捕获 reject
        console.error('then 1 error:', error); // then 1 error: 失败
        return 2
    }
)
.then(
    data => {
        console.log('then 2 data:', data); // then 2 data: 2
    },
    error => {
        console.error('then 2 error:', error);
    }
);
// 情况 2:catch 捕获 reject
const p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        reject('失败') // reject
    }, 1000)
})

p.then(
    data => {
        console.log('then 1 data:', data);
        return 1
    },
)
.catch(error=> {
    // catch 捕获 reject
    console.error('error 1 error:', error); // error 1 error: 失败
    return 2
})
.then(
    data => {
        console.log('then 2 data:', data); // then 2 data: 2
    },
    error => {
        console.error('then 2 error:', error);
    }
);

2、如果在上一个 then 或 catch 中没有捕获 reject,或 return 了一个 reject,或抛出了异常,那么最终会被下一个 then 的失败回调函数或下一个 catch 捕获:

// 情况 1:没有捕获 reject
const p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        reject('失败') // reject
    }, 1000)
})

p.then(
    data => {
        console.log('then 1 data:', data);
        return 1
    },
    // error 中没有捕获 reject,代码屏蔽了
    //error => {
    //    console.error('then 1 error:', error);
    //    return 2
    //}
)
// catch 也没有捕获 reject,代码屏蔽了
//.catch(error=> {
//    console.error('error 1 error:', error);
//    return 2
//})
.then(
    data => {
        console.log('then 2 data:', data);
    },
    // 会被第二个 then 的 失败回调捕获
    error => {
        console.error('then 2 error:', error); // then 2 error: 失败
    }
);
// 或者被第二个 catch 捕获
//.catch(error => {
//    console.error('catch 2 error:', error); // catch 2 error: 失败
//})
// 情况 2:return 了 reject
const p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        rosolve('成功') // rosolve
        //reject('失败') // reject
    }, 1000)
})

p.then(
    data => {
        console.log('then 1 data:', data); // then 1 data: 成功
        return new Promise((rosolve, reject)=> {
            setTimeout(()=> {
                reject('失败 2') // reject
            })
        })
    },
    // 无论是在成功回调还是失败回调中使用 return reject
    //error => {
    //    console.error('then 1 error:', error); // then 1 error: 失败
    //    return new Promise((rosolve, reject)=> {
    //        setTimeout(()=> {
    //            reject('失败 2') // reject
    //        })
    //    })
    //}
)
.then(
    data => {
        console.log('then 2 data:', data);
    },
    // 会被第二个 then 的 失败回调捕获
    error => {
        console.error('then 2 error:', error); // then 2 error: 失败 2
    }
)
// 或者被 catch 捕获
//.catch(error => {
//	console.error('catch 2 error:', error); // catch 2 error: 失败 2
//})
// 情况 3:抛出异常
const p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        rosolve('成功') // rosolve
    }, 1000)
})

p.then(
    data => {
        console.log('then 1 data:', data); // then 1 data: 成功
        throw new Error('抛出异常')
    }
)
.then(
    data => {
        console.log('then 2 data:', data);
    },
    // 会被第二个 then 的 失败回调捕获
    error => {
        console.error('then 2 error:', error); // then 2 error: 抛出异常
    }
)
// 或者被 catch 捕获
//.catch(error => {
//	console.error('catch 2 error:', error); // catch 2 error: 抛出异常
//})

链式调用的作用

1、可以按顺序执行多个异步操作。例如先获取用户信息,再根据用户信息获取部门 ID,再根据部门 ID 获取部门信息…

const p = new Promise((resolve, reject) => {  
    // 获取用户信息  
    setTimeout(() => {  
        const userInfo = {  
            name: '张三',  
            userId: '1'  
        };  
        resolve(userInfo);  
    }, 1000);  
});

p.then(data => {  
    console.log('then 1 data:', data); // {name: '张三', userId: '1'}
    // 根据用户信息获取部门 ID  
    return new Promise((resolve) => {  
        setTimeout(() => {  
            const departId = 1;  
            resolve(departId); // 将 departId 传递给下一个 then  
        }, 1000);  
    });  
})  
.then(data => {  
    console.log('then 2 data:', data); // 1
    // 根据部门 ID 获取部门信息  
    return new Promise((resolve) => {  
        setTimeout(() => {  
            const departInfo = {  
                departId: data,
                departName: '人事部'  
            };  
            resolve(departInfo); // 将 departInfo 传递给下一个 then  
        }, 1000);  
    });  
})  
.then(data => {  
    console.log('then 3 data:', data); // {departId: 1, departName: '人事部'}
});

上面的例子为什么要在 then 中 return new Promise 而不是在 setTimeout 内部中 return?

因为 setTimeout 本身是一个异步函数,它返回的是一个定时器 ID,而不是你函数内部 return 的值。因此,在 setTimeout 中返回的值并不会成为 Promise 链中的下一个值。


2、集中错误处理。在 Promise 链中,任何一步的 .then().catch() 方法都可以捕获并处理错误。这意味着你可以在整个链中只使用一个 .catch() 来捕获和处理所有之前的异步操作中可能出现的错误,这大大简化了错误处理逻辑。

const p = new Promise((rosolve, reject) => {
    setTimeout(() => {
        rosolve('成功') // rosolve
    }, 1000)
})

p.then(data => {
    console.log('then 1 data:', data); // then 1 data: 成功
    return 1
})
.then(data => {
    console.log('then 2 data:', data); // then 2 data: 1
    throw new Error('抛出异常')
    return 2
})
.then(data => {
    console.log('then 3 data:', data); // 没有执行
    return 3
})
.catch(error => {
    console.log('catch 3 error:', error); // catch 3 error: Error: 抛出异常
})

现在再来看看开始的那个问题:

“既然 then 方法的第二个参数也可以用于处理 reject 的情况,那为什么还要有 catch 方法呢?为什么要有两个重复的功能?”

  • catch 使代码结构更加清晰,可以很直观的知道这部分代码是专门用于处理错误的,而不用去 then 中找失败回调,它使得链式调用的流程非常流畅

  • then 的失败回调也并非毫无用处,它可以做到局部处理错误且不影响后续的链的运行,也可以根据拿到的具体值,做条件判断,再决定是否要抛出错误。

Promise 静态方法

Promise.resolve(value)

这个方法接收一个值(value)作为参数,并返回一个 Promise 对象。

  • 如果 value 本身就是一个 Promise 对象,那么返回的 Promise 会“跟随”这个 value Promise 的状态;

  • 如果 value 不是一个 Promise 对象,那么返回的 Promise 会以这个 value 作为成功的结果(即 fulfilled 状态)。

// 传递 Promise 成功对象
const promise = new Promise(resolve => resolve('成功'));
Promise.resolve(promise).then(data => {
    console.log(data); // 成功
});

-----

// 传递 Promise 失败对象
const promise = new Promise((resolve, reject)=> {
    reject('失败')
});
Promise.resolve(promise).then(
    data => {},
    // 或者用 catch
    error => {
        console.log("error:", error, typeof error); // error:失败 string
    }
);

-----

// 传递普通值
Promise.resolve(1).then(data => {
    console.log(data); // 1
});

Promise.reject(reason)

这个方法接收一个原因(reason)作为参数,并返回一个已经处于 rejected 状态的 Promise 对象。这个 reason 会被封装在返回的 Promise 的拒绝原因中。

Promise.reject 与 Promise.resolve 的区别是:

  • Promise.reject 只能返回 rejected 状态的 Promise 对象,而 Promise.resolve 可以返回任意状态的 Promise 对象

  • Promise.reject 的参数就是拒绝原因,而 Promise.resolve 的参数可以理解成是一个 Promise 对象,它包括 PromiseStatus 和 PromiseResult,并以此决定 Promise.resolve 返回的 Promise 对象的状态和结果值。

Promise.reject('失败').catch(data => {
    console.log(data); // 失败
});

-----

const promise = new Promise((resolve, reject)=> {
    reject(1)
});

// 这里,Promise.resolve(promise) 接收了一个已经 rejected 的 Promise 对象  
// 因此,返回的 Promise 也会是 rejected 的,并且传递相同的拒绝原因(数字 1) 
Promise.resolve(promise).then(
    data => {},
    error => {
        console.log("error:", error, typeof error); // 1, number
    }
);

// Promise.reject(reason) 的设计初衷是接收一个拒绝原因(reason),
// 并立即返回一个处于 rejected 状态的 Promise。
// 这里的 reason 应该是任何非 Promise 的值(如字符串、数字、对象等)。
// 虽然技术上可以传入一个 Promise 对象作为 reason,
// 但这并不是 Promise.reject() 的预期用法,也不是一个好的实践。
Promise.reject(promise).then(
    data => {},
    error => {
        console.log("error:", error, typeof error); // 1, object
    }
);

Promise.all(iterable)

这个方法接受一个可迭代对象(通常是一个数组),其中的每一项都是一个 Promise 对象。

它返回一个新的 Promise 对象,这个 Promise 对象在所有输入的 Promise 对象都成功时才会成功,并且它的结果是一个数组,包含所有输入 Promise 对象的结果;如果其中一个 Promise 对象失败,那么这个新的 Promise 对象会立即失败,并返回那个失败的原因。

const p1 = Promise.resolve(3);
const p2 = 42;
const p3 = new Promise((resolve, reject) => setTimeout(resolve, 100, 'foo'));

Promise.all([p1, p2, p3]).then(values => {
  console.log('values:', values); // [3, 42, 'foo']
}).catch(error => {
  console.log('error:', error);
});
const p1 = Promise.resolve(3);
const p2 = 42;
const p3 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));

Promise.all([p1, p2, p3]).then(values => {
  console.log('values:', values);
}).catch(error => {
  console.log('error:', error); // foo
});

拓展:setTimeout(function[, delay, arg1, arg2, ...]);

一旦定时器到期,setTimeout 的第三个及以后的参数会作为第一个 function() 的参数传进去。

所以上面的例子实际上就是 setTimeout(() => {resolve('foo')}, 100)

Promise.all() 在处理多个并发请求的时候非常有效。

这里经常会有一个面试题:如何控制 JavaScript 并发一百个请求?

all、race、any、allSettled

与 all 类似的还有 race、any、allSettled,他们都接收一个数组,数组中的每一项都是一个 Promise 对象,区别如下:

  • all:只有数组中的 Promise 都成功时才进入 then,有一个失败则进入 catch

  • race:数组中最先完成的 Promise(不是按参数顺序),成功则进入 then,失败则进入 catch

  • any:数组中最先成功的 Promise,其 resolve 的内容会进入 then,所有的 Promise 都失败则进入 catch

  • allSettled:数组中所有的 Promise 完成时(无论失败还是成功),会进入 then,永远不会进入 catch

Promise 和 async/await

async

async 用于将一个函数定义为异步函数,且该异步函数的返回值是一个 Promise 对象

const fn = async function() {
    console.log(1)
}
const p = fn()
console.log(p, p instanceof Promise)
/*
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: undefined
true
*/
const fn = async function() {
    console.log(1) // 1
    return 2
}
const p = fn()
console.log(p, p instanceof Promise)
/*
[[Prototype]]: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 2
true
*/

所以 async 可以不需要 await:

const fn = async function() {
    console.log(1) // 1
    return 2
};
const p = fn(); // p 是一个 promise 对象
p.then(res => {
    console.log(res) // 2
});

但是要注意下面这种情况是错误的示范:

const fn = async function() {
    setTimeout(() => {
        console.log(1)
        return 2
    })
}
const p = fn()
console.log(p, p instanceof Promise)
p.then(res => {
    console.log('res:', res) // undefined
})

为什么这里的 res 打印的是 undefined?这个问题其实在上面的【链式调用的作用】中有说到过:

因为 setTimeout 本身是一个异步函数,它返回的是一个定时器 ID,而不是你函数内部 return 的值。因此,在 setTimeout 中返回的值并不会成为 Promise 链中的下一个值。

await

await 的作用是等待一个 Promise 对象解决(resolve)或拒绝(reject),它会暂停 await 后面的代码的执行。

  • 当 await 等待的 Promise 被解决,会返回解决值

  • 当 await 等待的 Promise 被拒绝,会抛出异常

const fn = async function() {
    console.log(1)
    return 2
};

async function test() {
    const result = await fn() // 返回解决值
    console.log("result:", result)
}
console.log("主线程执行中...")
test()

/*
主线程执行中...
1
result:2
*/
const p = Promise.reject('失败')

async function test() {
    try {
        const result = await p
        console.log("result:", result) // 不执行
    } catch (error) {
        console.log('异常:', error);
    }
}
console.log("主线程执行中...")
test()

/*
主线程执行中...
异常: 失败
*/

如果 await 等待一个非 Promise 对象会怎样?

async function test() {
    const result = await 'str'
    console.log("result:", result)
}
console.log("主线程执行中...")
test()

/*
主线程执行中...
result: str
*/

答案:JavaScript 会尝试将该值隐式地转换为一个已解决的 Promise 对象。

为什么 await 一定要在 async 中?

  • 语法限制,单独使用 await 会报错

  • await 的作用是等待一个 Promise 对象被解决或拒绝,而 async 函数会隐式地返回一个 Promise 对象

async/await

为什么说 async/await 可以简化 Promise 呢?我们还是使用之前【链式调用的作用】中的一个例子;

先获取用户信息,再根据用户信息获取部门 ID,再根据部门 ID 获取部门信息…

使用 Promise:

const p = new Promise((resolve, reject) => {  
    // 获取用户信息  
    setTimeout(() => {  
        const userInfo = {  
            name: '张三',  
            userId: '1'  
        };  
        resolve(userInfo);  
    }, 1000);  
});

p.then(data => {  
    console.log('then 1 data:', data); // {name: '张三', userId: '1'}
    // 根据用户信息获取部门 ID  
    return new Promise((resolve) => {  
        setTimeout(() => {  
            const departId = 1;  
            resolve(departId); // 将 departId 传递给下一个 then  
        }, 1000);  
    });  
})  
.then(data => {  
    console.log('then 2 data:', data); // 1
    // 根据部门 ID 获取部门信息  
    return new Promise((resolve) => {  
        setTimeout(() => {  
            const departInfo = {  
                departId: data,
                departName: '人事部'  
            };  
            resolve(departInfo); // 将 departInfo 传递给下一个 then  
        }, 1000);  
    });  
})  
.then(data => {  
    console.log('then 3 data:', data); // {departId: 1, departName: '人事部'}
});

使用 async/await:

async function getUserInfo() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const userInfo = {
                name: '张三',  
                userId: '1'  
            };
            resolve(userInfo);
        }, 1000);
    });
}

async function getDepartId(userInfo) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let departId
            if(userInfo.userId == 1) {
                departId = 1;
            } 
            resolve(departId);
        }, 1000);
    });
}

async function getDepartInfo(departId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let departInfo
            if(departId == 1) {
                departInfo = {  
                    departId: departId,
                    departName: '人事部'  
                };
            } 
            resolve(departInfo);
        }, 1000);
    });
}

async function test() {  
    try {  
        const userInfo = await getUserInfo();  
        console.log('用户信息:', userInfo);

        const departId = await getDepartId(userInfo);
        console.log('部门 ID:', departId);

        const departInfo = await getDepartInfo(departId);
        console.log('部门信息:', departInfo);
    } catch (error) {  
        console.error('发生错误:', error);  
    }  
}  

test()

/*
用户信息: {name: '张三', userId: '1'}
部门 ID: 1
部门信息: {departId: 1, departName: '人事部'}
*/

自定义封装 Promise 函数

有助于理解 Promise 原理,可以不看。

class Promise {
    constructor(executor) {
        this.PromiseState = 'pending'
        this.PromiseResult = null
        this.callback = [] //then的参数值容器
        const that = this

        function resolve(data) {
            //保证状态值只能改一次
            if (that.PromiseState !== 'pending') return
            //设置状态值为成功
            that.PromiseState = 'fullfilled'
            //设置promise结果值
            that.PromiseResult = data
            //实现链式调用
            setTimeout(() => {
                that.callback.forEach(item => {
                    item.onResolve(data)
                })
            })
        }

        function reject(data) {
            if (that.PromiseState !== 'pending') return
            //设置状态值为失败
            that.PromiseState = 'rejected'
            //设置promise结果值
            that.PromiseResult = data
            setTimeout(() => {
                that.callback.forEach(item => {
                    item.onReject(data)
                })
            })
        }
        //设置通过throw 改变状态值
        try {
            //立即执行保证同步调用(执行器函数)
            executor(resolve, reject);
        } catch (e) {
            reject(e) //e为throw ‘error’ 抛出异常时的值
        }
    }

    //指定原型then方法
    then(onResolve, onReject) {
        const that = this
        //判断回调函数参数 设置参数默认值
        if (typeof onReject !== 'function') {
            onReject = reason => {
                throw reason
            }
        }
        if (typeof onResolve !== 'function') {
            onResolve = value => value
        }

        return new Promise((resolve, reject) => {
            //封装重复函数
            function callback(type) {
                try {
                    let result = type(that.PromiseResult)
                    if (result instanceof Promise) {
                        //if返回对象为promise
                        result.then(v => {
                            resolve(v)
                        }, r => {
                            reject(r)
                        })
                    } else {
                        //设置返回值的promise对象成功值
                        resolve(result)
                    }
                } catch (e) {
                    reject(e) //当抛出异常
                }
            }
            //console.log(this)// 运用箭头函数的原因 这里的this指向函数定义的代码块
            //设置成功的回调执行
            if (this.PromiseState === 'fullfilled') {
                //保证then方法内回调为异步执行
                setTimeout(() => {
                    callback(onResolve)
                })
            }
            //设置失败回调执行
            if (this.PromiseState === 'rejected') {
                setTimeout(() => {
                    callback(onReject)
                })
            }
            //设置改变状态后再执行then回调(对于回调方式改变状态值)
            if (this.PromiseState === 'pending') {

                this.callback.push({
                    onResolve: function() { //处理promise异步修改对象状态(实现then回调函数执行在异步回调之后)
                        callback(onResolve)
                    },
                    onReject: function() {
                        callback(onReject)
                    }
                })
            }

        })
    }

    //添加catch方法
    catch (onreject) {
        return this.then(undefined, onreject)
    }

    //给构造函数promise封装resolve 和reject方法
    static resolve(value) {
        return new Promise((resolve, reject) => {
            if (value instanceof Promise) {
                value.then(v => {
                    resolve(v)
                }, r => {
                    reject(r)
                })
            } else {
                resolve(value)
            }
        })
    }
    static reject(reason) {
        return new Promise((resolve, reject) => {
            reject(reason)
        })
    }

    //封装all方法
    static all(values) {
        return new Promise((resolve, reject) => {
            let count = 0
            let arr = []
            for (let i = 0; i < values.length; i++) {
                values[i].then(v => {
                    count++
                    arr[i] = v
                    if (count === values.length) {
                        resolve(arr)
                    }
                }, r => {
                    reject(r)
                })
            }
        })
    }

    //封装race方法
    static race(values) {
        return new Promise((resolve, reject) => {
            for (let i = 0; i < values.length; i++) {
                values[i].then(v => {
                    resolve(v)
                }, r => {
                    reject(r)
                })
            }
        })
    }

}

Axios

Axios 是一个基于 Promise 的用于发送 HTTP 请求的客户端库。它会返回一个 Promise 对象,代表 HTTP 请求的结果。

并且 Axios 还可以支持请求和响应拦截器、请求取消、HTTP 方法别名、自动转换 JSON 数据等高级功能。

示例:

// get 请求
axios({
    url: 'http://example.com/resource',
    method: 'get',
    params: {
        id: 1,
        name: '张三'
    },
    headers: {
        'Content-Type': 'application/json'
    }
})
.then(response => {
    console.log(response);
})
.catch(error => {
    console.error(error);
});

// post 请求
axios({
    url: 'http://example.com/resource',
    method: 'post',
    // 使用 data 接收参数
    data: {
        id: 1,
        name: '张三'
    },
    headers: {
        'Content-Type': 'application/json'
    }
})
.then(response => {
    console.log(response);
})
.catch(error => {
    console.error(error);
});

Axios 特点和优势

特点:

  • 从浏览器中创建 XMLHttpRequests

    • Axios 的浏览器 API 能够发送 XMLHttpRequests,这使得它能够从浏览器中以异步的方式与服务器通信。
  • 从 node.js 中创建 HTTP 请求

    • 在服务器端,Axios 提供了相似的 API 通过 HTTP 方式与其他服务进行交互。
  • 支持 Promise API

    • 使用 Promise API 实现请求和响应的同时处理,让异步代码变得简洁,并方便了错误处理。
  • 拦截请求和响应

    • Axios 的拦截器让你可以在请求发送到服务器前或服务器响应返回应用前更容易地处理它们。
  • 转换请求和响应的数据

    • 自动转换请求数据和响应数据为 JSON 格式。
  • 取消请求

    • 提供了通信接口来取消请求,避免不必要的网络活动。
  • 统一错误处理

    • Axios 使得错误管理和处理集中化,可以对所有 HTTP 请求统一处理异常。

Axios 与 Fetch 的区别

Fetch 示例:

// get 请求
fetch(`http://example.com/resource?id=1&name=张三`, {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json'
    }
})
.then(response => {
    // 解析 JSON 响应
    return response.json();
})
.then(data => {
    // 处理数据
    console.log(data);
})
.catch(error => {
    // 错误处理
    console.error('Fetch error:', error);
});

// post 请求:
fetch(`http://example.com/resource`, {
    method: 'GET',
    headers: {
        'Content-Type': 'application/json'
    },
    // 使用 body 接收参数
    body: JSON.stringify({
        id: 1,
        name: '张三'
    })
})
.then(response => {
    // 解析 JSON 响应
    return response.json();
})
.then(data => {
    // 处理数据
    console.log(data);
})
.catch(error => {
    // 错误处理
    console.error('Fetch error:', error);
});

比较:

  • 都支持链式调用,都是基于 Promise。

  • 原生

    • Axios 是一个第三方库

    • Fetch 是一个原生的 JavaScript API,即不需要引用

  • 拦截处理

    • Axios 能够拦截请求和响应。

    • Fetch 不支持请求和响应的拦截。

  • 取消请求

    • Axios 使用 CancelToken 取消请求。

    • Fetch 使用 AbortController 取消请求。

  • 错误处理

    • Axios 将任何状态码 >= 200 和 < 300 的响应视为有效响应,其他的都将被拒绝。

    • Fetch 只有在网络故障时或请求被阻止时才会拒绝 promise;如果 HTTP 状态码为 404 或 500,它会将 promise 的状态标记为 resolve,并且需要开发者进行额外检查。

  • JSON 数据处理

    • Axios 会自动将请求和响应转为 JSON 格式。

    • Fetch 需要手动将请求转为 JSON 字符串,手动将响应转为 JSON

  • 处理 Cookie

    跨域请求中 Axios 和 Fetch 默认都不会携带 cookie,需要手动设置:

    • Axios 设置 withCredentials: true

    • Fetch 设置 credentials: 'include'

Axios 响应结构

字段类型描述
dataany服务器响应的数据。Axios 默认会尝试将此数据转换为 JSON 对象
statusnumberHTTP 响应的状态码,如 200 表示成功,404 表示未找到等
statusTextstringHTTP 状态码的文本信息,如 OK 或 Not Found
headersObject响应头。一个包含所有响应头的对象,键名为响应头的名称,键值为响应头的值。
configObject请求时使用的所有配置选项
requestXMLHttpRequest生成当前响应的请求对象,在浏览器中,它是 XMLHttpRequest 实例(几乎不用)

你可以通过在 .then() 或者 async/await 捕获响应结果来访问这些字段。例如:

axios.get('/some-url')
  .then(response => {
    console.log(response.status); // 例如:200
    console.log(response.data);   // 服务器端返回的实际数据
  });
  
async function fetchData() {
  const response = await axios.get('/some-url');
  console.log(response.status);
  console.log(response.data);
}

Axios 请求拦截器(Interceptors)

Axios 拦截器允许开发者在请求被发送到服务器和响应到达客户端之前,对它们进行处理和修改。

请求拦截器的作用

  1. 添加公共头部。

  2. 请求数据序列化:在请求发送到服务器之前对请求数据进行处理或转换。

  3. 设置条件请求:根据特定的条件或业务逻辑取消或更改即将发送的请求。

  4. 日志记录:记录即将发送的请求的详细信息,方便调试。

  5. 设置通用参数:例如,添加时间戳参数以防止 GET 请求被浏览器缓存。

响应拦截器的作用

  1. 统一错误处理:可以在拦截器中捕捉到错误响应,并进行统一的错误处理。例如,根据 HTTP 状态码显示错误信息。

  2. 数据转换:将响应数据从 JSON 转换为其他格式,或者在数据被传递给 then 或 catch 方法之前,进行预处理。

  3. 自动刷新 Token:当收到 token 过期的响应时,可以发送请求来刷新 token,然后重试原始请求。

  4. 性能监控:可以计算请求响应时间,用于应用程序的性能监控。

拦截器例子:

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    config.headers.common['Authorization'] = `Bearer ${token}`;
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
}, function (error) {
    // 对响应错误做点什么
    if (error.response && error.response.status === 401) {
        // 处理授权错误
    }
    return Promise.reject(error);
});

return Promise.reject 的作用就是中断 Promise 链中的后续 .then() 方法调用,并允许 .catch() 方法捕获到这个错误。

但是要注意 return Promise.reject 并不会中断 HTTP 请求的发送和接收。例如一个 HTTP 请求刚进入 axios.interceptors.request 的时候就立即使用 Promise.reject,那么这个 HTTP 请求还是会被发送到服务器。这与 CancelToken 不同,CancelToken 会取消该请求的发送。

Axios 如何取消请求

使用 CancelToken.source 工厂方法创建取消令牌:

// 1. 引入 axios
const axios = require('axios');

// 2. 创建一个取消令牌源
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

// 3. 发起请求
axios.get('https://jsonplaceholder.typicode.com/posts', {
  // 4. 将取消令牌作为请求配置的一部分传递
  cancelToken: source.token
})
  // then 中处理请求成功的情况
  .then(response => {
    console.log('请求成功:', response.data);
  })
  // catch 中处理错误和取消的情况
  .catch(error => {
    // 6. 处理请求错误或取消的情况
    if (axios.isCancel(error)) {
      console.log('请求取消', error.message);
    } else {
      console.error('请求错误', error);
    }
  });

// 5. 在某个条件下取消请求
// 在 1 秒后取消请求
setTimeout(() => {
  source.cancel('取消原因');
}, 1000);

总结实现步骤:

  1. 创建取消令牌源

    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
    
  2. 发起请求时将取消令牌作为请求配置的一部分传递

    {
        cancelToken: source.token,
        ...
    }
    
  3. 在某个条件下使用 source.cancel() 取消请求

    source.cancel('取消原因');

  4. 在 catch() 中使用 axios.isCancel() 处理被取消的请求(非必须)

    .catch(error => {  
        if (axios.isCancel(error)) {  
            console.log('请求被取消:', error.message);  
        }
        ...
    });  
    

Axios 默认配置

如果想为所有的请求统一配置某些设置,可以使用默认配置 axios.defaults:

// 设置全局的 baseURL 默认值
axios.defaults.baseURL = 'https://jsonplaceholder.typicode.com';

// 设置全局的 headers 默认值
axios.defaults.headers.common['Authorization'] = 'Bearer your.token.here';

// 设置全局的 timeout 默认值
axios.defaults.timeout = 2500;

Axios 中如何创建实例(instance)

创建实例的目的是允许你为该实例的所有请求预定义一些配置项,比如基础 URL、请求头、超时时间等。这样做的好处是在同一个应用中可以重用这一配置,避免每次发送请求时都需要设置相同的配置项。

要创建 Axios 的新实例,你可以使用 axios.create() 方法并传递一个配置对象。以下是创建实例的示例:

// 创建实例
const axiosInstance = axios.create({
  baseURL: 'https://api.example.com', // 所有请求的基础 URL
  timeout: 1000, // 全部请求的超时时间
  headers: {'X-Custom-Header': 'foobar'} // 全局自定义的请求头
});

// 使用实例发起 GET 请求
axiosInstance.get('/users')
  .then(response => {
    console.log(response.data);
  });

// 使用实例发起 POST 请求
axiosInstance.post('/users', {
  data: {username: 'example'}
})
  .then(response => {
    console.log(response.data);
  });

// 每个实例都能够单独配置拦截器
// 添加请求拦截器
axiosInstance.interceptors.request.use(config => {
  config.headers['Authorization'] = 'Bearer token'; // 为即将发出的请求动态设置授权头部
  return config;
});

// 添加响应拦截器
axiosInstance.interceptors.response.use(response => {
  // 处理响应数据
  return response;
}, error => {
  // 处理响应错误
  return Promise.reject(error);
});

默认配置和实例配置的区别:

区别默认配置实例配置
作用范围全局,对所有请求生效局部,只对当前实例的请求生效
配置方式直接修改 axios.defaults使用 axios.create() 创建新的实例并配置
适用场景统一配置所有请求针对不同的请求需要不同配置时使用

Axios 性能优化

  1. 使用缓存避免不必要的请求

  2. 并发请求

  3. 使用请求和响应拦截器优化错误处理

  4. 取消重复请求

  5. 压缩数据:确保服务器能发送压缩的响应数据(如使用 gzip 或 br 压缩)。这减少了传输的数据量,从而提高了性能。

  6. 使用 HTTP2:如果服务器支持 HTTP2,使用 HTTP2 可以显著提高性能,因为它提供了头部压缩、多路复用等优势。你可以考虑选择或升级到支持 HTTP2 的服务器。

Axios 处理大型 JSON 数据

设置 responseType: 'stream' 表示获取一个可读的流(stream),然后你可以逐步读取和解析这个流,而不是一次性将整个响应体加载到内存中。这对于大型文件特别有用。

流是原始的二进制数据。你需要自己实现逻辑来将流中的数据转换为 JSON。

axios({
  method: 'get',
  url: 'http://example.com/large.json',
  responseType: 'stream'
})
.then((response) => {
  // 处理流式响应数据
});

Axios 完整封装示例

// request.js
import axios from 'axios';

// 创建一个 axios 实例
const service = axios.create({
    baseURL: process.env.VUE_APP_BASE_API, // 基础路径
    timeout: 5000 // 请求超时时间
});

// 存储正在进行的请求
const pendingRequests = new Map();
const requestTimestamp = new Map(); // 存储请求的时间戳

// 请求拦截器
service.interceptors.request.use(
    config => {
        const token = localStorage.getItem('token'); // 假设 token 存储在 localStorage
        if (token) {
            config.headers['Authorization'] = `Bearer ${token}`; // 设置 Authorization 头
        }

        const requestKey = `${config.method}_${config.url}`;

        // 获取当前时间戳
        const currentTime = Date.now();

        // 检查是否存在已有请求且在1秒内
        if (pendingRequests.has(requestKey)) {
            const lastRequestTime = requestTimestamp.get(requestKey);

            // 只有在1秒内才取消请求
            if (currentTime - lastRequestTime < 1000) {
                const cancelTokenSource = pendingRequests.get(requestKey);
                cancelTokenSource.cancel('Operation canceled due to new request.');
            }
        }

        // 更新时间戳和创建新的 CancelToken
        requestTimestamp.set(requestKey, currentTime);
        const cancelTokenSource = axios.CancelToken.source();
        config.cancelToken = cancelTokenSource.token;
        pendingRequests.set(requestKey, cancelTokenSource);

        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

// 响应拦截器
service.interceptors.response.use(
    response => {
        const requestKey = `${response.config.method}_${response.config.url}`;
        pendingRequests.delete(requestKey); // 请求完成后删除
        requestTimestamp.delete(requestKey); // 请求完成后删除时间戳

        // 根据具体需求处理响应数据
        return response.data; // 返回数据
    },
    error => {
        const requestKey = `${error.config.method}_${error.config.url}`;
        pendingRequests.delete(requestKey); // 请求失败后删除
        requestTimestamp.delete(requestKey); // 请求失败后删除时间戳

        // 这里可以根据错误状态码做相应处理
        if (axios.isCancel(error)) {
            console.log('请求被取消:', error.message);
        } else {
            console.error('请求错误:', error);
        }

        return Promise.reject(error);
    }
);

export default service;

http://www.kler.cn/a/563013.html

相关文章:

  • 【人工智能】数据挖掘与应用题库(101-200)
  • 【嵌入式原理设计】实验六:倒车控制设计
  • 蓝耘服务器与DeepSeek的结合:引领智能化时代的新突破
  • 30 分钟从零开始入门 CSS
  • Go入门之接口
  • 【Pandas】pandas Series backfill
  • 解决Moodo调节心情模块-大声喊出来无法测量出音频分贝
  • Java 中 ArrayList 和 LinkedList 的区别及使用场景
  • 十、大数据资源平台功能架构
  • RabbitMQ系列(零)概要
  • 17164字符迁移
  • P9231 [蓝桥杯 2023 省 A] 平方差--巧妙统计奇数的个数!
  • uniapp 小程序如何实现大模型流式交互?前端SSE技术完整实现解析
  • CF1305C Kuroni and Impossible Calculation
  • 现在集成大模型的IDE,哪种开发效率最高
  • 初识JavaFX-IDEA中创建第一个JavaFX项目
  • Project #0 - C++ Primer前置知识学习
  • ARM Coretex-M核心单片机(STM32)找到hardfault的原因,与hardfault解决方法
  • 算法题(79):两个数组的交集
  • seacmsv9注入管理员账号密码+order by+limit