精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

Vite 熱更新(HMR)原理了解一下

開發 項目管理
在開發環境,Vite以原生ESM方式提供源碼,讓瀏覽器接管了打包程序的部分工作:Vite 只需要在瀏覽器請求源碼時進行轉換并按需提供源碼。根據情景動態導入代碼,即只在當前屏幕上實際使用時才會被處理。

前言

用過Vite進行項目開發的同學,肯定聽說過,Vite在開發環境和生產環境是兩種不同的資源處理方式。

在開發環境,Vite以原生ESM方式提供源碼,讓瀏覽器接管了打包程序的部分工作:Vite 只需要在瀏覽器請求源碼時進行轉換并按需提供源碼。根據情景動態導入代碼,即只在當前屏幕上實際使用時才會被處理。

圖片圖片

而在本地開發中,肯定會有本地代碼的變更處理,如何最大限度的在不刷新整體頁面的情況下,進行代碼的替換呢。這就用到HMR[1]這一特性。而承載HMR的部分就是,我們需要在開發階段啟動一個Dev Server。體現在代碼中就是我們在Vite的配置文件- vite.config.ts中會有一個單獨的字段 - server,更詳細的解釋可以參看vite_開發服務器選項[2]

HMR 允許我們在不刷新頁面的情況下更新代碼,比如編輯組件標記或調整樣式,這些更改會立即反映在瀏覽器中,從而實現更快的代碼交互和更好的開發體驗。

在生產環境中,Vite利用Rollup對代碼進行打包處理,并配合著tree-shaking/懶加載和chunk分割的方式為瀏覽器提供最后的代碼資源。體現在代碼中就是我們在Vite的配置文件- vite.config.ts中會有一個單獨的字段 - build,更詳細的解釋可以參看vite_構建選項[3]

我們在之前的淺聊Vite中介紹過了,Vite內部打包流程。

圖片圖片

而今天我們來講講,在開發環境中,Vite是如何實現HMR的。

當然,針對不同的打包工具,可能有自己的實現原理。如果大家對其他工具的HMR感興趣。可以從下方鏈接中自行探索。

  1. webpack-hrm[4]
  2. rollup-plugin-hot[5]

當然,我們下面的內容,盡量從代碼的頂層設計去探索,如果大家想看Vite -HMR的具體實現可以找到對應的部分,自行探索。

  1. /@vite/client 源碼[6] 下文會有對應介紹
  2. vite_hmr的核心部分[7]
  3. vite_hmr傳播[8] 下文會有對應介紹

好了,天不早了,干點正事哇。

我們能所學到的知識點

  1. 模塊替換
  2. HMR何時發生
  3. HMR 客戶端

1. 模塊替換

模塊替換的基本原理是,在應用程序「運行時動態替換模塊」。

大多數打包工具使用 ECMAScript 模塊(ESM)作為模塊,因為它「更容易分析導入和導出」,這有助于確定一個模塊的替換會如何影響其他相關模塊。關于ESM的介紹,可以看我們之前的文章~你真的了解ESM嗎?

一個模塊通常可以訪問 HMR API,以處理舊模塊刪除和新模塊新增的情況。在 Vite 中,我們可以使用以下 API:

  • import.meta.hot.accept()[9]
  • import.meta.hot.dispose()[10]
  • import.meta.hot.prune()[11]
  • import.meta.hot.invalidate()[12]

從更高層次上看,我們就可以得到如下的模塊處理流程。

圖片圖片

還需要注意的是,我們需要使用這些 API 才能讓 HMR 工作。例如,Vite 默認情況下會為 CSS 文件使用這些 API,但對于像 Vue 這樣的其他文件,我們可以使用一個 Vite 插件來使用這些 HMR API。或者根據需要手動處理。否則,對文件的更新將導致默認情況下進行完整頁面重新加載。

針對不同的語言環境,也是需要對應的插件進行這些api的調用處理。下面列舉幾個比較場景的插件實現

  1. React: Fast Refresh[13]和@vitejs/plugin-react[14]
  2. Vue: @vue/runtime-core[15] 和 @vitejs/plugin-vue[16]
  3. Svelte:svelte-hmr[17]和@vitejs/plugin-svelte[18]

在Vite官網中,有這樣的介紹,

圖片圖片

而handleHotUpdate用于處理HRM更新。圖片我們從vite-vue中就可以看到對應的處理過程。圖片

?

上面是寫插件需要注意的地方,而我們繼續深入vite中HRM的對應API的工作原理。

accept()

import.meta.hot.accept()

當我們使用 import.meta.hot.accept() 添加一個回調時,該回調將負責「用新模塊替換舊模塊」。使用此 API 的模塊也稱為 已接受模塊。

已接受模塊創建了一個 HMR 邊界。一個 HMR 邊界包含模塊本身以及所有遞歸導入的模塊。接受模塊通常也是 HMR 邊界的 根,因為邊界通常是「圖形結構」。

