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

JavaScript 期约与异步函数的学习笔记

同步与异步的概念

JavaScript 是一门单线程的语言,这意味着它在任何给定的时间只能执行一个任务。

然而,JavaScript 通过异步编程技术来处理并发操作,以避免阻塞主线程的情况。

在这里插入图片描述

在上图中,同步行为的进程 A 因为等待进程 B 执行完而被阻塞了一段时间。异步行为的进程 A 则会继续执行,等到进程 B 有了结果,它再告知进程 A 来处理。

异步行为是为了优化计算量大而耗时长的操作,但也并非只能处理该类情况,只要需要执行某个异步操作且不想主线程被阻塞,那么都可以使用异步编程。异步行为类似于系统中断。

同步行为对应内存中顺序执行的处理器指令,指令执行完后就容易推断出程序的状态,每个操作都是可预测性的。

设计一个这样的异步系统是困难的,因为你不知道异步结果什么时候可以获取。

JavaScript 最初的异步编程方式:回调

使用回调作为异步编程的方式:回调函数作为另一个函数的参数,并在某个事件发送或异步操作完成后执行。

function fetchData(callback){
	setTimeout(function(){
		callback('Data fetched');
	},1000);
}

fetchData(function(result){
	console.log(result);
});

从宏任务(macrotask)和微任务(microtask)的观点来看这段代码,我们可以将其分为以下几个步骤:

  1. 宏任务1:开始执行主程序,调用fetchData函数。
  2. 宏任务2fetchData函数中的setTimeout会将回调函数注册为一个宏任务,该宏任务将在1秒后执行。
  3. 宏任务3:主程序继续执行,没有其他宏任务,因此等待。
  4. 微任务1:当宏任务2(setTimeout的回调)执行时,它调用传递给它的回调函数,并将其视为微任务。这个微任务将立即执行,因为它没有等待。
  5. 微任务2:微任务1执行完成后,主程序没有其他宏任务要执行,但是会检查是否有待处理的微任务。在这种情况下,微任务2是fetchData函数调用中的回调函数中的console.log(result)语句。

加上对失败回调的处理:

function fetchData(successCallback, errorCallback) {
  setTimeout(function () {
    // 模拟一个错误,你可以根据具体情况处理错误
    const error = null; // 这里假设没有错误
    if (error) {
      errorCallback(error); // 调用失败回调函数并传递错误
    } else {
      successCallback('Data fetched'); // 调用成功回调函数并传递数据
    }
  }, 1000);
}

// 使用 fetchData 函数
fetchData(
  function (data) {
    console.log('Success:', data);
  },
  function (error) {
    console.error('Error:', error);
  }
);

这种模式有很多弊端:首先需要在指定时间内才能得到异步函数的返回值,其次需要提前定义好回调函数。

最后,多个异步操作嵌套在一起,会形成回调地狱

function fetchData(callback) {
  setTimeout(function () {
    callback('Data fetched');
  }, 1000);
}

function processData(data, successCallback, errorCallback) {
  setTimeout(function () {
    // 模拟一个错误
    const error = null; // 这里假设没有错误
    if (error) {
      errorCallback(error);
    } else {
      successCallback('Data processed');
    }
  }, 1000);
}

function saveData(data, successCallback, errorCallback) {
  setTimeout(function () {
    // 模拟一个错误
    const error = new Error('Save failed');
    errorCallback(error);
  }, 1000);
}

fetchData(function (data) {
  processData(
    data,
    function (processedData) {
      saveData(
        processedData,
        function () {
          console.log('Data saved successfully');
        },
        function (error) {
          console.error('Error saving data:', error);
        }
      );
    },
    function (error) {
      console.error('Error processing data:', error);
    }
  );
});

在这里插入图片描述

嵌套的回调难以阅读和维护。

新时代:期约-Promise

期约是对尚不存在结果的一个替身。

期约提供了一种更清晰和可维护的方式来处理异步操作,避免了回调地狱的问题。

期约是基于 Promises/A+ 规范建立的。

期约的状态

Promise 是 ECMAScript6 新增的引用类型,是一个具有状态的对象。

它有如下三种状态:

  1. 待定-pending。期约最初始的状态。
  2. 兑现-fullfilled。也可以称为 解决-resolved
  3. 拒绝-rejected

在这里插入图片描述

状态一经改变,不可修改。

期约的状态是私有的,只能在内部进行操作,不能被外部代码检测和修改。

