自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如果面試官讓你講講發(fā)布訂閱設(shè)計(jì)模式?

開發(fā) 前端
發(fā)布訂閱設(shè)計(jì)模式在程序中經(jīng)常涉及,例如 Vue 中的 $on 和 $off、document.addEventListener()、document.removeEventListener()等,發(fā)布訂閱模式可以降低程序的耦合度,統(tǒng)一管理維護(hù)消息、處理事件也使得程序更容易維護(hù)和擴(kuò)展。

[[414875]]

本文轉(zhuǎn)載自微信公眾號「DYBOY」,作者DYBOY。轉(zhuǎn)載本文請聯(lián)系DYBOY公眾號。

發(fā)布訂閱設(shè)計(jì)模式在程序中經(jīng)常涉及,例如 Vue 中的 $on 和 $off、document.addEventListener()、document.removeEventListener()等,發(fā)布訂閱模式可以降低程序的耦合度,統(tǒng)一管理維護(hù)消息、處理事件也使得程序更容易維護(hù)和擴(kuò)展。

有小伙伴問,該如何學(xué)習(xí)設(shè)計(jì)模式,設(shè)計(jì)模式本身是一些問題場景的抽象解決方案,死記硬背肯定不行,無異于搭建空中樓閣,所以得結(jié)合實(shí)際,從解決問題角度去思考、舉一反三,如此便能更輕松掌握知識點(diǎn)。

最近在程序中使用到了 eventEmitter3 這個事件發(fā)布訂閱庫,該庫可用于組件之間的通信管理,通過簡單的 Readme 文檔可學(xué)會如何使用,但同時了解這個庫的設(shè)計(jì)也有助于大家了解認(rèn)識發(fā)布訂閱設(shè)計(jì)模式,不妨一起來看看。

一、定義

在軟件架構(gòu)中,發(fā)布訂閱是一種消息范式,消息的發(fā)送者(稱為發(fā)布者)不會將消息直接發(fā)送給特定的接收者(稱為訂閱者),而是將發(fā)布的消息分為不同的類別,無需了解哪些訂閱者(如果有的話)可能存在。同樣的,訂閱者可以表達(dá)對一個或多個類別的興趣,只接收感興趣的消息,無需了解哪些發(fā)布者(如果有的話)存在。

類比一個很好理解的例子,例如微信公眾號,你關(guān)注(理解為訂閱)了“DYBOY”公眾號,當(dāng)該公眾號發(fā)布了新文章,微信就會通知你,而不會通知其他為訂閱公眾號的人,另外你還可以訂閱多個公眾號。

放到程序的組件中,多個組件的通信除了父子組件傳值外,還有例如 redux、vuex 狀態(tài)管理,另外就是本文所說的發(fā)布訂閱模式,可以通過一個事件中心來實(shí)現(xiàn)。

發(fā)布訂閱模式

二、手搓一個發(fā)布訂閱事件中心

“紙上得來終覺淺,絕知此事要躬行”,所以根據(jù)定義,我們嘗試實(shí)現(xiàn)一個JavaScript版本的發(fā)布訂閱事件中心,看看會遇到哪些問題?

2.1 基本結(jié)構(gòu)版

