面试必考 - 手写 Promise, 由浅入深(附源码)
2020/7/6 11:27:02
本文主要是介绍面试必考 - 手写 Promise, 由浅入深(附源码),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
前言
Hello 大家好!我是壹甲壹!
相信大家无论在前端还是后端开发工作中,都接触并使用过 Promise ,本文将带领大家「step-by-step」实现一个符合 Promises/A+ 规范的 Promise,同时探索 Promise 中的一些方法以及第三方扩展如何实现的。
通过阅读本篇文章你可以学习到:
手写实现符合规范的 Promise 使用 promises-aplus-tests
进行规范测试掌握 Promise.all
,Promise.race
,Promise.resolve
,Promise.reject
等实现原理掌握 Node 中对 Promise 的一些扩展
在正式进入正题之前,为了更好地理解和掌握 Promise ,我们先来介绍一些与 Promise 相关的基础知识。
一、什么是异步
1.1 JS 中为什么存在异步
大家应该都知道,JS 属于单线程语言,所谓单线程,就是一次只能干一件事,其它事情只能在后面乖乖排队等待。
在浏览器中,页面加载过程中存在大量请求,当一个网络请求迟迟没有响应,页面将傻傻等着,不能处理其它事情。
因此,JS 中设计了异步,即发送完网络请求后就可以继续处理其它操作,而网络请求返回的数据,可通过回调函数来接收处理,这样就保证了页面的正常运行。
1.2 异步解决方案
先看下面一段 Node 代码
var fs = require('fs') fs.readFile('data.json', (err, data) => { console.log(data.toString()) }) 复制代码
fs.readFile
方法的第二个参数是个函数,函数并不会立即执行,而是等到读取的文件结果出来才执行,这是函数就是回调函数,即 callback
1.3 回调地狱
处理多个异步请求,并且一个一个嵌套时,就容易产生回调地狱。看下面一段 Node 代码
const fs = require('fs') fs.readFile('data1.json', (err, data1) => { fs.readFile('data2.json', (err, data2) => { fs.readFile('data3.json', (err, data3) => { fs.readFile('data4.json', (err, data4) => { console.log(data4.toString()) }) }) }) }) 复制代码
使用 Promise 改写
const fs = require('fs') const readFilePromise = (file) => { return new Promise((resolve, reject) => { fs.readFile(file, (err, data) => { if (err) { reject(err) } resolve(data) }) }) } readFilePromise('data1.json') .then(data1 => { return readFilePromise('data2.json') }).then(data2 => { return readFilePromise('data3.json') }).then(data3 => { return readFilePromise('data4.json') }).then(data4 => { console.log(data4.toString()) }).catch(err => { console.log(err) }) 复制代码
“「思考题」:Promise 真的取代 callback 了嘛?
Promise 只是对于异步操作代码的可读性的一种变化,没有改变 JS 中异步执行的本质,也无法取代 callback 在 JS 中的存在。同时,在 Promise 中,也存在着 callback 的使用,实例的 then() 的参数分别是执行成功、失败的函数,也就是 callback 回调函数。
二、Promise 的实现
本篇文章对应的项目地址: github.com/Yangjia23..…
2.1 基本实现
2.1.1 executor 执行器
首先,Promise 是个类,需要使用 new 来创建实例
new Promise((resolve, reject) => {})
传入的参数是个函数,被称为executor
执行器,默认会立即执行executor
执行时会传入两个参数resolve, reject
,分别是执行成功函数、执行失败函数resolve, reject
两个执行函数不属于 Promise 类上的静态属性,也不是实例上的方法,而是一个普通函数
class Promise { constructor (executor) { // 成功 const resolve = () => {} // 失败 const reject = () => {} // 立即执行 executor(resolve, reject) } } 复制代码
2.1.2 三种状态
关于 Promise 状态
promise 有三种状态:等待 (pending)、已成功 (fulfilled)、已失败(rejected),默认状态为
pending
promise 的状态只能从
pending
转换成fulfilled
或rejected
两种状态变化
了解promise状态更多内容,请查看Promises/A+规范: promise-states
以 readFilePromise 为例
读取文件成功时,会调用 resolve
函数,传入读取的内容,表示执行成功,此时的状态应是fulfilled
成功态读取文件失败,会调用 reject
函数,传入失败的原因,表示执行失败,此时的状态应是fulfilled
失败态读取的文件内容或失败的原因需要保存,分别使用 value
和reason
存储
const ENUM = { PENDING: 'pending', FULFILLED: 'fulfilled', REJECTED: 'rejected' } class Promise { constructor (executor) { this.status = ENUM.PENDING // 默认状态 this.value = undefined // 保存执行成功的值 this.reason = undefined // 保存执行失败的原因 // 成功 const resolve = (value) => { if (this.status === ENUM.PENDING) { this.status = ENUM.FULFILLED this.value = value } } // 失败 const reject = (reason) => { if (this.status === ENUM.PENDING) { this.status = ENUM.REJECTED this.reason = reason } } // 立即执行 executor(resolve, reject) } } 复制代码
2.1.3 异常捕获
由于 executor
执行器是由用户传入的,在执行过程中可能出现错误,此时需要使用 try...catch...
进行异常捕获,当发生错误后,直接调用 reject 抛出错误
class Promise { constructor (executor) { // .... // 异常捕获 try{ // 立即执行 executor(resolve, reject) } catch (e) { reject(e) } } } 复制代码
2.1.4 实现 then 方法
调用 new Promise()
返回的实例上有个 then
方法,then
方法需要用户提供两个参数,分别是执行成功后对应的成功回调 onFulfilled
和执行失败后对应的失败回调 onRejected
当状态变成 fulfilled,会调用 onFulfilled
方法,并传入成功的值 this.value当状态变成 rejected,会调用 onRejected
方法,并传入失败的原因 this.reason
class Promise { constructor(executor) { // ... } then(onFulfilled, onRejected) { if (this.status == ENUM.FULFILLED) { onFulfilled(this.value) } if (this.status == ENUM.REJECTED) { onRejected(this.reason) } } } 复制代码
当 executor
中执行的是异步操作时,执行 then
方法时状态还是 pending
“异步操作例如
setTimeout
属于宏任务,而promise.then
属于微任务, 微任务先于宏任务执行,所以then
方法执行时,promise
的状态还是pending
同时实例promise可以多次调用 then 方法,所以,需要将所有 then
方法中的回调函数搜集保存好,当异步操作完成后,再执行保存的回调函数(基于发布订阅模式)
const promise = new Promise((resolve, reject) => { setTimeout(() => {}, 2000) }) promise.then(data => {//...}, err => {}) promise.then(data => {//...}, err => {}) 复制代码
所以,接下来需要实现的是
创建两个队列 onResolvedCallbacks
和onRejectedCallbacks
,分别存放 then 方法中对应的成功回调和失败回调当异步操作成功时,调用 resolve
函数时,执行onResolvedCallbacks
队列中每个成功回调当异步操作失败时,调用 reject
函数时,执行onRejectedCallbacks
队列中每个失败回调
class Promise { constructor(executor) { this.status = ENUM.PENDING this.value = undefined this.reason = undefined this.onResolvedCallbacks = [] // 成功队列 this.onRejectedCallbacks = [] // 失败队列 // 成功回调 const resolve = (value) => { if (this.status === ENUM.PENDING) { this.status = ENUM.FULFILLED this.value = value this.onResolvedCallbacks.forEach(cb => cb()) // 相对于发布 } } // 失败回调 const reject = (reason) => { if (this.status === ENUM.PENDING) { this.status = ENUM.REJECTED this.reason = reason this.onRejectedCallbacks.forEach(cb => cb()) } } // 立即执行 executor(resolve, reject) } then(onFulfilled, onRejected) { // ... if (this.status === ENUM.PENDING) { // 相对于订阅 this.onResolvedCallbacks.push(() => { // todo... onFulfilled(this.value) }); this.onRejectedCallbacks.push(() => { // todo... onRejected(this.reason); }) } } } 复制代码
注意:在 then
方法中,并没有往队列中直接插入回调函数, 而是使用函数包装后再 push
,是为了方便后续扩展 ( eg:获取并处理 onFulfilled()
的返回值)
到现在为止,实现了基础版 Promise , 但看着和之前的 callback 只是写法上不同,并没有体现出 Promise 的优势,接下来,继续探索 Promise 中的高级特性
2.2 高级特性
2.2.1 实现 then 链式调用
对于实例上的 then(onFulfilled, onRejected)
方法,其参数为成功、失败两个回调函数。总结出以下几个使用场景
如果两个方法执行返回值是普通值,则会被传递到外层的下一个 then
中如果两个方法执行过程中抛出异常,则会在下一个 then
的失败回调中捕获异常当两个方法执行返回值是 promise, 那么会用该 promise 的状态作为结果 ( promise 的状态是“成功”,则会调用下一个 then
的成功回调;状态为“失败”则会调用下一个then
的失败回调)错误处理,当发生错误时( then
中抛错或返回一个失败的 promise ),该错误会被最近的一个失败回调捕获,当该失败回调执行后,可以继续调用then
方法
在 Promise 中,promise.then 链式调用的实现原理是通过返回一个新的 promise 来实现的
“「思考题」为什么返回新的 promise, 而不是使用原来的 promise?
因为 promise 的状态一旦"成功"或"失败"了,就不能再改变了,所以只能返回新的 promise,这样才可以继续调用下一个then
中的成功/失败回调
接下来,需要实现以下几点
调用 then
方法,创建一个新的 promise, 最后将这个新 promise 返回需要获取 then
方法中onFulfilled
、onRejected
回调函数的返回值,通过新的promise
传递到下一个then
方法中
class Promise { //.... then(onFulfilled, onRejected) { // 新的 promise let promise2 = new Promise((resolve, reject) => {}) if (this.status == ENUM.FULFILLED) { let x = onFulfilled(this.value) } if (this.status == ENUM.REJECTED) { let x = onRejected(this.reason) } if (this.status === ENUM.PENDING) { this.onResolvedCallbacks.push(() => { let x = onFulfilled(this.value) }); this.onRejectedCallbacks.push(() => { let x = onRejected(this.reason); }) } return promise2 } } 复制代码
现在,需要将回调函数执行的返回值 x 传递到下一个 then
方法中,是传递到下一个 then
方法中的成功回调,还是失败回调?需要根据 x 的值来判断。
若 x 是普通值,将通过 promise2 中的 resolve
传递给成功回调;若 x 是个 Error,则通过 promise2 中的 reject
传递给失败回调;当然 x 也又有可能是个 promise 实例,所以都需要考虑到。
因为需要使用 promise2 中的 resolve
, reject
传递 x (两个方法在外部无法获取到), 同时new Promise(executor)
时,executor
是立即执行,所以,将整个 then
方法中的逻辑放到 executor
函数中执行,就可以访问到 resolve
, reject
方法了
class Promise { //.... then(onFulfilled, onRejected) { // 新的 promise let promise2 = new Promise((resolve, reject) => { if (this.status == ENUM.FULFILLED) { // onFulfilled 执行可能报错,使用 try...catch...捕获 try{ let x = onFulfilled(this.value) resolve(x) } catch (e){ reject(e) } } // ... }) return promise2 } } 复制代码
因为返回值 x 存在多种情况, 所以将判断逻辑抽离到外部函数 resolvePromise 中
class Promise { //.... then(onFulfilled, onRejected) { // 新的 promise let promise2 = new Promise((resolve, reject) => { if (this.status == ENUM.FULFILLED) { try{ let x = onFulfilled(this.value) resolvePromise(x, promise2, resolve, reject) } catch (e){ reject(e) } } // ... }) return promise2 } } const resolvePromise = (x, promise2, resolve, reject) => { } 复制代码
相信仔细的小伙伴已经发现,在 new Promise
还没结束就访问 promise2 肯定会报错。只需将 resolvePromise
变成异步代码执行就可以访问到 promise2
//... if (this.status == ENUM.FULFILLED) { setTimeout(() => { try { let x = onFulfilled(this.value) resolvePromise(x, promise2, resolve, reject) } catch (e) { reject(e) } }, 0) } 复制代码
接下来,需要实现 resolvePromise 方法了
2.2.2 resolvePromise 方法
resolvePromise 方法主要是用来解析 x 是否是promise, 按照 Promises/A+规范: the-promise-resolution-procedure 规定,分成以下几步
函数参数 resolvePromise(x, promise2, resolve, reject)
(1) 若 x 和 promise2 引用的是同一个对象,则直接报错。(示例代码如下)
let promise = new Promise((resolve, reject) => {}) let promise2 = promise.then(() => { return promise2 // x 代表了then中函数的返回值,也就是 promise2 }) promise2.then(() => {}, err=> { console.log('err:', err) }) // err: TypeError: Chaining cycle detected for promise #<Promise> (循环引用了) 复制代码
(2) 若 x 是一个普通值,直接通过 resolve
返回(3) 若 x 是一个对象或者函数,判断 x 是否存在 then
方法,当存在then
方法,表明 x 就是一个 promise,此时执行then
方法(4) 执行 then
方法时,有一个成功回调和一个失败回调,执行成功走成功回调,并传入成功结果 y;执行失败走失败回调,并传入失败原因 e, 使用reject
返回(5) 执行成功返回值 y 可能还是个 promise, 继续递归解析 y 的值 (6) then
的回调函数只能执行一次,要么成功,要么失败(设置标识符 called)(7) 当 x 不存在 then
方法时,表明 x 是普通的对象,直接通过resolve
返回
const resolvePromise = (x, promise2, resolve, reject) => { // (1) if (x === promise2) { reject(new TypeError(`TypeError: Chaining cycle detected for promise #<Promise>`)) } if ((typeof x === 'object' && x !== null) || typeof x === 'function') { let called = false // (6) try { const then = x.then // (3) if (typeof then === 'function') { // (4) then.call(x, y => { // (5) y 可能是个 promise if (called) return called = true resolvePromise(y, promise2, resolve, reject) }, e => { if (called) return called = true reject(e) }) } else { // (7) resolve(x) } } catch (e) { // then 执行过程出错,也不能继续向下执行 if (called) return called = true reject(e) } } else { // (2) resolve(x) } } 复制代码
现在 resolvePromise 方法已经基本实现,其中还有以下几点需要说明
为啥需要判断 x 为函数?
因为 resolvePromise 需要兼容其他人写的 promise , 别人的 promise 可能就是一个函数
执行 const then = x.then
为啥需要使用try...catch...
捕获异常 ?
因为可以使用 Object.defineProperties
或 Proxy
改写 x.then 的返回值
执行 then
方法,为啥使用call
, 而不是直接执行x.then()
?
可以复用上次取出来的then
方法,避免二次调用 x.then()
2.2.3 值穿透
new Promise((resolve, reject) => { resolve(123) }).then().then().then(data => { console.log('success:', data) }) // success: 123 复制代码
上面代码中的 123 是如何直接穿透到最后一个 then
方法中的呢?
Promises/A+规范: onFulfilled, onRejected are optional arguments , 规定 then
方法中的 onFulfilled
, onRejected
是可选参数,所以我们需要提供一个默认值
class Promise { // ... then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v onRejected = typeof onRejected === 'function' ? onRejected: e => {throw e} // ... } } 复制代码
通过给 onFulfilled
, onRejected
设置默认值就可以实现值穿透。至此,已经实现 Promises/A+ 中规范的功能,可以对代码进行规范测试了
2.3 规范测试
规范测试,首先需要安装 promises-aplus-tests npm 包,同时需要在导出 Promise
前增加下面测试代码
class Promise { // ... } Promise.defer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve,reject)=>{ dfd.resolve = resolve; dfd.reject = reject; }); return dfd; } module.exports = Promise; 复制代码
安装依赖
npm install promises-aplus-tests -D 复制代码
同时在 package.json 增加
"scripts": { "test": "promises-aplus-tests ./index.js" }, 复制代码
最后,运行 npm run test
就可以进行测试了,测试结果如下
2.4 其它方法和属性
下面介绍的内容,并不是 Promises/A+ 中的规范,但我们也可以继续探索
2.4.1 catch 方法
实例上的 catch
方法用来捕获执行过程中产生的错误,同时返回值为 promise, 参数为一个失败回调函数,相对于执行 then(null, onRejected)
class Promise{ // ... catch (onErrorCallback) { return this.then(null, onErrorCallback) } } 复制代码
2.4.2 finally 方法
finally
的参数是一个回调函数,无论 promise 是执行成功,还是失败,该回调函数都会执行。
应用场景有:页面异步请求数据,无论数据请求成功还是失败,在 finally 回调函数中都关闭 loading。
同时,finally
方法有以下特点
值穿透。可以将前面 promise 的值传递到下一个 then
方法中,或者将错误传递到下一个catch
方法中等待执行。当 finally
回调函数返回一个新的 promise,finally
会等待该 promise 执行结束后才处理传值若该 promise 执行成功, finally
方法将不予理会执行结果,还是将上一个的结果传递到下一个then
中若新的 promise 执行失败报错, finally
方法会将错误原因传递到下一个catch
方法
下面是具体代码演示
// (1) 值穿透, 请注意 finally 的回调函数是不存在参数的 Promise.resolve(100).finally((data) => { console.log('finally: ', data) }).then(data => { console.log('success: ', data) }).catch(err => { console.log('error', err) }) // finally: undefined // success: 100 // (2) 等待执行 // 返回一个执行成功的 promise, 但向下传递但还是上一次执行结果 Promise.resolve(100).finally(() => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 1000) }) }).then(data => { console.log('success: ', data) // success: 100 }).catch(err => { console.log('error', err) }) // 当 promise 执行失败,则将该 promise 执行结果向下传递 Promise.reject(100).finally(() => { return new Promise((resolve, reject) => { setTimeout(() => { reject(200) }, 1000) }) }).then(data => { console.log('success: ', data) }).catch(err => { console.log('error', err) // error 200 }) 复制代码
在掌握了 finally
的用法后,继续探索如何实现它?
class Promise{ finally (callback) { return this.then(value => { return Promise.resolve(callback()).then(() => value) }, err => { return Promise.resolve(callback()).then(() => {throw err}) }) } } 复制代码
2.4.3 静态方法
静态方法是那通过 Promise 来调用,而不是通过实例 promise 来调用的方法
Promise.resolve()、Promise.reject() 返回值:一个成功状态的 promise 、一个失败状态的 promise
class Promise{ // ... // 成功状态 static resolve(value){ return new Promise((resolve, reject) => { resolve(value) }) } // 失败状态 static reject(reason){ return new Promise((resolve, reject) => { reject(reason) }) } } 复制代码
假设执行成功返回值 value
是个 promise,Promise.resolve() 会对该 value 递归解析,直到该 promise 执行结束才会向下执行
class Promise{ constructor() { //... const resolve = (value) => { if (value instanceof Promise) { // 递归解析, 直到 value 为普通值 value.then(resolve, reject) } // ... } const reject = (err) => { // ... } //... } } 复制代码
现在,执行下面代码,就可以正常获取数据了
Promise.resolve(new Promise((resolve, reject) => { setTimeout(() => { resolve('hello') }, 2000) })).then(data => { console.log(data) // hello }) 复制代码
Promise.all()
解决并发问题,多个异步并发并获取最终的结果。
参数是一个 promise数组,当数组中每一项都执行成功,结果就是成功,反之,有一个失败,结果就是失败。
class Promise { static all(arrList) { if (!Array.isArray(arrList)) { const type = typeof arrList; return new TypeError(`TypeError: ${type} ${arrList} is not iterable`) } return new Promise((resolve, reject) => { const backArr = [] const count = 0 const processResultByKey = (value, index) => { backArr[index] = value if (++count === arrList.length) { resolve(backArr) } } for (let i = 0; i < arrList.length; i++) { const item = arrList[i]; if (item && item.then === 'function') { item.then((value) => { processResultByKey(value, i) }, reject) } else { processResultByKey(item, i) } } }) } } 复制代码
⚠️注意:在 all
方法中,是通过 ++count === arrList.length
(count 为计数器) 来判断是否全部执行完成,而不是使用 index === arrlist.length - 1
来判断,具体原因如下
// p1 为 promise 实例 Promise.all([1,2, p1, 4]).then(data => {}) // 当执行数组最后一项时,index === arrlist.length - 1 表达式成立, // 就会执行 resolve 返回执行结果, // 但此时的 p1 可能还没执行结束,所以使用计数器来判断 复制代码
Promise.race()
跟 all
方法不同的是,Promise.race 采用最先成功或最先失败的作为执行结果
class Promise { static race(arrList) { return new Promise((resolve, reject) => { for (let i = 0; i < arrList.length; i++) { const value = arrList[i]; if (value && value.then === 'function') { value.then(resolve, reject) } else { resolve(value) } } }) } } 复制代码
Promise.race 的主要应用场景如下
(基础)多个请求采取最快的 (eg: 小飞机的多个代理线路,哪条线路的响应速度最快,就使用哪条) (高级)封装中断方法,中断 promise
的执行 (异步请求设置超时时间,当超时后,异步请求就会被迫失败)
原生的 promise 上并没有 abort
(停止、中断) 方法,假设使用场景如下
const p1 = new Promise((resolve, reject) => { setTimeout(() => { // 模拟异步请求,5s 后返回 resolve('hello') }, 5000) }) const newP = wrap(p1) setTimeout(() => { // 设置超时时间,超时后,调用 newP.abort newP.abort('请求超时了') }, 4000) newP.then(data => {}).catch(err => {}) 复制代码
newP1 是一个具有 abort
方法的 promise, 超时后就调用 newP.abort()
。
现在需要实现 wrap
封装方法,传入一个普通 promise 实例,返回一个具有 abort
方法的 promise 实例
const wrap = (promise) => { let abort let newPromise = new Promise((resolve, reject) => { abort = reject }) let p = Promise.race([promise, newPromise]) p.abort = abort return p } 复制代码
wrap
方法就是利用 Promise.race 采用最快的作为执行结果这一特性,来看 promise, newPromise
哪个最先执行,而 newPromise
的执行,是通过外部调用 abort 来实现的
三、Promise 的扩展
“⚠️注意:以下对 Promise 的扩展仅适用于 Node 环境
3.1 promisify
功能:把 node 中的一个 api 转换成promise的写法, 以 fs.readFile
读取文件为例
常规写法
const fs = require('fs) fs.readFile('./name.json', (err, data) => {}) 复制代码
缺点:回调地狱嵌套
改成 promisify 链式调用写法
const util = require('util') const read = util.promisify(fs.readFile) read('./name.json').then(data => console.log(data)) 复制代码
特点:promisify 方法特点如下
返回一个函数,函数执行后才返回 promise
const promisify = fn => { return (...args) => { return new Promise((resolve, reject) => { fn(...args, (err, data) => { if (err) reject(err) resolve(data) }) }) } } 复制代码
在 promisify 函数中,执行 fn 函数时,可以手动添加了回调函数是因为 node 中大部分的方法的回调都是这种格式
3.2 bluebird
promisify
方法每次只能修改一个方法,而第三方的库 bluebird 中实现了 promisifyAll
方法,可以将某个对象下所有的方法转换成 promise 写法
const fs = require('fs') const bluebird = require('bluebird'); // 第三方库,需提前安装 const newFs = bluebird.promisifyAll(fs); newFs.readFileAsync('./name.txt', 'utf-8').then(data => {}).catch(err => {}) 复制代码
promisifyAll() 特点如下
函数参数为对象,会将对象上所有的方法,增加一个 Async** **后缀,变成 promise 写法 并没有覆盖原方法,只是扩展
const promisifyAll = (target) { Reflect.ownKeys(target).forEach(key => { target[`${key}Async`] = promisify(target[key]) }) return target } 复制代码
Reflect 对象是 ES 中内置对象,它提供拦截 JavaScript 操作的方法 Reflect | MDN, 此处,也可使用 Object.keys()
。同时,使用了前面的 promisify
来改写方法
3.3 原生 Node 支持
目前,在高版本浏览器中,已经对 api 集成了 promise 的写法,使用如下
const fs = require('fs').promises fs.readFile('./name.txt', 'utf-8').then(data => {}) 复制代码
正因为原生的支持,导致第三方的一些扩展不再流行
四、参考资源
Promises/A+ BAT前端经典面试问题:史上最最最详细的手写Promise教程 - Carus 45道Promise面试题一次爽到底 - LinDaiDai_霖呆呆
五、后语
本文使用 mdnice 排版
这篇关于面试必考 - 手写 Promise, 由浅入深(附源码)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-06-26结对编程到底难不难?答案在这里
- 2024-06-19《2023版Java工程师》课程升级公告
- 2024-06-15matplotlib作图不显示3D图,怎么办?
- 2024-06-1503-Loki 日志监控
- 2024-06-1504-让LLM理解知识 -Prompt
- 2024-06-05做软件测试需要懂代码吗?
- 2024-06-0514-ShardingSphere的分布式主键实现
- 2024-06-03为什么以及如何要进行架构设计权衡?
- 2024-05-31全网首发第二弹!软考2024年5月《软件设计师》真题+解析+答案!(11-20题)
- 2024-05-31全网首发!软考2024年5月《软件设计师》真题+解析+答案!(21-30题)