圖片圖片

已接受模塊也可以根據 HMR回調的位置縮小范圍,如果accept中只接受一個回調,此時模塊被稱為 自接受模塊。

import.meta.hot.accept 有兩種函數簽名:

  • import.meta.hot.accept(cb: Function) - 接受來自自身的更改
  • import.meta.hot.accept(deps: string | string[], cb: Function) - 接受來自導入的模塊的更改

如果使用第一種簽名,就是自接受模塊。

自接受模塊

export let data = [1, 2, 3]

if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 用新值替換舊值
    data = newModule.data
  })
}

已接受模塊

import { value } from './stuff.js'

document.querySelector('#value').textContent = value

if (import.meta.hot) {
  import.meta.hot.accept(['./stuff.js'], ([newModule]) => {
    // 用新值重新渲染
    document.querySelector('#value').textContent = newModule.value
  })
}

dispose()

import.meta.hot.dispose()

當一個已接受模塊被替換為新模塊,或者被移除時,我們可以使用 import.meta.hot.dispose() 進行清理。這允許我們清理掉舊模塊創建的任何副作用,例如刪除事件監聽器、清除計時器或重置狀態。

globalThis.__my_lib_data__ = {}

if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    // 重置全局狀態
    globalThis.__my_lib_data__ = {}
  })
}

prune()

import.meta.hot.prune()

當一個模塊要從運行時「完全移除時」,例如一個文件被刪除,我們可以使用 import.meta.hot.prune() 進行「最終清理」。這類似于 import.meta.hot.dispose(),但只在模塊被移除時調用一次。

Vite 通過導入分析階段來進行模塊清理,因為我們能夠知道「一個模塊不再被使用的唯一時機是當它不再被任何模塊導入」。

以下是 Vite 使用該 API 處理 CSS HMR 的示例:

// 導入用于更新/移除 HTML 中樣式標簽的工具
import { updateStyle, removeStyle } from '/@vite/client'

updateStyle('/src/style.css', 'body { color: red; }')

if (import.meta.hot) {
  // 空的回調表示我們接受了這個模塊,但是我們可以啥都不做
  // `updateStyle` 將自動刪除舊的樣式標簽。
  import.meta.hot.accept()
  // 當模塊不再被使用時,移除樣式
  import.meta.hot.prune(() => {
    removeStyle('/src/style.css')
  })
}

invalidate()

import.meta.hot.invalidate()

與上述 API 不同,import.meta.hot.invalidate() 是一個「操作」,而不是一個生命周期鉤子。我們通常會在 import.meta.hot.accept 中使用它,在運行時可能會意識到模塊無法安全更新時,我們需要退出。

當調用這個方法時,Vite服務器將被告知「該模塊已失效」,就像該模塊已被更新一樣。HMR傳播將再次執行,以確定其導入者是否可以遞歸地接受此更改。

export let data = [1, 2, 3]

if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // 如果 `data` 導出被刪除或重命名
    if (!(data in newModule)) {
      // 退出并使模塊失效
      import.meta.hot.invalidate()
    }
  })
}

上述就是針對涉及到HRM的相關API的簡單介紹。更具體的解釋,可以參考vite_hmr[19]

2. HMR何時發生

既然,HMR API賦予了我們替換和刪除模塊的能力,光有能力是不行的,我們需要了解它們何時才會起作用。其實,HMR 通常發生在「編輯文件之后」,但是之后又發生了啥,我們不得而知,這就是我們這節需要了解的內容。

它的總體流程如下:

圖片圖片

讓我們來逐步揭開它神秘的面紗!

編輯文件

當我們編輯文件并保存時,HMR 就開始了。文件系統監視器(例如 chokidar[20])會檢測到更改并將編輯后的「文件路徑」傳遞到下一步。

處理編輯后的模塊

Vite 開發服務器得知了編輯后的文件路徑。然后「使用文件路徑來找到模塊圖中相關的模塊」。

文件和模塊是兩個不同的概念,一個文件可能對應一個或多個模塊。例如,一個 Vue 文件可以編譯成一個 JavaScript模塊和一個相關的 CSS模塊。

然后,這些模塊被傳遞給 Vite 插件的 handleHotUpdate() 鉤子進行進一步處理。它們可以選擇過濾或擴展模塊數組。最終的模塊將傳遞到下一步。

過濾模塊數組

在上一節介紹HMR API時,就介紹過handleHotUpdate,為了節省時間,我們再次將其搬過來。

圖片圖片

function vuePlugin() {
  return {
    name: 'vue',
    handleHotUpdate(ctx) {
      if (ctx.file.endsWith('.vue')) {
        const oldContent = cache.get(ctx.file)
        const newContent = await ctx.read()
        // 如果編輯文件時只有樣式發生了變化,我們可以過濾掉 JS 模塊,并僅觸發 CSS 模塊的 HMR。
        if (isOnlyStyleChanged(oldContent, newContent)) {
          return ctx.modules.filter(m => m.url.endsWith('.css'))
        }
      }
    }
  }
}

