JavaScript 期约 Promise 总结

同步与异步的概念

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 中出现重复冗余代码。

期约的非重入-non-reentrancy特性

当一个期约进入"落定状态"(settled state)时,与该状态相关的处理程序(例如,.then.catch 中的回调函数)不会立即执行,而是会被排入执行队列,等待事件循环处理。

一旦Promise对象进入了已成功或已失败状态,它就被认为是"落定",不再处于悬挂状态。在这个状态下,Promise的结果或错误已经确定,不会再发生变化。

与附加处理程序相关的同步代码(即添加处理程序的代码之后的代码)会在处理程序执行之前优先执行。

这个特性的存在是为了确保期约处理程序的执行不会中断当前执行的同步代码块。

Promise.resolve().then(() => console.log("onResolved 执行"));
console.log("同步代码执行");

输出

同步代码执行
onResolved 执行

更为精彩的示例:

// 创建一个Promise
const myPromise = new Promise((resolve, reject) => {console.log("Promise开始执行");// 模拟异步操作setTimeout(() => {resolve("成功"); // 期约进入落定状态}, 2000);console.log("resolve() 返回");
});console.log("Promise创建完成");// 添加处理程序
myPromise.then((result) => {console.log(`处理程序执行,结果为:${result}`);
});console.log("处理程序添加完成");// 同步代码
console.log("同步代码执行");

输出

Promise开始执行
resolve() 返回
Promise创建完成
处理程序添加完成
同步代码执行
处理程序执行,结果为:成功

哪怕你把期约状态变化的代码的封装放在添加处理程序之后,结果也是一样:

let synchronousResolve;
// 创建一个Promise
const myPromise = new Promise((resolve, reject) => {synchronousResolve = function () {console.log("Promise开始执行");// 模拟异步操作setTimeout(() => {resolve("成功"); // 期约进入落定状态}, 2000);console.log("resolve() 返回");}
});console.log("Promise创建完成");// 添加处理程序
myPromise.then((result) => {console.log(`处理程序执行,结果为:${result}`);
});synchronousResolve();console.log("处理程序添加完成");// 同步代码
console.log("同步代码执行");

邻近处理程序的执行顺序

当给一个期约(Promise)添加多个处理程序(例如,.then().catch().finally()),这些处理程序会按照它们被添加的顺序依次执行。

const myPromise = new Promise((resolve, reject) => {setTimeout(() => {resolve("成功");}, 2000);
});myPromise.then((result) => {console.log(`第一个处理程序执行,结果为:${result}`);}).then(() => {console.log("第二个处理程序执行");}).catch((error) => {console.error(`捕获到错误:${error}`);}).finally(() => {console.log("无论如何都会执行的finally处理程序");});

输出

第一个处理程序执行,结果为:成功
第二个处理程序执行
无论如何都会执行的finally处理程序

传递解决值和拒绝理由

当一个Promise对象进入"已成功"或"已失败"的落定状态后,它会提供相应的解决值(如果成功)或拒绝理由(如果失败)给与之相关联的处理程序。这些解决值和拒绝理由会作为函数参数传递给处理程序,从而允许进一步操作这些值。

  1. 当一个Promise成功(通过调用resolve())时,解决值会传递给.then()处理程序,允许您进一步操作这个值。
  2. 当一个Promise失败(通过调用reject())时,拒绝理由会传递给.catch()处理程序,允许您处理错误情况。
  3. 在执行Promise的执行器函数中,解决值和拒绝理由是作为resolve()reject()函数的第一个参数传递的。
  4. Promise.resolve()Promise.reject()方法在被调用时也可以接收解决值和拒绝理由,并返回一个已经处于相应状态的Promise对象。这些值将会传递给与之关联的处理程序。
function fetchDataFromServer(url) {return new Promise((resolve, reject) => {setTimeout(() => {const success = Math.random() < 0.7;if (success) {const data = { id: 1, name: "John Doe" };resolve(data); // 请求成功,传递数据} else {reject("网络请求失败"); // 请求失败,传递错误信息}}, 1000);});
}fetchDataFromServer("https://example.com/api/data").then((response) => {console.log("成功获取数据:", response);// 在这里可以对获取到的数据进行操作return response.name; // 返回一个新值}).catch((error) => {console.error("网络请求错误:", error);// 在这里可以处理网络请求失败的情况throw new Error("处理失败"); //不会阻止代码执行}).then((value) => {console.log("在第二个.then()中获取的值:", value);}).catch((error) => {console.error("在第二个.catch()中获取的错误:", error.message);});console.log("网络请求已发出");

在这里插入图片描述

在这里插入图片描述

期约连锁形成 Promise 链

前文提到过 then 的链式调用,也被称为期约连锁thencatchfinally 都返回一个新期约对象,链式调用它们就会形成一条期约链

function delay(ms, str) {return new Promise((resolve) => {console.log(str);setTimeout(resolve, ms);});
}
delay(1000, "promise 1").then(() => delay(1000, "promise 2")).then(() => delay(1000, "promise 3")).then(() => delay(1000, "promise 4"));

在这里插入图片描述

期约图的概念

一个期约可能对应多个处理程序。每个处理程序也可能返回一个新期约。

这就可能形成期约图。

期约图中,一个期约是一个节点,期约的处理程序则是对应期约的不同边。

let A = new Promise((resolve, reject) => {console.log("A");resolve();
});
let B = A.then(() => console.log('B'));
let C = A.then(() => console.log('C'));
B.then(() => console.log('D'));
B.then(() => console.log('E'));
C.then(() => console.log('F'));
C.then(() => console.log('G'));

如上代码形成如下有向非循环的期约图:

在这里插入图片描述

期约合成的静态方法

期约合成是指将多个期约组合成一个期约。

Promise.all

Promise.all 方法创建的期约会在一组期约全部解决后再解决,即并行执行多个异步操作,并在所有操作都成功完成时才返回结果,如果其中任何一个失败,Promise.all 方法立即返回失败的期约。

Promise.all(iterable);

Promise.all 接受一个可迭代对象作为参数。可迭代对象的元素会通过 Promise.resolve 转换为期约

function createPromise() {return new Promise((resolve, reject) => {setTimeout(() => {const randomnNumber = Math.random();(randomnNumber < 0.5 && resolve(`成功: ${randomnNumber}`))|| reject(`失败: ${randomnNumber}`);},1000);});
}
let p = Promise.all([createPromise(),createPromise()
]);
setTimeout(console.log, 0, p);
p.then(() => setTimeout(console.log, 0, 'all() resolved'));

Promise.race

Promise.all 一样,Promise.race 接受一个可迭代对象作为参数。可迭代对象的元素会通过 Promise.resolve 转换为期约

不同的是,race 表示“竞争;角逐”的意思,Promise.race 的可迭代对象的元素转换后的期约谁先 settled 谁就返回谁。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/86041.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

elsarticle class not displaying email addresses

See https://tex.stackexchange.com/questions/84573/elsarticle-class-not-displaying-email-addresses

iPhone辐射超标,发布三年突然禁售了

昨晚 iPhone 15 预售大家抢到了吗&#xff1f; 虽然13日发布会后大家的反应十分冷静&#xff0c;但身体还是很诚实&#xff0c;官网都排到6-7周以后了... 在大伙都争着第一波尝鲜的时候&#xff0c;有一个地方正准备禁售 iPhone 。 不用想肯定是欧盟某个国家啦&#xff0c;这…

点分治维护dp+连通块上新型dp思路+乘积方面进行根号dp:0922T4

首先连通块&#xff0c;所以点分治肯定是 Trick1 钦定选根的连通块dp 对于钦定选根的连通块dp&#xff0c;有一种常见思路 先对原树求其dfn序&#xff0c;按dfn序倒序求解 具体的&#xff0c;对于当前点 i i i&#xff08;注意这里都是指dfn序&#xff09;&#xff0c;我们…

进程间通信(IPC)的方法:UNIX域套接字

UNIX域套接字(UNIX domain socket)为我们提供了一种在进程之间建立通信通道的便捷方法&#xff0c;具有许多有用的内置功能。它支持面向流(TCP)和面向数据报(UDP)协议作为TCP/IP互联网套接字。我们还可以在阻塞和非阻塞模式之间进行选择。 首先需要创建套接字并在套接字函…

Qt事件处理

1. 事件 众所周知Qt是一个基于C的框架&#xff0c;主要用来开发带窗口的应用程序&#xff08;不带窗口的也行&#xff0c;但不是主流&#xff09;。我们使用的基于窗口的应用程序都是基于事件&#xff0c;其目的主要是用来实现回调&#xff08;因为只有这样程序的效率才是最高…

基于SpringBoot的旅游系统

基于SpringBootVue的旅游系统、前后端分离 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 【主要功能】 角色&#xff1a;管理员、用户 用户&#xff1a;浏览旅游…

【论文阅读】内存数据库并发控制算法的实验研究

内存数据库并发控制算法的实验研究 原文链接jos.org.cn/jos/article/pdf/6454 摘要 并发控制算法的基本思想归纳为"先定序后检验”&#xff0c;基于该思想对现有各类并发控制算法进行 了重新描述和分类总结&#xff0c;于在开源内存型分布式事务测试床 3TS 上的实际对比实…

详解TCP/IP协议第四篇:数据在网络中传输方式的分类概述

文章目录 前言 一&#xff1a;面向有连接型与面向无连接型 1&#xff1a;大致概念 2&#xff1a;面向有连接型 3&#xff1a;面向无连接型 二&#xff1a;电路交换与分组交换 1&#xff1a;分组交换概念 2&#xff1a;分组交交换过程 三&#xff1a;根据接收端数量分…

免费玩云上大数据--海汼部落实验室

玩大数据遇到的问题 大家好&#xff0c;这次分享一个免费的大数据部署工具&#xff0c;并非是给人家打广告&#xff0c;试过了真的爽。 学习大数据的人都知道&#xff0c;如果用VMware模拟Linux搭建大数据集群的话我们需要很高的内存和硬盘内存&#xff0c;随随便便跑一下mapre…

【云原生】Kubernetes学习笔记

部署 在部署前强调几点 不要使用IPv6, 很多组件都不支持IPv6不要使用最新版本, 最新版本非常不稳定, 甚至可能存在无法运行的bug不要版本更新, 安装后就将版本固定下来, 新的版本可能会引入新功能, 或移除旧功能, 导致Kubernetes无法运行 Kubeadm介绍 K8s是由多个模块构成的…

2023华为杯数模C题——大规模创新类竞赛评审方案研究

B题——大规模创新类竞赛评审方案研究 思路&#xff1a;采用数据分析等手段改进评分算法性能 完成情况(1-2问已经完成) 代码下载 问题一 在每个评审阶段&#xff0c;作品通常都是随机分发的&#xff0c;每份作品需要多位评委独立评审。为了增加不同评审专家所给成绩之间的可比…

解决因为修改SELINUX配置文件出错导致Faild to load SELinux poilcy无法进入CentOS7系统的问题

一、问题 最近学习Kubernetes&#xff0c;需要设置永久关闭SELINUX,结果修改错了一个SELINUX配置参数&#xff0c;关机重新启动后导致无法进入CentOS7系统&#xff0c;卡在启动进度条界面。 二、解决 多次重启后&#xff0c;在启动日志中发现 Faild to load SELinux poilcy…

简单的自托管书签服务NeonLink

什么是 NeonLink &#xff1f; NeonLink 是一个简单且开源的自托管书签服务。它是轻量级的&#xff0c;使用最少的依赖项&#xff0c;并且易于通过 Docker 安装。由于系统要求较低&#xff0c;该应用程序非常适合部署在 RaspberryPI 上。 安装 在群晖上以 Docker 方式安装。 …

【ES6】

ES6 1 ES6简介1.1 什么是ES61.2 为什么使用ES6 2 ES6的新增语法2.1 let2.2 const2.3 let、const、var的区别2.4 解构赋值2.4.1 数组解构2.4.2 对象解构 2.5 箭头函数2.6 剩余参数 3 ES6的内置对象扩展3.1 Array的扩展方法3.1.1 扩展运算符(展开语法)3.1.2 构造函数方法&#xf…

Docker 部署 Bitwarden RS 服务

Bitwarden RS 服务是官方 Bitwarden server API 的 Rust 重构版。因为 Bitwarden RS 必须要通过 https 才能访问, 所以在开始下面的步骤之前, 建议先参考 《Ubuntu Nginx 配置 SSL 证书》 配置好域名和 https 访问。 部署 Bitwarden RS 拉取最新版本的 docker.io/vaultwarden…

第一百五十二回 自定义组件综合实例:游戏摇杆三

文章目录 内容回顾优化性能示例代码我们在上一章回中介绍了 如何实现游戏摇杆相关的内容,本章回中将继续介绍这方面的知识.闲话休提,让我们一起Talk Flutter吧。 内容回顾 我们在前面章回中介绍了游戏摇杆的概念以及实现方法,并且通过示例代码演示了实现游戏摇杆的整个过程…

Windows安装cuda和cudnn教程最新版(2023年9月)

文章目录 cudacudnn cuda 查看电脑的cuda最高驱动版本&#xff08;适用于N卡电脑-Nvidia&#xff09; winR打开命令行&#xff0c;输入nvidia-smi 右上角cuda -version就是目前支持的最高cuda版本&#xff0c;目前是12.2 nvidia官网下载cuda 下载地址&#xff1a;https://d…

基于eBPF的安卓逆向辅助工具——stackplz

前言 stackplz是一款基于eBPF技术实现的追踪工具&#xff0c;目的是辅助安卓native逆向&#xff0c;仅支持64位进程&#xff0c;主要功能如下&#xff1a; hardware breakpoint 基于pref_event实现的硬件断点功能&#xff0c;在断点处可读取寄存器信息&#xff0c;不会被用户…

C/C++自定义读取ini、cfg配置文件

常见cfg、ini文件如下: [config1] setting192.168.1.1 [config2] setting192.168.1.2 [config3] setting192.168.1.3 示例代码使用 // opt_ini.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 //#include <iostream> #include "cfg.h"…

短信登录功能如何实现?

简介&#xff1a; 在日常生活中我们登录/注册某些网站/APP是通常可以选择 密码登录和手机号登录。 为什么手机号发送后会有验证码返回呢&#xff1f; 网站如何识别我的验证码是否正确&#xff1f; 如果我的个人网站也想要实现短信登录功能&#xff0c;具体该如何实现&#xff1…