跟简单却又晦涩的 Promise 说 Hello

一、前言

  1. 我不知大家是如何接触到的 Promise, 我想可能是 Axios? 可能是对异步方法的封装?可能是对 Vue 中方法的封装?对 React 中方法的封装?
  2. 我记得我当时还是只会一些 ES5 语法小白的时候,为了看懂别人写的代码,大量恶补 ES6 的新语法,与此同时我接触到了可爱又可恨的 Promise,为了学会并掌握 Promise,我看了大量的文章,很大一部分的文章都是使用 setTimeout() 进行模拟异步流程,有的时候会陷入到:哦,我懂了 → 这是为啥呢? → 查看文章学习 → 哦,我又懂了 → What?的死循环中。
  3. OMG!对于当时的我来说就是噩梦,因为当时的我每打开一篇文章,啊!又是 setTimeout() 模拟的 Promise(),因为在真实场景中都是发送的Ajax请求,使用setTimeout() 模拟的这种根本不能真实体会到发送ajax的这种感觉。
  4. 这篇我将携手我们的小助理 ChatGPT从 Prmose 的 基本概念 → Prmose 的 基本原理 → Promise 的使用 Tips → 使用 Promise 封装原生 XMLHttpRequest → 使用 Promise 封装微信小程序中的 request 接口 → 使用 Promise 封装原生工具 ,使用 Express 快速搭建后端接口数据,一站式帮助大家理解 Promise 。

提升通道:这里我强烈推荐尚硅谷的 张晓飞 老师的 《手写Pomise课程》,我并没有做什么推广,只是手写完成之后,感觉茅塞顿开,你可能会在之后忘记具体的实现,但你会对 Promise 的基本原理会记忆犹新 !! 话不多说直接开整。

二、Promise 的基本概念

Promise的概念我们可以从多方面搜索一下

维基百科
请添加图片描述

ChatGPT
请添加图片描述

MDN

在这里插入图片描述

从多方搜索很显然显然发现 pending、fulfilled、rejected
三个状态总是可以映入我们的眼帘,之后我们会从这三个关键状态说起,从维基百科可以发现,Promise 是在 并发编程语言中都是存在的,显而易见如果你学懂了 JavaScript 中的 Promise , 那对于 Java 中的 Future,Netty 中的Promise 也就不在话下了,概念基本都是相同的,可能在实现上有所差别罢了!

OK, 我们了解了一下 Promise 中的基本概念后,即将进入我们的正题,如果感觉还不够详细的话,大家可以自行进行搜索了解,本人推荐使用 NewBings 进行搜索,他可以给你精确的答案。

三、Promise 的基本原理

1、 前置知识

在这里我不会直接去讲解 Promise 的基本原理,因为 Promise的原理在网络上随便一搜索就可以得到成千上万的答案,直接带大家学习可能大家也不会深入理解。

能看到这里的家人们一定对 DOM 的事件回调,以及 setTimeout() 的事件回调,还有大家常用的 AJAX发送请求的事件回调,可是大家知道他的调用流程吗?

在这里,我给大家提供了一张执行图,本人艺术细胞也不是很高,画图水平也就那样,将就着看看,目的是让大家能够理解。

在这里插入图片描述
刚看到这个图的时候,一定会感觉到十分的混乱,接下来由我一一讲解。

首先是图的橙色部分:这部分是JavaScript 的执行引擎部分,学过 三件套的都应该知道,Web浏览器的三大核心 DOM,BOM, JavaScript 执行引擎, 执行引擎部分主要包括堆、函数的调用栈等信息,堆主要存储一些对象信息,栈则是函数执行时的容器。

红色部分: Web页面的API的管理模块主要负责异步方法的处理。

蓝色部分:是宏队列的执行流程,如果Main线程中函数有宏任务,则会将宏任务放到宏队列中。

绿色部分:是微队列的执行流程,如果Main线程中函数有微任务,则会将微任务放到微队列中。

还有一个十分重要的点就是事件循环机制,接下来将会为大家讲解。