上面只是一個簡單示例,像我們比較熟悉的vite-vue其實處理HMR的邏輯差不多,只不過新增了一些額外的校驗和處理。

圖片圖片

更詳細的代碼是可以實現,可以參考github_vite-plugin-vue[21]

擴展模塊數組

function globalCssPlugin() {
  return {
    name: 'global-css',
    handleHotUpdate(ctx) {
      if (ctx.file.endsWith('.css')) {
        // 如果編輯了 CSS 文件,我們還會觸發此特殊的 `virtual:global-css` 模塊的 HMR,該模塊需要重新轉換。
        const mod = ctx.server.moduleGraph.getModuleById('virtual:global-css')
        if (mod) {
          return ctx.modules.concat(mod)
        }
      }
    }
  }
}

模塊失效

在 HMR傳播之前,我們需要將最終更新的模塊數組及其導入者遞歸失效。每個模塊的「轉換代碼都將被移除,并附加一個失效時間戳」。時間戳將用于在客戶端的下一個請求中獲取新模塊。

HMR 傳播

現在,最終的更新模塊數組將通過 HMR 傳播。這是HMR是否起作用的核心步驟,如果傳播過程有數據丟失,那么HMR就會達不到我們想要的預期,也就是部分模塊沒及時更新或者更新失敗了。

HMR 傳播就是以更新的模塊作為起點,向四周擴散,最后找到與該模塊相關的模塊信息,并且形成一個「無形」的環。或者給它起一個更高大上的名字 - HMR 邊界

  • 如果所有更新的模塊都在一個邊界內,Vite 開發服務器將通知 HMR 客戶端通知接受的模塊執行 HMR。
  • 如果有些模塊不在邊界內,則會觸發完整的頁面重新加載。

案例分析

為了更好地理解它是如何工作的,讓我們來看幾個例子。

圖片圖片

  1. app.jsx 是一個接受模塊,也就意味著,在其內部觸發了import.meta.hot.accept()
  2. 與app.jsx相關的文件有stuff.js和utils.js,也就意味著,它們三個會形成一個HMR 邊界
情況 1

如果更新 stuff.js,傳播將「遞歸查找」其導入者以找到一個接受的模塊。在這種情況下,我們將發現 app.jsx 是一個接受的模塊。但在結束傳播之前,我們需要確定 app.jsx 是否可以接受來自 stuff.js 的更改。這取決于 import.meta.hot.accept() 的調用方式。

  • 情況 1(a):如果 app.jsx 是自接受的,或者它接受來自 stuff.js 的更改,我們可以在這里停止傳播,因為沒有其他來自 stuff.js 的導入者。然后,HMR 客戶端將通知 app.jsx 執行 HMR。
  • 情況 1(b):如果 app.jsx 不接受這個更改,我們將繼續向上傳播以找到一個接受的模塊。但由于沒有其他接受的模塊,我們將到達項目的「根節點」 - index.html 文件。此時將觸發整個項目的重新加載。
情況 2:

如果更新 main.js 或 other.js,傳播將再次遞歸查找其「導入者」。然而,沒有接受的模塊,我們將到達項目的「根節點」 - index.html 文件。因此,將觸發完整的頁面重新加載。

情況 3:

如果更新 app.jsx,我們立即發現它是一個接受的模塊。然而,一些模塊可能無法更新其自身的更改。我們可以通過檢查它們是否是自接受的模塊來確定它們是否可以更新自身。

  • 情況 3(a):如果 app.jsx 是自接受的,我們可以在這里停止,并讓 HMR 客戶端通知它執行 HMR。
  • 情況 3(b):如果 app.jsx不是自接受的,我們將繼續向上傳播以找到一個接受的模塊。但由于它們都沒有,我們將到達項目的「根節點」 - index.html 文件,將觸發完整的頁面重新加載。
情況 4:

如果更新 utils.js,傳播將再次遞歸查找其導入者。首先,我們將找到 app.jsx 作為接受的模塊,并在那里停止傳播(例如情況 1(a))。然后,我們也會遞歸查找 other.js 及其導入者,但沒有接受的模塊,我們將到達項目的「根節點」 - index.html 文件。

最后,HMR傳播的結果是是否需要進行完整頁面重新加載,或者是否應該在客戶端應用 HMR 更新。

3. HMR 客戶端

在 Vite 應用中,我們可能會注意到 HTML 中添加了一個特殊的腳本<script type="module" src="/@vite/client"></script>,請求 /@vite/client。這個腳本包含了 HMR 客戶端!

我們可以在Chrome-devtool-sources中進行查看

圖片圖片

HMR 客戶端負責:

  1. 與 Vite 開發服務器建立 WebSocket 連接。
  2. 監聽來自服務器的 HMR 載荷。
  3. 在運行時提供和觸發 HMR API。
  4. 將任何事件發送回 Vite 開發服務器。