首先實(shí)現(xiàn)的 DiyEventEmitter 如下:

  1. /** 
  2.  * 事件發(fā)布訂閱中心 
  3.  */ 
  4. class DiyEventEmitter { 
  5.   static instance: DiyEventEmitter; 
  6.   private _eventsMap: Map<string, Array<() => void>>; 
  7.  
  8.   static getInstance() { 
  9.     if (!DiyEventEmitter.instance) { 
  10.       DiyEventEmitter.instance = new DiyEventEmitter(); 
  11.     } 
  12.     return DiyEventEmitter.instance; 
  13.   } 
  14.  
  15.   constructor() { 
  16.     this._eventsMap = new Map(); // 事件名與回調(diào)函數(shù)的映射Map 
  17.   } 
  18.  
  19.   /** 
  20.    * 事件訂閱 
  21.    * 
  22.    * @param eventName 事件名 
  23.    * @param eventFnCallback 事件發(fā)生時的回調(diào)函數(shù) 
  24.    */ 
  25.   public on(eventName: string, eventFnCallback: () => void) { 
  26.     const newArr = this._eventsMap.get(eventName) || []; 
  27.     newArr.push(eventFnCallback); 
  28.     this._eventsMap.set(eventName, newArr); 
  29.   } 
  30.  
  31.   /** 
  32.    * 取消訂閱 
  33.    * 
  34.    * @param eventName 事件名 
  35.    * @param eventFnCallback 事件發(fā)生時的回調(diào)函數(shù) 
  36.    */ 
  37.   public off(eventName: string, eventFnCallback?: () => void) { 
  38.     if (!eventFnCallback) { 
  39.       this._eventsMap.delete(eventName); 
  40.       return
  41.     } 
  42.  
  43.     const newArr = this._eventsMap.get(eventName) || []; 
  44.     for (let i = newArr.length - 1; i >= 0; i--) { 
  45.       if (newArr[i] === eventFnCallback) { 
  46.         newArr.splice(i, 1); 
  47.       } 
  48.     } 
  49.     this._eventsMap.set(eventName, newArr); 
  50.   } 
  51.  
  52.   /** 
  53.    * 主動通知并執(zhí)行注冊的回調(diào)函數(shù) 
  54.    * 
  55.    * @param eventName 事件名 
  56.    */ 
  57.   public emit(eventName: string) { 
  58.     const fns = this._eventsMap.get(eventName) || []; 
  59.     fns.forEach(fn => fn()); 
  60.   } 
  61.  
  62. export default DiyEventEmitter.getInstance(); 

導(dǎo)出的 DiyEventEmitter 是一個“單例”,保證在全局中只有唯一“事件中心”實(shí)例,使用時候直接可使用公共方法

  1. import e from "./DiyEventEmitter"
  2.  
  3. const subscribeFn = () => { 
  4.   console.log("DYBOY訂閱收到了消息"); 
  5. }; 
  6. const subscribeFn2 = () => { 
  7.   console.log("DYBOY第二個訂閱收到了消息"); 
  8. }; 
  9.  
  10. // 訂閱 
  11. e.on("dyboy", subscribeFn); 
  12. e.on("dyboy", subscribeFn2); 
  13.  
  14. // 發(fā)布消息 
  15. e.emit("dyboy"); 
  16.  
  17. // 取消第一個訂閱消息的綁定 
  18. e.off("dyboy", subscribeFn); 
  19.  
  20. // 第二次發(fā)布消息 
  21. e.emit("dyboy"); 

輸出 console 結(jié)果:

  1. DYBOY訂閱收到了消息 
  2. 第二個訂閱的消息 
  3. 第二個訂閱的消息 

那么第一版的支持訂閱、發(fā)布、取消的“發(fā)布訂閱事件中心”就OK了。

2.2 支持只訂閱一次once方法

在一些場景下,某些事件訂閱可能只需要執(zhí)行一次,后續(xù)的通知將不再響應(yīng)。

實(shí)現(xiàn)的思路:新增 once 訂閱方法,當(dāng)響應(yīng)了對應(yīng)“發(fā)布者消息”,則主動取消訂閱當(dāng)前執(zhí)行的回調(diào)函數(shù)。

為此新增類型,如此便于回調(diào)函數(shù)的描述信息擴(kuò)展:

  1. type SingleEvent = { 
  2.   fn: () => void; 
  3.   once: boolean; 
  4. }; 

_eventsMap的類型更改為:

  1. private _eventsMap: Map<string, Array<SingleEvent>>; 

同時抽出公共方法 addListener,供 on 和 once 方法共用:

  1. private addListener( eventName: string, eventFnCallback: () => void, once = false) { 
  2.   const newArr = this._eventsMap.get(eventName) || []; 
  3.   newArr.push({ 
  4.     fn: eventFnCallback, 
  5.     once, 
  6.   }); 
  7.   this._eventsMap.set(eventName, newArr); 
  8.  
  9. /** 
  10.  * 事件訂閱 
  11.  * 
  12.  * @param eventName 事件名 
  13.  * @param eventFnCallback 事件發(fā)生時的回調(diào)函數(shù) 
  14.  */ 
  15. public on(eventName: string, eventFnCallback: () => void) { 
  16.   this.addListener(eventName, eventFnCallback); 
  17.  
  18. /** 
  19.  * 事件訂閱一次 
  20.  * 
  21.  * @param eventName 事件名 
  22.  * @param eventFnCallback 事件發(fā)生時的回調(diào)函數(shù) 
  23.  */ 
  24. public once(eventName: string, eventFnCallback: () => void) { 
  25.   this.addListener(eventName, eventFnCallback, true); 

與此同時,我們需要考慮在觸發(fā)事件時候,執(zhí)行一次就需要取消訂閱

  1. /** 
  2.  * 觸發(fā):主動通知并執(zhí)行注冊的回調(diào)函數(shù) 
  3.  * 
  4.  * @param eventName 事件名 
  5.  */ 
  6. public emit(eventName: string) { 
  7.   const fns = this._eventsMap.get(eventName) || []; 
  8.   fns.forEach((evt, index) => { 
  9.     evt.fn(); 
  10.     if (evt.once) fns.splice(index, 1); 
  11.   }); 
  12.   this._eventsMap.set(eventName, fns); 

另外取消訂閱中函數(shù)中比較需要替換對象屬性比較:newArr[i].fn === eventFnCallback

這樣我們的事件中心支持 once 方法改造就完成了。

2.3 緩存發(fā)布消息

在框架開發(fā)下,通常會使用異步按需加載組件,如果發(fā)布者組件先發(fā)布了消息,但是異步組件還未加載完成(完成訂閱注冊),那么發(fā)布者的這條發(fā)布消息就不會被響應(yīng)。因此,我們需要把消息做一個緩存隊(duì)列,直到有訂閱者訂閱了,并只響應(yīng)一次緩存的發(fā)布消息,該消息就會從緩存出隊(duì)。

首先梳理下緩存消息的邏輯流程:

UML時序圖

發(fā)布者發(fā)布消息,事件中心檢測是否存在訂閱者,如果沒有訂閱者訂閱此條消息,則把該消息緩存到離線消息隊(duì)列中,當(dāng)有訂閱者訂閱時,檢測是否訂閱了緩存中的事件消息,如果是,則該事件的緩存消息依次出隊(duì)(FCFS調(diào)度執(zhí)行),觸發(fā)訂閱者回調(diào)函數(shù)執(zhí)行一次。

新增離線消息緩存隊(duì)列:

  1. private _offlineMessageQueue: Map<string, number>; 

在emit發(fā)布消息中判斷對應(yīng)事件是否有訂閱者,沒有訂閱者則向離線事件消息中更新

  1. /** 
  2.  * 觸發(fā):主動通知并執(zhí)行注冊的回調(diào)函數(shù) 
  3.  * 
  4.  * @param eventName 事件名 
  5.  */ 
  6. public emit(eventName: string) { 
  7.   const fns = this._eventsMap.get(eventName) || []; 
  8. +  if (fns.length === 0) { 
  9. +    const counter = this._offlineMessageQueue.get(eventName) || 0; 
  10. +    this._offlineMessageQueue.set(eventName, counter + 1); 
  11. +    return
  12. +  } 
  13.   fns.forEach((evt, index) => { 
  14.     evt.fn(); 
  15.     if (evt.once) fns.splice(index, 1); 
  16.   }); 
  17.   this._eventsMap.set(eventName, fns); 

然后在 addListener 方法中根據(jù)離線事件消息統(tǒng)計(jì)的次數(shù),重新emit發(fā)布事件消息,觸發(fā)消息回調(diào)函數(shù)執(zhí)行,之后刪掉離線消息中的對應(yīng)事件。

  1. private addListener( 
  2.   eventName: string, 
  3.   eventFnCallback: () => void, 
  4.   once = false 
  5. ) { 
  6.   const newArr = this._eventsMap.get(eventName) || []; 
  7.   newArr.push({ 
  8.     fn: eventFnCallback, 
  9.     once, 
  10.   }); 
  11.   this._eventsMap.set(eventName, newArr); 
  12.  
  13. +  const cacheMessageCounter = this._offlineMessageQueue.get(eventName); 
  14. +  if (cacheMessageCounter) { 
  15. +    for (let i = 0; i < cacheMessageCounter; i++) { 
  16. +      this.emit(eventName); 
  17. +    } 
  18. +    this._offlineMessageQueue.delete(eventName); 
  19. +  } 

這樣,一個支持離線消息的事件中心就寫好了!

2.4 回調(diào)函數(shù)傳參&執(zhí)行環(huán)境

在上面的回調(diào)函數(shù)中,我們可以發(fā)現(xiàn)是一個沒有返回值,沒有入?yún)⒌暮瘮?shù),這其實(shí)有些雞肋,在函數(shù)運(yùn)行的時候會指向執(zhí)行的上下文,可能某些回調(diào)函數(shù)中含有this指向就無法綁定到事件中心上,因此針對回調(diào)函數(shù)需要綁定執(zhí)行上下文環(huán)境。

2.4.1 支持回調(diào)函數(shù)傳參

首先將TypeScript中的函數(shù)類型fn: () => void 改為 fn: Function,這樣能夠通過函數(shù)任意參數(shù)長度的TS校驗(yàn)。

其實(shí)在事件中心里回調(diào)函數(shù)是沒有參數(shù)的,如有參數(shù)也是提前通過參數(shù)綁定(bind)方式傳入。

另外如果真要支持回調(diào)函數(shù)傳參,那么就需要在 emit() 的時候傳入?yún)?shù),然后再將參數(shù)傳遞給回調(diào)函數(shù),這里我們暫時先不實(shí)現(xiàn)了。

2.4.2 執(zhí)行環(huán)境綁定

在需要實(shí)現(xiàn)執(zhí)行環(huán)境綁定這個功能前,先思考一個問題:“是應(yīng)該開發(fā)者自行綁定還是應(yīng)該事件中心來做?”

換句話說,開發(fā)者在 on('eventName', 回調(diào)函數(shù)) 的時候,是否應(yīng)該主動綁定 this 指向?在當(dāng)前設(shè)計(jì)下,初步認(rèn)為無參數(shù)的回調(diào)函數(shù)自行綁定 this 比較合適。

因此,在事件中心這暫時不需要去做綁定參數(shù)的行為,如果回調(diào)函數(shù)內(nèi)有需要傳參、綁定執(zhí)行上下文的,需要在綁定回調(diào)函數(shù)的時候自行 bind。這樣,我們的事件中心也算是保證了功能的純凈性。

到這里我們自己手搓簡單的發(fā)布訂閱事件中心就完成了!

三、學(xué)習(xí)EventEmitter3的設(shè)計(jì)實(shí)現(xiàn)

雖然我們按照自己的理解實(shí)現(xiàn)了一版,但是沒有對比我們也不知道好壞,因此一起看看 EventEmitter3 這個優(yōu)秀“極致性能優(yōu)化”的庫是怎么去處理事件訂閱與發(fā)布,同時可以學(xué)習(xí)下其中的性能優(yōu)化思路。

首先,EventEmitter3(后續(xù)簡稱:EE3)的實(shí)現(xiàn)思路,用Events對象作為“回調(diào)事件對象”的存儲器,類比我們上述實(shí)現(xiàn)的“發(fā)布訂閱模式”作為事件的執(zhí)行邏輯,另外addListener() 函數(shù)增加了傳入執(zhí)行上下文環(huán)境參數(shù),emit() 函數(shù)支持最多傳入5個參數(shù),同時EventEmitter3中還加入了監(jiān)聽器計(jì)數(shù)、事件名前綴。

3.1 Events存儲器

避免轉(zhuǎn)譯,以及為了提升兼容性和性能,EventEmitter3用ES5來編寫。

在JavaScript中萬物是對象,函數(shù)也是對象,因此存儲器的實(shí)現(xiàn):

  1. function Events() {} 

3.2 事件偵聽器實(shí)例

同理,我們上述使用singleEvent對象來存儲每一個事件偵聽器實(shí)例,EE3 中用一個EE對象存儲每個事件偵聽器的實(shí)例以及必要屬性

  1. /** 
  2.  * 每個事件偵聽器實(shí)例的表示形式 
  3.  * 
  4.  * @param {Function} fn 偵聽器函數(shù) 
  5.  * @param {*} context 調(diào)用偵聽器的執(zhí)行上下文 
  6.  * @param {Boolean} [once=false] 指定偵聽器是否僅支持調(diào)用一次 
  7.  * @constructor 
  8.  * @private 
  9.  */ 
  10. function EE(fn, context, once) { 
  11.   this.fn = fn; 
  12.   this.context = context; 
  13.   this.once = once || false

3.3 添加偵聽器方法

  1. /** 
  2.  * 為給定事件添加偵聽器 
  3.  * 
  4.  * @param {EventEmitter} emitter EventEmitter實(shí)例的引用. 
  5.  * @param {(String|Symbol)} event 事件名. 
  6.  * @param {Function} fn 偵聽器函數(shù). 
  7.  * @param {*} context 調(diào)用偵聽器的上下文. 
  8.  * @param {Boolean} once 指定偵聽器是否僅支持調(diào)用一次. 
  9.  * @returns {EventEmitter} 
  10.  * @private 
  11.  */ 
  12. function addListener(emitter, event, fn, context, once) { 
  13.   if (typeof fn !== 'function') { 
  14.     throw new TypeError('The listener must be a function'); 
  15.   } 
  16.  
  17.   var listener = new EE(fn, context || emitter, once) 
  18.     , evt = prefix ? prefix + event : event; 
  19.  
  20.   // TODO: 這里為什么先是使用對象,多個的時候使用對象數(shù)組存儲,有什么好處? 
  21.   if (!emitter._events[evt]) emitter._events[evt] = listener, emitter._eventsCount++; 
  22.   else if (!emitter._events[evt].fn) emitter._events[evt].push(listener); 
  23.   else emitter._events[evt] = [emitter._events[evt], listener]; 
  24.  
  25.   return emitter; 

該“添加偵聽器”的方法有幾個關(guān)鍵功能點(diǎn):

如果有前綴,給事件名增加前綴,避免事件沖突

每次新增事件名則 _eventsCount+1,用于快速讀寫所有事件的數(shù)量

如果事件只有單個偵聽器,則 _events[evt] 指向這個 EE 對象,訪問效率更高

3.4 清除事件

  1. /** 
  2.  * 通過事件名清除事件 
  3.  * 
  4.  * @param {EventEmitter} emitter EventEmitter實(shí)例的引用 
  5.  * @param {(String|Symbol)} evt 事件名 
  6.  * @private 
  7.  */ 
  8. function clearEvent(emitter, evt) { 
  9.   if (--emitter._eventsCount === 0) emitter._events = new Events(); 
  10.   else delete emitter._events[evt]; 

清除事件,只需要使用 delete 關(guān)鍵字,刪除對象上的屬性

另外這里一個很巧妙的地方在于,依賴事件計(jì)數(shù)器,如果計(jì)數(shù)器為0,則重新創(chuàng)建一個 Events 存儲器指向 emitter 的 _events 屬性。

這樣做的優(yōu)點(diǎn)是,假如需要清空所有事件,只需要將 emitter._eventsCount 的值賦值為1,然后調(diào)用 clearEvent() 方法就可以了,而不必遍歷清除事件

3.5 EventEmitter

  1. function EventEmitter() { 
  2.   this._events = new Events(); 
  3.   this._eventsCount = 0; 

EventEmitter 對象參考 NodeJS 中的事件觸發(fā)器,定義了最小的接口模型,包含 _events 和 _eventsCount屬性,另外的方法都通過原型來增加。

EventEmitter 對象等同于上述我們的事件中心的定義,其功能梳理如下:

EventEmitter

其中有必要講的就是 emit() 方法,而訂閱者注冊事件的on() 和 once() 方法,都是使用的 addListener() 工具函數(shù)。

emit() 方法實(shí)現(xiàn)如下:

  1. /** 
  2.  * 調(diào)用執(zhí)行指定事件名的每一個偵聽器 
  3.  * 
  4.  * @param {(String|Symbol)} event 事件名. 
  5.  * @returns {Boolean} `true` 如果當(dāng)前事件名沒綁定偵聽器,則返回false
  6.  * @public 
  7.  */ 
  8. EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { 
  9.   var evt = prefix ? prefix + event : event; 
  10.  
  11.   if (!this._events[evt]) return false
  12.  
  13.   var listeners = this._events[evt] 
  14.     , len = arguments.length 
  15.     , args 
  16.     , i; 
  17.  
  18.   // 如果只有一個偵聽器綁定了該事件名 
  19.   if (listeners.fn) { 
  20.     // 如果是執(zhí)行一次的,則移除偵聽器 
  21.     if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); 
  22.      
  23.     // Refrence:https://juejin.cn/post/6844903496450310157 
  24.     // 這里的處理是從性能上考慮,傳入5個入?yún)?shù)的調(diào)用call方法處理 
  25.     // 超過5個參數(shù)的使用apply處理 
  26.     // 大部分場景超過5個參數(shù)的都是少數(shù) 
  27.     switch (len) { 
  28.       case 1: return listeners.fn.call(listeners.context), true
  29.       case 2: return listeners.fn.call(listeners.context, a1), true
  30.       case 3: return listeners.fn.call(listeners.context, a1, a2), true
  31.       case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true
  32.       case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true
  33.       case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true
  34.     } 
  35.  
  36.     for (i = 1, args = new Array(len -1); i < len; i++) { 
  37.       args[i - 1] = arguments[i]; 
  38.     } 
  39.  
  40.     listeners.fn.apply(listeners.context, args); 
  41.   } else { 
  42.     // 當(dāng)有多個偵聽器綁定了同一個事件名 
  43.     var length = listeners.length 
  44.       , j; 
  45.      
  46.     // 循環(huán)執(zhí)行每一個綁定的事件偵聽器 
  47.     for (i = 0; i < length; i++) { 
  48.       if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); 
  49.  
  50.       switch (len) { 
  51.         case 1: listeners[i].fn.call(listeners[i].context); break; 
  52.         case 2: listeners[i].fn.call(listeners[i].context, a1); break; 
  53.         case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; 
  54.         case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; 
  55.         default
  56.           if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { 
  57.             args[j - 1] = arguments[j]; 
  58.           } 
  59.           listeners[i].fn.apply(listeners[i].context, args); 
  60.       } 
  61.     } 
  62.   } 
  63.  
  64.   return true
  65. }; 

在 emit() 方法中顯示的傳入了五個入?yún)ⅲ篴1 ~ a5,同時優(yōu)先使用 call() 方法綁定 this 指向并執(zhí)行偵聽器的回調(diào)函數(shù)。

這樣處理的原因是,call 方法比 apply 方法效率更高,相關(guān)比較驗(yàn)證討論可參考《call和apply的性能對比》

到這基本上 EventEmitter3 的實(shí)現(xiàn)就啃完了!

四、總結(jié)

EventEmitter3 是一個號稱優(yōu)化到極致的事件發(fā)布訂閱的工具庫,通過梳理可知曉:

 

  • call 與 apply 在效率上的差異
  • 對象和對象數(shù)組的存取性能考慮
  • 理解發(fā)布訂閱模式,以及在事件系統(tǒng)中的應(yīng)用實(shí)例

 

責(zé)任編輯:武曉燕 來源: DYBOY
相關(guān)推薦

2022-04-29 08:17:38

RPC遠(yuǎn)程代理代理模式

2021-11-08 11:32:01

觀察

2020-11-06 07:11:40

內(nèi)存虛擬Redis

2020-07-28 00:58:20

IP地址子網(wǎng)TCP

2015-08-13 10:29:12

面試面試官

2021-01-14 05:23:32

高并發(fā)消息中間件

2021-02-28 07:52:24

蠕蟲數(shù)據(jù)金絲雀

2020-12-09 05:18:17

面試觀察者訂閱模式

2020-06-17 21:22:56

Serverless面試官架構(gòu)

2020-07-03 07:39:45

查詢語句

2023-07-13 08:19:30

HaspMapRedis元素

2024-08-16 13:59:00

2020-10-15 06:26:24

高并發(fā)場景冰河

2020-09-07 06:28:37

Nginx靜態(tài)負(fù)載均衡動態(tài)負(fù)載均衡

2021-06-29 11:05:25

MySQLCPU數(shù)據(jù)庫

2020-11-02 07:02:10

加載鏈接初始化

2023-07-11 08:50:34

2024-08-23 11:51:39

2021-10-29 09:40:21

設(shè)計(jì)模式軟件

2020-09-14 06:57:30

緩存穿透雪崩
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號