在这里我们根据一个简单的例子来理解一下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <button id="click-button">单击按钮</button>

    <script>

        // 获取 button DOM 对象
        let button = document.getElementById('click-button')

        // 定义函数 function-one
        const functionOne = () => {
            button.addEventListener('click', () => {
                console.log('按钮被点击了')
            })
            console.log('functionOne 执行完成')
        }

        // 定义函数 function-two
        const functionTwo = () => {
            setTimeout(() => {
                console.log('setTimeout 执行完成')
            }, 1000)

            console.log('functionTwo 执行完成')
        }

        // 执行functionOne()
        functionOne()

        // 执行functionTwo()
        functionTwo()

        console.log('主函数执行完成')
    </script>
</body>

</html>

我给大家提供了一段简单的代码,大家试着运行理解一下

大家可以把 script 标签中的内容简单理解为 Main 线程,其实这里的理解不是很准确的,Main线程的功能不止这些。首先

→获取 button 对象
→ 定义 functionOne()
→ 定义 functionTwo()
→ 执行functionOne() (将 click 监听任务放到宏队列)
→ 执行 functionTwo()(将 setTimeOut任务 放到宏队列)
→ 主函数执行完成
→ setTimeOut 函数时间到触发回调执行
→ 如果你点击了按钮执行按钮回调执行事件

这里有一个很重要的点:宏队列和微队列的执行一定在 Main 线程执行完成后才会去执行。可以有些人会有疑问如果我把 setTimeout 的事件设置为0 会比 console.log(‘主函数执行完成’) 先执行吗? No, 不会的,它一定会被放到宏队列中去,等 Main 函数执行完成才会去执行。

Ok,基础的知识讲解完成,大家对宏队列也有了一定的理解,那么开始我们的重点: Promise

2、Promise 基础知识

当我们不知道如何下手的时候,我们可以 寻求 AI 的帮助,让他帮忙写一个案例:

在这里插入图片描述
大家还记得 在第二部分 提到的三个状态和微队列吗?pending、fulfilled、rejected 和前置条件中的图,那我们就从这个案例说起,当我们 new Promise() 时,该 Promise 处于 pending 状态也就是代办状态, JavaScript 执行引擎会将该 Promise 放入到APIs管理模块中去进行管理,当状态发生改变时,则会将 Promise 的回调函数放入到微队列中去,那我们如何改变状态?以及如何给Promise 指定回调呢?在 new Promise对象时,不要忽略了两个参数 resolve 和 reject,这两个参数其实是两个函数也就是通过这两个函数来改变 Promise 的状态;那我们如何指定回调函数呢?大家可以通过 ChatGPT 给我们生成的案例的第二条, 通过 then 和 catch 指定正确和错误的回调。

下面我将通过一个例子来带大家进行理解,大家可以创建一个 HTML 文件尝试多次运行:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title> the start of promise </title>
</head>

<body>
    <script>
        // 创建新的 Promise
        let promise = new Promise((resolve, reject) => {
            let number = Math.random()

            // 根据随机数的大小修改 Promise 的状态
            if (number < 0.5) {
                resolve({ code: 200, message: '成功', data: number })
            } else {
                reject({ code: 500, message: '失败', data: number })
            }
        })

        // 指定 成功的回调 和 失败的回调
        promise.then((successResult) => {
            console.log('success result : ', successResult)
        }).catch((failResult) => {
            console.log('fail result : ', failResult)
        })
    </script>
</body>
</html>

通过上面这个案例可以看出,在 Promise 中使用 resolve 函数 修改 Promise 的状态为 fulfilled 状态也就是成功状态,并且通过resolve 将成功的结果发送给了 then 回调函数,同理可以看出 Promise 也可以通过 reject 函数将失败的结果发送给了 catch 回调函数。

注意点在 Promise 中 resolve 和 reject 中二者只能执行其一,只要二者执行其一,就会触发微队列,进行事件回调。

3、细节分析 Promise 在微队列的执行