從更廣泛的角度來看,HMR 客戶端幫助將 Vite 開發服務器和 HMR API 粘合在一起。

圖片圖片

客戶端初始化

在 HMR 客戶端能夠從 Vite 開發服務器接收任何消息之前,它首先需要建立與其的連接,通常是通過 WebSockets[22]。

下面是一個設置 WebSocket 連接并處理 HMR 傳播結果的示例:

/@vite/client

const ws = new WebSocket('ws://localhost:5173')

ws.addEventListener('message', ({ data }) => {
  const payload = JSON.parse(data)
  switch (payload.type) {
    case '...':
    // 處理載荷...
  }
})

// 將任何事件發送回 Vite 開發服務器
ws.send('...')

除此之外,HMR 客戶端還初始化了一些處理 HMR 所需的狀態,并導出了幾個 API,例如 createHotContext(),供使用 HMR API 的模塊使用。例如:

app.jsx

// 由 Vite 的導入分析插件注入
import { createHotContext } from '/@vite/client'
import.meta.hot = createHotContext('/src/app.jsx')

export default function App() {
  return <div>Hello Front789</div>
}

// 由 `@vitejs/plugin-react` 注入
if (import.meta.hot) {
  // ...
}

傳遞給 createHotContext() 的 URL 字符串(也稱為 owner 路徑)有助于「標識哪個模塊能夠接受更改」。在createHotContext 將注冊的 HMR 回調分配單例類型,而該類型用于存儲owner 路徑到接受回調、處理回調和修剪回調之間的關聯信息。const ownerPathToAcceptCallbacks = new Map<string, Function[]>()

這基本上就是模塊如何與 HMR 客戶端交互并執行 HMR 更改的方式。

處理來自服務器的信息

建立 WebSocket 連接后,我們可以開始處理來自 Vite 開發服務器的信息。

/@vite/client

ws.addEventListener('message', ({ data }) => {
  const payload = JSON.parse(data)
  switch (payload.type) {
    case 'full-reload': {
      location.reload()
      break
    }
    case 'update': {
      const updates = payload.updates
      // => { type: string, path: string, acceptedPath: string, timestamp: number }[]
      for (const update of updates) {
        handleUpdate(update)
      }
      break
    }
    case 'prune': {
      handlePrune(payload.paths)
      break
    }
    // 處理其他載荷類型...
  }
})

上面的示例處理了 HMR 傳播的結果,根據 full-reload 和 update 信息類型觸發完整頁面重新加載或 HMR 更新。當模塊不再使用時,它還處理修剪。

還有更多類型的信息類型需要處理

  • connected:當建立 WebSocket 連接時發送。
  • error:當服務器端出現錯誤時發送,Vite 可以在瀏覽器中顯示錯誤覆蓋層。
  • custom:由 Vite 插件發送,通知客戶端任何事件。對于客戶端和服務器之間的通信非常有用。

接下來,讓我們看看 HMR 更新實際上是如何工作的。

HMR 更新

HMR 傳播期間找到的每個 HMR 邊界通常對應一個 HMR 更新。

在 Vite 中,更新采用這種簽名:

interface Update {
  // 更新的類型
  type: 'js-update' | 'css-update'
  // 接受模塊(HMR 邊界根)的 URL 路徑
  path: string
  // 被接受的 URL 路徑(通常與上面的路徑相同)
  acceptedPath: string
  // 更新發生的時間戳
  timestamp: number
}

在 Vite 中,它被區分為 JS 更新 或 CSS 更新,其中 CSS 更新被特別處理為在更新時簡單地交換 HTML 中的鏈接標簽。

對于 JS 更新,我們需要找到相應的模塊,以調用其 import.meta.hot.accept() 回調,以便它可以對自身應用 HMR。由于在 createHotContext() 中我們已經「將路徑注冊為第一個參數」,因此我們可以通過更新的路徑輕松找到匹配的模塊。有了更新的時間戳,我們還可以獲取模塊的新版本以傳遞給 import.meta.hot.accept()。

下面是一個實現的示例:

/@vite/client

// 由 `createHotContext()` 填充的映射
const ownerPathToAcceptCallbacks = new Map<string, Function[]>()

async function handleUpdate(update: Update) {
  const acceptCbs = ownerPathToAcceptCallbacks.get(update.path)
  const newModule = await import(`${update.acceptedPath}?t=${update.timestamp}`)

  for (const cb of acceptCbs) {
    cb(newModule)
  }
}

之前我們就介紹過,import.meta.hot.accept() 有兩個函數簽名

  1. import.meta.hot.accept(cb: Function)
  2. import.meta.hot.accept(deps: string | string[], cb: Function)

上面的實現對于第一個函數簽名(自接受模塊)的情況處理良好,但對于第二個函數簽名則不適用。第二個函數簽名的「回調函數只有在依賴項發生更改時才需要被調用」。為了解決這個問題,我們可以將每個回調函數綁定到一組依賴項。