初始化期约-new Promise(executor)

使用 XMLHttpRequest 模拟异步操作创建期约:

// 建立请求,创建期约的工厂函数
function makeRequest(url){
	return new Promise((resolve,reject)=>{
		// 异步操作
		const xhr = new XMLHttpRequest();
		xhr.open('GET', url);
		xhr.onload = function () {
	      if (xhr.status >= 200 && xhr.status < 300) {
	        // 请求成功,将响应文本作为成功结果
	        resolve(xhr.responseText);
	      } else {
	        // 请求失败,将错误信息作为失败原因
	        reject('请求失败,状态码: ' + xhr.status);
	      }
	    };
	    xhr.onerror = function () {
	      // 请求错误,将错误信息作为失败原因
	      reject('网络错误');
	    };
	    xhr.send();
	});
}

const myPromise=makeRequest('https://example.com/api/data');

使用 new 创建 Promise 实例时,需要传入一个执行器(executor)函数作为参数,该函数接受两个参数:resolvereject

前面提到期约的状态只能在内部操作,这个操作就是在执行器函数中完成的。

resolve 会将 Promise 状态切换为 fullfilledreject则会将其切换为 rejected。同时,调用 reject 会抛出错误。

执行器函数是期约的初始化程序,且是同步执行的,当初始化期约时就已经改变了期约的状态。

期约的构造器方法|静态方法之二

Promise.resolve()

Promise.resolve 是一个静态方法,它返回一个已解决(fulfilled)的 Promise 对象,并可以选择将一个值解析为成功的结果。

如果传递给 Promise.resolve 的值本身已经是一个 Promise 对象,则它会保持不变(不会再次解析)。

setTimeout(console.log, 0, Promise.resolve());
setTimeout(console.log, 0, Promise.resolve(1));

const p = new Promise(()=>{});
setTimeout(console.log, 0, Promise.resolve(p));
setTimeout(console.log, 0, p === Promise.resolve(p));

在这里插入图片描述

Promise.reject()

Promise.reject 是一个静态方法,用于创建一个已拒绝(rejected)的 Promise 对象,并指定一个原因(通常是一个错误对象)作为拒绝的原因。

Promise.resolve 不同,Promise.reject 不会解析传递给它的值,而是将其作为拒绝原因直接传递给 Promise 对象。

setTimeout(console.log, 0, Promise.reject());
setTimeout(console.log, 0, Promise.reject(1));

const p = new Promise(()=>{});
setTimeout(console.log, 0, Promise.reject(p));
setTimeout(console.log, 0, p === Promise.reject(p));

在这里插入图片描述

注意到,错误被抛出但没有被捕获(Uncaught)。我们给它套上 try...catch 试试。

try {
    setTimeout(console.log, 0, Promise.reject());
} catch (e) {
    console.log(e);
}

在这里插入图片描述

这就奇怪了,为什么还是没有捕获到错误呢?

因为 try..catch 只能捕获同步代码中的错误,它位于当前执行栈中,而 Promise.reject 会被推入微任务队列,当当前执行栈执行完后,再执行它。

要和异步代码交互,只能使用期约的实例方法——Promise.prototype.thenPromise.prototype.catchPromise.prototype.finally

期约的实例方法

期约的实例方法是连接外部同步代码和内部异步代码的桥梁。

任何暴露的异步结构——或者叫做期约的实例方法中都实现了一个 then() 方法。这个方法被认为实现了一个 Thenable 接口。

Promise.prototype.then

Promise.prototype.then 是 Promise 对象的一个实例方法,用于附加回调函数来处理 Promise 的解决(fulfilled)和拒绝(rejected)状态。

promise.then(onFulfilled, onRejected)

then 方法返回一个新的 Promise 对象,该对象有以下几种情况:

  1. 如果 onFulfilledonRejected 返回一个值(不是 Promise),则返回的新 Promise 将以该值解决。
  2. 如果 onFulfilledonRejected 抛出异常,则返回的新 Promise 将以该异常作为原因拒绝。注意返回错误对象会把该错误对象包装在一个解决的期约中。
  3. 如果 onFulfilledonRejected 返回一个 Promise,则返回的新 Promise 将与该返回的 Promise 具有相同的状态和结果。

下面是一个示例:

const promise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const randomNumber = Math.random();
    if (randomNumber < 0.5) {
      resolve(`成功:${randomNumber}`);
    } else {
      reject(`失败:${randomNumber}`);
    }
  }, 1000);
});