如果没有宏队列的任务,只有微队列的任务,那么微队列和宏队列的执行其实是大同小异的。只有我们去试着分析既有宏队列又有微队列的任务时,才能真正的理解,接下来我将会写一个比较复杂的调用流程(微任务使用 Promise,宏任务使用 setTimeout),试着和大家一起分析一下,话不多说直接上代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>细节分析 Promise</title>
</head>

<body>
    <script>
        console.log("主函数开始执行")
        // 宏任务①
        setTimeout(() => {
            console.log("宏任务①, 回调执行")
        }, 0)

        // 微任务①
        new Promise((resolve, reject) => {
            resolve("成功")
        }).then(() => {
            console.log('微任务①, 回调执行')
        })

        // 宏任务②
        setTimeout(() => {
            console.log("宏任务②, 回调执行")
            // 创建微任务②
            new Promise((resolve, reject) => {
                resolve("成功")
            }).then(() => {
                console.log('微任务②, 回调执行')
            })
        }, 0)

        // 微任务③
        new Promise((resolve, reject) => {
            resolve("成功")
        }).then(() => {
            console.log('微任务③, 回调执行')
        })

        console.log("主函数结束执行")
    </script>
</body>

</html>

大家想到了执行的过程了吗?
在这里插入图片描述
接下来我将带大家一起分析执行流程:用这个 【】 表示宏队列,用 [] 表示微队列

→ 毫无疑问 首先打印 主函数开始执行
→ 将宏任务①放到宏队列中 【①】
→ 将微任务①放到微队列中 [①]
→ 将宏任务②放到宏队列中 【①,②】,此时还并没有创建微任务②
→将微任务③放到微队列中 [①,③]
→打印 主函数结束执行 主函数执行结束
由于微任务和宏任务都有任务,则先执行微任务, 打印 微任务①, 回调执行
微任务③, 回调执行 微任务队列变为 [ ]
执行宏任务 打印 宏任务①, 回调执行;打印 宏任务② 回调执行;大家还记得宏任务一种有一个微任务②吗?这是会将微任务②加入到微任务队列中 [②]
打印微任务②, 回调执行

其实大家有没有注意到一点,我把 setTimeout 的时间设置为了 0,大家有没有想过我在发送网络请求的时候会有一定的延时。上面的案例只是演示了最理想的情况下,如果我们把宏任务 ① 的倒计时时间设置为 10000 呢? 他一定是在最后执行的,大家没有记得在我画的那张图中,有一个 APIs 管理模块,其实倒计时的过程是在那里面执行的,只有执行完毕后才会将回调放到宏任务队列中的。Promise 也是相同的,我在发送网络请求的时候 Promise 状态为 pending 的其实是在 APIs 管理中执行的,之后调用了 reject 或者 resolve 函数,才会将回调放到微队列中。

4、EventLoop 事件循环

大家有没有想过我们为什么在注册了 DOM 点击事件之后,无论我们何时点击 DOM,APIs 管理模块将其回调 Push 到宏任务队列中,都可以触发该事件,其实这就是事件循环机制的功劳,他会一个周期一个周期轮询监听,如果在微队列和宏队列中有可执行的任务的任务回调,它就会将该任务的回调读取到 Main 线程中进行执行。

5、微队列和宏队列 微任务与宏任务 小总结

  1. 宏任务和微任务都是异步任务,只是JavaScript 在宏队列和微队列中读取回调函数到主线程的任务顺序不同,在读取每个宏任务之前会读取所有的微任务。
  2. JavaScript 中用来存储待执行回调函数的队列包含两个不同的队列:宏队列和微队列
  3. 宏队列:用来保存待执行的宏任务(回调)比如:定时器回调 / ajax回调
  4. 微队列:用来保存待执行的微任务(回调)比如:Promise 回调
  5. 重点:每次取出宏任务到主线程之前,则会将微任务一一取出到主线程执行。

四、Promise 的使用 Tips

1、.then 和 .catch 的返回结果为 Promise