app.jsx

import { add } from './utils.js'
import { value } from './stuff.js'

if (import.meta.hot) {
  import.meta.hot.accept(...)
  // { deps: ['/src/app.jsx'], fn: ... }

  import.meta.hot.accept('./utils.js', ...)
  // { deps: ['/src/utils.js'], fn: ... }

  import.meta.hot.accept(['./stuff.js'], ...)
  // { deps: ['/src/stuff.js'], fn: ... }
}

然后,我們可以使用 acceptedPath 來匹配依賴關系并觸發正確的回調函數。

例如,如果更新了 stuff.js,那么 acceptedPath 將是 /src/stuff.js,而 path 將是 /src/app.jsx。這樣,我們可以通知擁有者路徑接受路徑(acceptedPath)已經更新,而擁有者可以處理其更改。我們可以調整 HMR 處理程序如下:

/@vite/client

// 由 `createHotContext()` 填充的映射
const ownerPathToAcceptCallbacks = new Map<
  string,
  { deps: string[]; fn: Function }[]
>()

async function handleUpdate(update: Update) {
  const acceptCbs = ownerPathToAcceptCallbacks.get(update.path)
  const newModule = await import(`${update.acceptedPath}?t=${update.timestamp}`)

  for (const cb of acceptCbs) {
    // 確保只執行可以處理 `acceptedPath` 的回調函數
    if (cb.deps.some((deps) => deps.includes(update.acceptedPath))) {
      cb.fn(newModule)
    }
  }
}

但我們還沒有完成!在導入新模塊之前,我們還需要確保正確處理舊模塊,使用 import.meta.hot.dispose()。

/@vite/client

// 由 `createHotContext()` 填充的映射
const ownerPathToAcceptCallbacks = new Map<
  string,
  { deps: string[]; fn: Function }[]
>()
const ownerPathToDisposeCallback = new Map<string, Function>()

async function handleUpdate(update: Update) {
  const acceptCbs = ownerPathToAcceptCallbacks.get(update.path)

  // 如果有的話調用 dispose 回調
  ownerPathToDisposeCallbacks.get(update.path)?.()

  const newModule = await import(`${update.acceptedPath}?t=${update.timestamp}`)

  for (const cb of acceptCbs) {
    // 確保只執行可以處理 `acceptedPath` 的回調函數
    if (cb.deps.some((deps) => deps.includes(update.acceptedPath))) {
      cb.fn(newModule)
    }
  }
}

上面的代碼基本上實現了大部分的 HMR 客戶端!

HMR 修剪

我們之前聊過,在 導入分析 階段,Vite 內部處理了 HMR 修剪。當一個模塊不再被任何其他模塊導入時,Vite 開發服務器將向 HMR 客戶端發送一個 { type: 'prune', paths: string[] } 載荷,其中它將獨立地在運行時修剪模塊。

/@vite/client

// 由 `createHotContext()` 填充的映射
const ownerPathToDisposeCallback = new Map<string, Function>()
const ownerPathToPruneCallback = new Map<string, Function>()

function handlePrune(paths: string[]) {
  for (const p of paths) {
    ownerPathToDisposeCallbacks.get(p)?.()
    ownerPathToPruneCallback.get(p)?.()
  }
}

HMR 作廢

與其他 HMR API 不同,import.meta.hot.invalidate() 是可以在 import.meta.hot.accept() 中調用的動作,以退出 HMR。在 /@vite/client 中,只需發送一個 WebSocket 消息到 Vite 開發服務器:

/@vite/client

// `ownerPath` 來自于 `createHotContext()`
function handleInvalidate(ownerPath: string) {
  ws.send(
    JSON.stringify({
      type: 'custom',
      event: 'vite:invalidate',
      data: { path: ownerPath }
    })
  )
}

當 Vite 服務器接收到此消息時,它將從其導入者再次執行 HMR 傳播,結果(完整重新加載或 HMR 更新)將發送回 HMR 客戶端。

HMR 事件

雖然不是 HMR 必需的,但 HMR 客戶端還可以在運行時發出事件,當收到特定信息時。import.meta.hot.on 和 import.meta.hot.off 可以用于監聽和取消監聽這些事件。

if (import.meta.hot) {
  import.meta.hot.on('vite:invalidate', () => {
    // ...
  })
}

發出和跟蹤這些事件與上面處理 HMR 回調的方式非常相似。

/@vite/client(URL)

+ const eventNameToCallbacks = new Map<string, Set<Function>>()

// `ownerPath` 來自于 `createHotContext()`
function handleInvalidate(ownerPath: string) {
+  eventNameToCallbacks.get('vite:invalidate')?.forEach((cb) => cb())
  ws.send(
    JSON.stringify({
      type: 'custom',
      event: 'vite:invalidate',
      data: { path: ownerPath }
    })
  )
}

HMR 數據