promise.then(
  (result) => {
    console.log(`成功回调:${result}`);
  },
  (error) => {
    console.error(`失败回调:${error}`);
  }
);

then 方法返回一个新的 Promise 对象,那么就可以链式调用它。

// 模拟延迟
function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

function fetchUserData() {
  return delay(1000).then(() => {
    return { username: "john_doe", email: "john@example.com" };
  });
}

function fetchUserPosts(username) {
  return delay(1000).then(() => {
    return ["Post 1", "Post 2", "Post 3"];
  });
}

function displayUser(username, posts) {
  console.log(`Username: ${username}`);
  console.log("Posts:");
  posts.forEach((post, index) => {
    console.log(`${index + 1}. ${post}`);
  });
}

fetchUserData()
  .then((user) => {
    console.log("Fetching user data...");
    console.log(user);
    return fetchUserPosts(user.username);
  })
  .then((posts) => {
    console.log("Fetching user posts...");
    console.log(posts);
    displayUser("john_doe", posts);
  })
  .catch((error) => {
    console.error("Error:", error);
  });

输出:

Fetching user data...
{ username: 'john_doe', email: 'john@example.com' }
Fetching user posts...
[ 'Post 1', 'Post 2', 'Post 3' ]
Username: john_doe
Posts:
1. Post 1
2. Post 2
3. Post 3

then 的链式调用避免了回调地狱,提高了代码的可维护性。

Promise.prototype.catch

虽然 then 方法已经可以为期约添加拒绝处理程序——promise.then(null,onRejected),但是这样不是很美观,于是就有了语法糖:promise.catch(onRejected)

行为上与 then 是一致的。这里不再细究。

Promise.prototype.finally

finally 方法用于给程序添加 onFinally 回调函数,该回调无论 Promise 对象是 fullfilled 还是 rejected 都会执行。

const promise = new Promise((resolve, reject) => {
  // 模拟异步操作
  setTimeout(() => {
    const randomNumber = Math.random();
    if (randomNumber < 0.5) {
      resolve(`成功:${randomNumber}`);
    } else {
      reject(`失败:${randomNumber}`);
    }
  }, 1000);
});

promise
  .then(
    (result) => {
      console.log(`成功回调:${result}`);
    },
    (error) => {
      console.error(`失败回调:${error}`);
    }
  )
  .finally(() => {
    console.log("不管成功或失败,都会执行这里的回调");
  });

大多数情况下,调用 finally 会原样返回期约。

如果期约是待定的且在 onFinally 处理程序中抛出错误或返回了一个拒绝期约,则会返回一个拒绝期约。

这个方法避免了 thencatchonFufilledonRejected 中出现重复冗余代码。

未完待续。。。

相关文章:

  • 自定义事件的使用
  • 【FAQ】安防监控系统/视频云存储/监控平台EasyCVR服务器解释器出现变更该如何修改?
  • 代理IP与Socks5代理:跨界电商时代的网络安全与数据引擎
  • 测试与FastAPI应用数据之间的差异
  • Spring Boot虚拟线程与Webflux在JWT验证和MySQL查询上的性能比较
  • arcgis拓扑检查实现多个矢量数据之间消除重叠区域
  • 小程序自定义tabbar
  • Activiti回退与跳转节点
  • python基础语法(四)
  • 什么是HTTP状态码?常见的HTTP状态码有哪些?
  • 下载HTMLTestRunner并修改
  • java.math.BigDecimal常用操作
  • Docker命令
  • ES6-解构赋值
  • python爬虫爬取电影数据并做可视化
  • ip地址怎么改网速快
  • Mac 安装软件各种报错解决方案
  • HarmonyOS应用开发—资源分类与访问
  • MFC多文档程序,从菜单关闭一个文档和直接点击右上角的x效果不同
  • 【漏洞复现】泛微e-office OfficeServer2.php 存在任意文件读取漏洞复现
  • 美国第一季度经济环比萎缩0.3%
  • 视频丨中国海警位中国黄岩岛领海及周边区域执法巡查
  • 水利部将联合最高检开展黄河流域水生态保护专项行动
  • 国际锐评:菲律宾“狐假虎威”把戏害的是谁?
  • 暗蓝评《性别打结》丨拆解性别之结需要几步?
  • 书业观察|一本书的颜值革命:从毛边皮面到爆火的刷边书