首先我们把一开始 Promise 的案例拿过来, 我们用 returnPromise 来接收.then 或者 .catch的结果

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Promise .then .catch</title>
</head>
<body>
    <script>

        // 首先我们, 创建新的 Promise
        let promise = new Promise((resolve, reject) => {
            let number = Math.random()
            
            // 根据随机数的大小修改 Promise 的状态
            if (number < 0.5) {
                resolve({ code: 200, message: '成功', data: number })
            } else {
                reject({ code: 500, message: '失败', data: number })
            }
        })

        // 返回的 Promise
        let returnPromise = promise.then((successResult) => {
            console.log('success result : ', successResult)
        }).catch((failResult) => {
            console.log('fail result : ', failResult)
        })

				console.log(`returnPromise`, returnPromise)
    </script>
</body>
</html>

通过打印 returnPromise 发现确实无论是.catch 和 .then 的返回值都是 Promise,大家想一个问题,如果返回的是一个 Promise 我们就可以一直 .then 下去。

我们可以将上面的代码块,进行改进。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Promise .then .catch</title>
</head>
<body>
    <script>

        // 首先我们, 创建新的 Promise
        new Promise((resolve, reject) => {
            let number = Math.random()
            
            // 根据随机数的大小修改 Promise 的状态
            if (number < 0.5) {
                resolve({ code: 200, message: '成功', data: number })
            } else {
                reject({ code: 500, message: '失败', data: number })
            }
        }).then((successResult) => {
            console.log('then1 ----> success result : ', successResult)
        }).then((result) => {
            console.log('then2 ----> result : ', result)
        }).catch((failResult) => {
						// 在这里可以直接返回一个空的Promise,来阻断链的调用
						// return new Promise(() => {})
            console.log('fail result : ', failResult)
        }).then((result) => {
            console.log('final then ---> result : ', result)
        }).catch((result) => {
            console.log('final catch ---> result : ', result)
        })

    </script>
</body>
</html>

如果成功了 会执行
在这里插入图片描述
如果失败了会执行
在这里插入图片描述
这里大家可以观察到:无论是执行失败还是都会执行 final then —→ result : undefined

这是为什么呢?大家有没有记得我刚才说的,.catch 和 .then 一定会返回一个 Promise ,如果我们没有返回值,则会默认返回一个成功的 Promise ,并且其携带的值为 undefined。

同理也可以得到,final catch → result 永远都不会执行,因为 如果出现了异常,则会被第一个 catch 所捕获,而第一个 catch 一定返回一个成功的 Promise.

可能会有人疑问为什么我中间的 catch 已经捕获到异常了,为什么还要成功的走下面的流程呢?

其实这就是 Promise 的一个规则,如果你想 catch 后不行继续执行下面的流程,则直接返回一个 pending 状态的 Promise ,来阻断 Promise 链的调用,这也就是所说的终止 Promise, 这个点还是十分的关键的,请大家牢记。为什么可以用来阻断 Promise 呢? 大家想想,如果 Promise 没有改变状态他的回调是永远都不会放到异步队列中去的,所以永远都不会执行。

2、Promise 的原型方法

在 JS 中一开始是没有像 Java 那种继承体系,他只有通过这种 原型链的方式 模拟继承体系,也不能说模拟吧,每种语言有每种语言的特点。JavaScript 可以在对象的原型上定义一些方法,可以供 new 出来的对象公共使用。

比如之前我们所用的 catch()、then() 都是原型链上的方法。

还有两个十分常用的 Promise.resolve() 和 Promise.reject() 大家看这两个方式是不是与 new Promise()的时,与其中的回调函数相同。是的其实他们的作用是基本相同的,但是这两个方法可以在不 new Promise 的情况下直接返回 一个成功或者失败状态的 Promise