最后,HMR 客戶端還提供了一種存儲數據以在 HMR API 之間共享的方法,即 import.meta.hot.data。這些數據也可以傳遞給 import.meta.hot.dispose() 和 import.meta.hot.prune() 的 HMR 回調函數。

保留數據也與我們跟蹤 HMR 回調的方式類似。

以 HMR 修剪代碼為例:

/@vite/client

// 由 `createHotContext()` 填充的映射
const ownerPathToDisposeCallback = new Map<string, Function>()
const ownerPathToPruneCallback = new Map<string, Function>()
+ const ownerPathToData = new Map<string, Record<string, any>>()

function handlePrune(paths: string[]) {
  for (const p of paths) {
+    const data = ownerPathToData.get(p)
+    ownerPathToDisposeCallbacks.get(p)?.(data)
+    ownerPathToPruneCallback.get(p)?.(data)
  }
}

責任編輯:武曉燕 來源: 前端柒八九
相關推薦

2024-07-18 00:05:58

Vite代碼前端

2020-12-10 08:44:35

WebSocket輪詢Comet

2022-03-24 13:36:18

Java悲觀鎖樂觀鎖

2021-04-21 14:19:52

javaignalHandle接口

2021-06-15 07:20:47

Webpack 機制HMR

2019-02-20 14:16:43

2020-02-10 14:26:10

GitHub代碼倉庫

2024-04-11 12:19:01

Rust數據類型

2018-06-05 17:40:36

人工智能語音識別

2020-07-23 07:26:49

JVM類加載器

2022-03-07 06:34:22

CQRS數據庫數據模型

2024-01-03 08:59:52

2020-03-01 17:53:38

Excel大數據微軟

2023-11-18 09:09:08

GNUBSD協議

2024-02-28 18:22:13

AI處理器

2018-07-17 14:42:50

2024-07-01 00:00:04

ViteUMD瀏覽器

2022-11-02 08:12:47

TurbopackVite

2023-10-23 10:20:25

2021-01-21 10:23:43

數據庫架構技術
點贊
收藏

51CTO技術棧公眾號

日本视频一区在线观看| 欧美日韩福利在线观看| 艹b视频在线观看| 蜜芽在线免费观看| 国产精品91一区二区| 欧美精品精品精品精品免费| 偷偷色噜狠狠狠狠的777米奇| 欧美性xxx| 亚洲日韩欧美一区二区在线| 国模精品一区二区三区| 懂色av蜜臀av粉嫩av分享吧最新章节| 99热国内精品| 亚洲精品福利在线| 自拍偷拍一区二区三区四区| 国产丝袜在线观看视频| 免费一级毛片在线观看| 国内在线观看一区二区三区| 日韩精品欧美国产精品忘忧草 | 日韩毛片在线一区二区毛片| 蜜臀av性久久久久蜜臀av麻豆 | 亚洲第一在线综合在线| 成人av无码一区二区三区| 亚洲一区二区动漫| 久久亚洲精品小早川怜子66| 中国极品少妇videossexhd| 成人一区视频| 黑人巨大精品欧美一区二区免费 | 日本不卡不卡| 久久久亚洲午夜电影| 91色精品视频在线| 国产suv精品一区二区33| 国产精品videosex极品| 日韩在线观看av| 在哪里可以看毛片| 红杏视频成人| 欧美成人一区二区三区片免费| 久久久久久久久久久久久久国产| 色呦呦在线观看视频| 国产精品热久久久久夜色精品三区| 国产在线一区二区三区四区| 国产麻豆免费视频| 久草在线在线精品观看| 国产精品第七影院| 99精品人妻国产毛片| 日韩亚洲在线| 久久久久久国产免费| 欧美日韩在线视频免费| 国产精品麻豆久久| 色妞欧美日韩在线| 91成人精品一区二区| 神马电影久久| 国产午夜精品全部视频在线播放| 日韩网站在线播放| 日韩xxx高潮hd| 日本一区免费网站| 在线亚洲一区观看| 国产无套内射久久久国产| 国产精品电影| 精品动漫一区二区| 精品久久一二三| rebdb初裸写真在线观看| 亚洲资源中文字幕| www.xxx麻豆| 不卡av免费观看| 性久久久久久久久| 亚洲熟妇无码一区二区三区导航| 黄网站在线观| 亚洲成人动漫精品| 亚洲 高清 成人 动漫| 碰碰在线视频| 欧美午夜丰满在线18影院| 欧美韩国日本在线| 91国拍精品国产粉嫩亚洲一区| 91福利在线观看| 亚洲aⅴ日韩av电影在线观看| 精品亚洲视频在线| 成人福利片在线| 欧美精品一级二级| 亚洲午夜精品在线观看| 北条麻妃一区二区三区在线观看| 亚洲护士老师的毛茸茸最新章节| 亚洲中文字幕一区| 欧美三级三级| 久久躁日日躁aaaaxxxx| 精品少妇theporn| 亚洲欧美日本国产专区一区| 国产精品久久久久久av福利| 一二三四区视频| 国产精品自拍三区| 久久国产日韩欧美| av资源在线观看免费高清| 国产精品久久久99| 久久精品无码中文字幕| 色是在线视频| 欧美精品视频www在线观看| 激情成人在线观看| 天天躁日日躁狠狠躁欧美巨大小说 | 久久久久久免费网| 在线观看成人av| 中文字幕12页| 国产成人精品亚洲日本在线观看| 欧美日韩一区中文字幕| 中文字幕99页| 精品国产精品国产偷麻豆| 美日韩精品视频免费看| 国产微拍精品一区| 久久国产三级精品| 精品乱码一区| 国产在线看片| 欧美日韩中文字幕| 香蕉视频xxxx| 国产探花在线精品| 欧美精品电影免费在线观看| 波多野结衣小视频| 成人免费视频网站在线观看| 视频在线99re| av2020不卡| 欧美精品v国产精品v日韩精品| 少妇一级淫片免费放播放| 91精品国产成人观看| 欧美中文字幕在线观看| 国产a级免费视频| 国产色产综合色产在线视频 | 国产sm调教视频| 好看的av在线不卡观看| 国产精品看片资源| 午夜视频免费在线| 一区二区三区四区乱视频| 黄色成人免费看| 狼人精品一区二区三区在线| 久久精品青青大伊人av| 久久精品视频2| 成人免费毛片嘿嘿连载视频| 中文字幕一区二区三区四区五区六区 | 人妻熟妇乱又伦精品视频| 精品国产亚洲一区二区三区| 亚洲丝袜一区在线| 久久精品视频1| www.欧美.com| 欧美在线观看黄| 91麻豆精品国产综合久久久 | 久久中文字幕在线观看| av大大超碰在线| 成人精品国产免费网站| 国产成人三级视频| 美女色狠狠久久| 国产一区二区三区欧美| 天堂在线免费观看视频| 99国产精品一区| 免费av观看网址| 噜噜噜天天躁狠狠躁夜夜精品| 九九热精品视频国产| va视频在线观看| 一区二区三区在线免费观看| 九色91porny| 在线看片不卡| 5566中文字幕一区二区| fc2ppv国产精品久久| 91精品啪在线观看国产60岁| 国产中文字幕久久| 久久99久久99小草精品免视看| 亚洲一区二区精品在线| 免费在线成人激情电影| 在线视频亚洲欧美| 一级片一区二区三区| 亚洲欧美在线视频观看| 国产黄色一区二区三区| 国内精品久久久久久久97牛牛| 超碰97在线资源| av在线加勒比| 亚洲精品视频在线播放| 波多野结衣午夜| 中文字幕亚洲综合久久菠萝蜜| 在线一区二区不卡| 欧美日本国产| 精品欧美一区二区久久久伦| 另类图片综合电影| 中文字幕亚洲字幕| 国产高清不卡视频| 午夜av电影一区| 精品人妻一区二区三区蜜桃视频| 蜜桃视频第一区免费观看| 亚洲精品720p| 男女做暖暖视频| 成人网在线播放| 国模吧无码一区二区三区 | 午夜欧美一区二区三区免费观看| 欧美日韩免费电影| 欧美激情视频播放| 精品资源在线看| 91麻豆精品国产91久久久使用方法| 福利所第一导航| 久久夜色精品国产欧美乱极品| 激情 小说 亚洲 图片: 伦| 一区二区在线影院| 久久精品国产第一区二区三区最新章节 | 91精品国产91久久综合| 国产精品jizz视频| 99re久久| 久久都是精品| 精品无人乱码一区二区三区的优势| 久九九久频精品短视频| 久久艳片www.17c.com| 天堂网在线观看视频| 欧美午夜影院一区| 精品一级少妇久久久久久久| 久久久99精品免费观看不卡| 欧美日韩久久婷婷| 久久久精品日韩| www.69av| 成人综合一区| 九九九热999| 国产精品视频一区视频二区| 欧美亚洲视频在线观看| 黄色网在线免费观看| 亚洲欧美日韩一区在线| 亚洲第一视频在线播放| 欧美制服丝袜第一页| 亚洲国产精品午夜在线观看| 国产精品高潮久久久久无| 一级性生活毛片| 成人ar影院免费观看视频| 天天操夜夜操很很操| 日韩**一区毛片| 99999精品视频| 精品不卡视频| 黄色污污在线观看| 日韩影院二区| 日韩一区二区三区高清| 狠狠久久伊人| 国产美女99p| 精品伊人久久| 国产精品午夜一区二区欲梦| 高清不卡av| 欧美性视频精品| av女在线播放| 久久久久国产精品免费| v天堂福利视频在线观看| 日韩中文理论片| av在线播放av| 中文字幕欧美视频在线| 黄色av免费在线观看| 日韩不卡一区| 日本精品一区二区三区高清 久久| 国产香蕉精品| 国产一区二区在线网站| 粉嫩精品导航导航| 国产精品久久久久久久久婷婷| 欧美中文高清| av噜噜色噜噜久久| 视频一区中文字幕精品| 亚洲自拍av在线| 秋霞一区二区三区| 国产99在线播放| 菁菁伊人国产精品| 精品乱码一区二区三区| 亚洲精品蜜桃乱晃| 欧美一区2区三区4区公司二百 | 亚洲国产精品女人久久久| 成人激情四射网| 精品国产人成亚洲区| 天天射,天天干| 亚洲性无码av在线| 欧美成人hd| 欧美激情视频在线观看| 丁香影院在线| 日本欧美国产在线| 99re久久| 成人动漫在线视频| 日韩伦理一区二区三区| 五月天丁香综合久久国产| 99re6这里只有精品| 日韩人妻一区二区三区蜜桃视频| 综合国产在线| 国产淫片免费看| 蜜桃一区二区三区在线| 国产精品无码自拍| 2024国产精品| 欧美一级片在线视频| 亚洲国产三级在线| 波多野结衣视频在线看| 日韩欧美色综合| 久久综合九色综合久| 久久精品电影网站| 999精品网| 国产欧美欧洲在线观看| 中文字幕久久精品一区二区 | 88国产精品视频一区二区三区| 国产乱子伦精品无码专区| 丝袜美腿亚洲色图| 亚洲av无码久久精品色欲| 久久人人97超碰com| 爱爱视频免费在线观看| 色综合天天做天天爱| a网站在线观看| 亚洲欧美日韩中文在线| 国产在线69| 91精品国产高清久久久久久久久 | 黄色片一区二区| 欧美国产禁国产网站cc| 少妇性l交大片| 久久99热狠狠色一区二区| 亚洲国产精品自拍视频| 日本一区二区在线不卡| 中文字幕av久久爽av| 在线免费av一区| 国产青青草视频| 亚洲人成自拍网站| huan性巨大欧美| 日本成人免费在线| 成人高潮a毛片免费观看网站| 蜜桃成人在线| 香蕉久久网站| 亚洲不卡视频在线| 国产91富婆露脸刺激对白| 欧美乱大交做爰xxxⅹ小说| 亚洲一区二区黄色| 中文字幕人妻一区二区在线视频 | 久久久久亚洲av无码专区体验| 亚洲h在线观看| 国产精品国产三级国产普通话对白| 日韩女优av电影| 国产小视频免费在线观看| 欧美极度另类性三渗透| 日韩成人av电影| 成人av免费电影| 91综合网人人| 亚洲高清免费在线观看| 成人综合婷婷国产精品久久| 午夜理伦三级做爰电影| 午夜视频一区二区三区| 91中文字幕在线播放| 中文字幕国产亚洲2019| 色是在线视频| 国产99在线免费| 欧美另类综合| 九九九九九国产| 国产精品人人做人人爽人人添| 国产成人在线视频观看| 日韩一区二区三区电影在线观看| 亚洲视频tv| 国产成人精品在线观看| 九九视频精品全部免费播放| 日韩人妻无码精品久久久不卡| 狠狠色伊人亚洲综合成人| 精品视频第一页| 在线观看成人小视频| 三级视频在线看| 欧美激情一区二区三区在线视频观看| 国产原创一区| 在线码字幕一区| 麻豆91在线观看| 中文字幕av网址| 欧美视频三区在线播放| 毛片在线播放网址| 国产精品18久久久久久首页狼| 女同一区二区三区| 久久av高潮av| 成人国产精品视频| 久久免费在线观看视频| 精品国产伦一区二区三区观看方式| 五月婷婷六月香| 久久久久久夜| av无码一区二区三区| 亚洲妇女屁股眼交7| 亚洲av成人精品一区二区三区在线播放| 欧美激情视频在线观看| 中文字幕日韩高清在线| 狠狠97人人婷婷五月| 97国产一区二区| 一级久久久久久| 一区二区三区 在线观看视| 成人国产综合| 一区二区不卡在线| 狠狠狠色丁香婷婷综合久久五月| 久久国产露脸精品国产| 精品精品国产高清一毛片一天堂| 极品视频在线| 欧美日韩国产一二| 老司机精品视频网站| 国产极品美女在线| 精品美女在线播放| 日韩国产激情| 亚洲一区二区三区免费看| 国产精品性做久久久久久| 青青操国产视频| 亚洲精品一区二区三区四区高清 | www.色.com| 亚洲一级不卡视频| 成人福利在线| 91久久精品国产91久久| 亚洲少妇一区| 日本猛少妇色xxxxx免费网站| 欧美嫩在线观看| 国产桃色电影在线播放| 欧美国产二区| 国产高清亚洲一区| 国产精品suv一区二区三区| 日韩中文字幕视频在线| 中文字幕亚洲在线观看|