还记不记得上一个模块我们所说的 .then() 和 .catch() 的回调返回一个 Promise 实例对象, 如果没有返回值,则返回一个成功的且内容为 undefined 的回调,其实这里可以直接使用 Promise.resolve() 或者 Promise.reject() 返回一个成功或者失败的回调。例如

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Promise.reject()和Promise.resolve()</title>
</head>
<body>
    <script>
        new Promise((resolve, reject) => {
            let number = Math.random()
            
            // 根据随机数的大小修改 Promise 的状态
            if (number < 0.5) {
                resolve({ code: 200, message: '成功', data: number })
            } else {
                reject({ code: 500, message: '失败', data: number })
            }
        }).then((result) => {
            return Promise.reject('fail')
            // 或者 return Promise.resolve('success')
        }).catch((err) => {
            console.log('err', err)
        })
    </script>
    
</body>
</html>

还有一种情况:如果我们直接返回一个具体的值相当于返回成功的 Promise 并且携带成功的值相当于 Promise.resolve(value),如果 throw 一个异常相当于返回一个失败的 Promise 相当于使用 Promise.reject(value)

3、解决回调地狱?

解决回调地狱是怎么一回事呢?比如我现在要发送两个请求,如果第二个请求需要第一个请求的参数,那么第二个请求就必须要在第一个请求结束后,第二个请求拿到第一个请求的参数,才可以发送第二个请求。

我刚开始写的时候是这样的:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Promise .then .catch</title>
</head>
<body>
    <script>

            // 模拟用户发送
            const getStudentMessage = () => {
                // 模拟发送网络请求,模拟获取学生信息
                return Promise.resolve({id:`001`, name:'Dylan', age:20})
            }

            const getSchoolByStudent = (studentId) => {
                // 模拟发送网络请求,根据学生id获取 学生的学校信息
                return Promise.resolve({id:'001', schoolName:'xxxxxx'})
            }
            
        // 需求查询用户信息的学校信息
        const queryStudentSchoolMessage = () => {

            getStudentMessage().then((student) => {
                console.log('学生信息:', student);
                getSchoolByStudent(student.id).then((school) => {
                    console.log("学校信息: ",school)
                })
            })
        }
        
        // 调用执行
        queryStudentSchoolMessage()

    </script>
</body>
</html>

其实上面的写法并没有解决回调地狱的问题,他只不过是 Promise 的回调地狱罢了, 我一开始写的时候,也很纳闷,这不还是回调地狱吗?

那我们该如何解决回调地狱问题呢? 还记得我们说的 .then() 返回 Promise 吗,对了我们就是用这个。

// 需求查询用户信息的学校信息
const queryStudentSchoolMessage = () => {

    getStudentMessage().then((student) => {
        console.log('学生信息:', student);
        return getSchoolByStudent(student.id)
    }).then((school) => {
        console.log('学校信息:',school);
    })
}

4、.catch的传透性

穿透性 比较好理解,就是我们可以写很多个 .then() 方法,在最终统一处理异常 也就是 .catch() ,这样的话,无论在那个 .then 中出现了异常,都会透传到 .catch()

大家还记得 在 .then() 和 .catch() 中,只要抛出异常,就会返回 失败的 Promise,就会透传到最终的 .catch(),这一点大家不要忘记。

5、终止 Promise

终止 Promise 我们上面已经说过了,就是可以直接返回一个 pending 状态的 Promise 即 new Promise(() ⇒{})

6、async 和 await

首先我们从字面意思上理解 async 是同步的意思,而 await 是 等待的意思,有人会问了,我们现在不是使用异步吗,为什么还需要同步呢? 其实不然,有的时候就像我们刚才举的那个例子,两个异步方法就是需要同步执行,二异步是无法控制执行顺序的,只有通过回调地狱或者 Promise.then() 方式控制其顺序,与其回调地狱,与其连续.then JavaScript 直接推出的新的语法糖 async 和 await,供大家使用。那我们该如何使用呢,我们还是以上面的例子为例进行改写:

const queryStudentSchoolMessage = async () => {
    const student = await getStudentMessage()
    console.log('学生信息:', student);
    const school = await getSchoolByStudent(student.id)
    console.log('学校信息:',school);
}

有没有柳暗花明又一村的感觉, 那这是为什么呢?

其实在 Promise 前面使用 await 相当于进行了 .then 其拿到的结果就是 .then 回调中的参数,也就是成功的值,如果使用 await 则会将该方法转化为同步的方法,如果没有拿到返回的值,则会等待,无论是等待 10 秒 还是 20 秒,这样一来就起到了同步的作用。

还有十分重要的一点,await 必须在 async 声明的函数中使用,故名思议这个函数是同步的,这里的函数通过不是说有了 async 就是所有的内容都同步,而是必须结合使用 await 来控制同步

接下来我将带大家写一些基本的案例,来强化 Promise 的使用,我们可以借助 ChatGPT 帮助我们轻松的实现,如果你掌握了 Promise ,你的编码水平一定会更胜一筹。

五、使用 Promise 封装原生 XMLHttpRequest

1. 封装 XMLHttpRequest

大家应该使用过 Axios ,其实就是使用 Promise 对 原生 XMLHttpRequest 的封装,如果大家能这个案例学会,其实对 Axios 的理解也就大差不差了。

下面我们根据这段代码来理解一下:

在这里插入图片描述

OK, 上来就可以看到,直接返回 Promise, 还记不记得我上文写的 获取学生的信息,其实我相当于直接返回了成功的 Promise 并且携带学生的信息。这里是相同的。

大家注意一下 onreadystatechange 回调函数,其实就是如果成功(200~300),则将 Promise 设置为成功 ,如果失败,将 Promise 设置为失败状态

onerror 方法 如果请求过程中出现异常,则直接返回失败结果。

2、搭建 接口服务器

你可以根据你喜欢的语言搭建一些请求的接口比如 Java、Node.js、 go、c 等等,这里我们借助 ChatCPT 可以快速搭建后端路由接口。
在这里插入图片描述

我们借助 AI 工具有的时候还是很容易搭建出我们想要的结果的,只要我们进行微调即可,既然他能返回 Hello,World,那么我们就可以返回对象,数组。

六、使用 Promise 封装 微信小程序 中的 Request 接口

如果做过微信小程序的开发你一定会知道,wx.request() 是 真 xx 难用,其实我们完全可以使用 Promise 进行封装,来统一接口的处理。

const request(url, method, data) => {
  return new Promise((resolve, reject) => {
    wx.request({
      url: url,
      method: method,
      data: data,
			header:header
      success: (res) => {
        resolve(res.data);
      },
      fail: (error) => {
        reject(error);
      }
    })
  })
}

在真实的业务需求中你可以进行封装的完善。比如统一异常处理等等。

七、使用 Promise 封装原生工具

我们可以直接用来封装 wx.modal() 简化我们的操作,我们聚焦于业务中,而不需要,考虑这些方法的使用上。

export const modal = (content = '是否确认操作', title = '提示') => {
  return new Promise((resolve) => {
    wx.showModal({
      title,
      content,
      success: ({confirm, cancel}) => {
        if (confirm) {
          resolve(true)
        } else if (cancel) {
          resolve(false)
        }
      }
    })
  })
}

我们也可以封装一些 Axios 请求,使我们的业务逻辑更加的清晰。

login({ commit }, userInfo) {
    const { username, password } = userInfo
    return new Promise((resolve, reject) => {
      login({username, password }).then(response => {
        let data = response
        if(data.success){
          commit('SET_TOKEN', '库里的球衣')
          resolve()
        }else{
          reject(data)
        }
      }).catch(error => {
        reject(error)
      })
    })
  },

八、总结

  1. 如果我们真正掌握了 Promise , Promise 在前端领域是无处不在的,毕竟要发送网络请求嘛,有很多方法将其使用 Promise 封装后,在调用层面会显得格外的清晰明了。
  2. 通过全文可以看出,我有更多的点都是借助 AI 工具去生成的,这样可以帮助我们快速大家出我们想要的环境,使我们 聚焦于 某一领域。
  3. 如果有问题,可以在评论区及时提出,我们一同改进,我的理解也只是冰山一角,欢迎大家一同讨论。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

库里的球衣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值