Pre –

After a few introductory articles, let’s take a look at the hot update principle of Vite

The process shown above will be analyzed in detail

demo

Suppose the main.ts file looks like this

import a from './a'
console.log(a)
if(import.meta.hot){
    import.meta.hot.accept('./a'.a= > {
        console.log('hmr', a)
    })
}
Copy the code

Request the main ts

If import. Meta. Hot. accept is present in main.ts when requesting main.ts. Once compiled by the importAnalysisPlugin, a piece of code is added to this file

import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext(url/* The absolute path to the current file from the project root */);
Copy the code

When the client executes main.ts, createHotContext of /@vite/client is called

Listen for messages returned by the server

/@vite/client/SRC /client/client.ts

const socketProtocol =
  __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
// Create a WebSocket object
const socket = new WebSocket(`${socketProtocol}: / /${socketHost}`.'vite-hmr')
const base = __BASE__ || '/'

// Listen for messages
socket.addEventListener('message'.async ({ data }) => {})
Copy the code

First, a WebSocket object is created and registered to listen for events, meaning that when the server returns information through the WebSocket, it will be caught by this event.

Some variables are also defined, which are as follows

/** ** import.meta.hot.accept * key: current file address * value: {id: file address, callbacks: [{deps: file path array, fn: Defined callback function}]} */
const hotModulesMap = new Map<string, HotModule>()
/** * Store import.meta.hot. Dispose * key: current file address * value: defined callback function */
const disposeMap = new Map<string.(data: any) = > void | Promise<void> > ()/** * store import.meta.hot.prune * key: current file address * value: defined callback function */
const pruneMap = new Map<string.(data: any) = > void | Promise<void> > ()/** * stores the import.meta.hot.data * objects of all requested files to persist between different instances of the same update module. It can be used to pass information from the previous version of a module to the next. * key: current file address * value: is an object, persistent information */
const dataMap = new Map<string.any> ()/** * stores the HMR hook that the requested file listens on * key: event name * value: array of event callback functions */
const customListenersMap = new Map<string, ((data: any) = > void) [] > ()/** * holds the HMR hook * key: current file address * value: is a Map, the key in the Map is the event name, valye is an array of callback functions */
const ctxToListenersMap = new Map<string.Map<string, ((data: any) = > void) [] > > ()export const createHotContext = (ownerPath: string) = > {}
Copy the code

CustomListenersMap and ctxToListenersMap will be introduced at the end of the introduction to custom hooks

Create an import.meta.hot object

Further down, export the createHotContext function

As mentioned above, this function is executed for modules with import.meta.hot.accept, passing in the absolute path to the current file.

import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext(url/* The absolute path to the current file from the project root */);
Copy the code

The createHotContext function is as follows

// ownerPath Specifies the path to the current file
export const createHotContext = (ownerPath: string) = > {
    // If there is no current path in dataMap, add it to dataMap
    if(! dataMap.has(ownerPath)) { dataMap.set(ownerPath, {}) }// when a file is hot updated, a new context is created
    // clear its stale callbacks
    const mod = hotModulesMap.get(ownerPath)
    if (mod) {
        / / clear the cb
        mod.callbacks = []
    }

    // Remove obsolete custom event listeners (as we'll see at the end of the tutorial on custom hooks)
  	// ...

    const newListeners = new Map()
    ctxToListenersMap.set(ownerPath, newListeners)

    function acceptDeps(){}

    const hot = {
        get data() {},
        accept(deps: any, callback? :any) {},
        acceptDeps() {},
        dispose(cb: (data: any) = >void) {},
        prune(cb: (data: any) = >void) {},
        // TODO
        decline() {},
        invalidate() {},
        on: (event: string, cb: (data: any) = >void) = >{},}return hot
}
Copy the code

The main purpose of this function is to define a hot object and return it. The returned object is assigned to the module’s import.meta.hot

To summarize what createHotContext does:

  • Create persistent data objects and add them todataMapIn the
  • emptyhotModulesMapDependency callback for the current path binding inacceptFunction arguments)
  • Clear outdated custom event listeners
  • Create a container for custom event callbacks to the current path and drop thectxToListenersMapIn the

performimport.meta.hot.acceptmethods

Here is the main analysis of the role of the accept method, other methods are relatively simple here will not repeat, you can directly go to the source code.

accept(deps: any, callback? :any) {
    if (typeof deps === 'function'| |! deps) {// self-accept: hot.accept(() => {})
        acceptDeps([ownerPath], ([mod]) = > deps && deps(mod))
    } else if (typeof deps === 'string') {
        // explicit deps
        acceptDeps([deps], ([mod]) = > callback && callback(mod))
    } else if (Array.isArray(deps)) {
        acceptDeps(deps, callback)
    } else {
        throw new Error(`invalid hot.accept() usage.`)}},Copy the code

In the code, the different parameter forms of import.meta.hot.accept are processed differently, respectively

  • hot.accept(() => {})Receive their
  • hot.accept('./a', () => {})Accept an update of a direct dependency
  • hot.accept(['./a', './b'], () => {})Accept updates for multiple direct dependencies

The acceptDeps method is called to accept an array of files and a callback to update the corresponding file

function acceptDeps(deps: string[], callback: HotCallback['fn'] = () => {}) {
    const mod: HotModule = hotModulesMap.get(ownerPath) || {
        id: ownerPath,
        callbacks: [],
    }
    mod.callbacks.push({
        deps,
        fn: callback,
    })
    / / set hotModulesMap
    hotModulesMap.set(ownerPath, mod)
}
Copy the code

The acceptDeps method adds the callbacks to hotModulesMap for file updates, as described above

By the time the work is done, the next step is the file update process

update

Suppose the main.ts code looks like this

import a from './a'
console.log(a)
if(import.meta.hot){
    import.meta.hot.accept('./a'.a= > {
        console.log('hmr', a)
    })
}
Copy the code

When the contents of a.ts are updated, they are caught by the chokidar registered listener

// file: indicates the absolute path of the modified file
watcher.on('change'.async (file) => {
    file = normalizePath(file)
    // Clears the transformResult property of the ModuleNode object corresponding to the modified file
    // This property stores the compiled code content
    moduleGraph.onFileChange(file)
    if(serverConfig.hmr ! = =false) {
        try {
            await handleHMRUpdate(file, server)
        } catch (err) {}
    }
})
Copy the code

When a file modification is captured by a listener registered with Chokidar, the cached source code in the corresponding ModuleNode is cleared based on the path of the modified file. The handleHMRUpdate function is then called to start the hot update process

export async function handleHMRUpdate(
    file: string,
    server: ViteDevServer
) :Promise<any> {
    const { ws, config, moduleGraph } = server
    // Get the relative path of file to the root path
    const shortFile = getShortName(file, config.root)
    True if the current file is a configuration file
    const isConfig = file === config.configFile
    // True if the current filename is a custom plug-in
    const isConfigDependency = config.configFileDependencies.some(
        (name) = > file === path.resolve(name)
    )
    // True for environment variable files
    constisEnv = config.inlineConfig.envFile ! = =false && file.endsWith('.env')
    // Changes to environment variable files, custom plug-ins, and configuration files restart the service
    if (isConfig || isConfigDependency || isEnv) {
        await restartServer(server)
        return
    }


    // /xxx/node_modules/vite/dist/client
    // Reload the page if it is a hot update file used by the client
    if (file.startsWith(normalizedClientDir)) {
        ws.send({
            type: 'full-reload'.path: The '*',})return
    }
    // Get ModuleNode object (Set) from the absolute path of the file
    // is an array, because a single file may map to multiple service modules, such as a Vue single file component
    const mods = moduleGraph.getModulesByFile(file)

    const timestamp = Date.now()
    const hmrContext: HmrContext = {
        file,
        timestamp,
        modules: mods ? [...mods] : [],
        read: () = > readModifiedFile(file),
        server,
    }
    // Call the handleHotUpdate hook functions defined by all plug-ins
    for (const plugin of config.plugins) {
        if (plugin.handleHotUpdate) {
            const filteredModules = await plugin.handleHotUpdate(hmrContext)
            if (filteredModules) {
                hmrContext.modules = filteredModules
                break}}}if(! hmrContext.modules.length) {// Reload the page if it is an HTML file
        if (file.endsWith('.html')) {

            ws.send({
                type: 'full-reload'.path: config.server.middlewareMode
                    ? The '*'
                    : '/' + normalizePath(path.relative(config.root, file)),
            })
        } else {}
        return
    }
    updateModules(shortFile, hmrContext.modules, timestamp, server)
}
Copy the code

The overall process is as follows

  • Configuration file update,.envUpdates, custom plug-ins, or custom file updates that introduce configuration files restart the server
  • Hot update files used by the client update,index.htmlUpdate, reload the page
  • Invoke all plug-ins definedhandleHotUpdateHook function, specific functions for referenceThe document
    • Filter and narrow the list of affected modules to make HMR more accurate.
    • Returns an empty array and performs full custom HMR processing by sending custom events to the client
  • If other files are updated, callupdateModulesfunction

So what does the update ules function do

function updateModules(
    file: string,
    modules: ModuleNode[],
    timestamp: number,
    { config, ws }: ViteDevServer
) {
    const updates: Update[] = []
    const invalidatedModules = new Set<ModuleNode>()
    // Whether to reload
    let needFullReload = false

    // Traverses all files after the file is compiled
    for (const mod of modules) {}

    if (needFullReload) {
        ws.send({
            type: 'full-reload'})},else {
        ws.send({
            type: 'update',
            updates,
        })
    }
}
Copy the code

First, define an Updates array to store the module to be updated. Define a needFullReload variable that, if true, reloads the entire page. It then iterates through the modules passed in. Finally, determine whether to reload the entire page or update some modules based on needFullReload.

It can be seen that the role of the loop is to get the entire update link based on the modify module.

// Traverses all files after the file is compiled
for (const mod of modules) {
    // Look up the reference path, set the timestamp, and clear the transformResult
    invalidate(mod, timestamp, invalidatedModules)
    // If you need to reload, there is no need to iterate over the others
    if (needFullReload) {
        continue
    }

    const boundaries = new SetThe < {boundary: ModuleNode
        acceptedVia: ModuleNode
    }>()
    // Find the reference module to determine whether the page needs to be reloaded
    const hasDeadEnd = propagateUpdate(mod, boundaries)
    if (hasDeadEnd) {
        // If hasDeadEnd is true, all updates are made.
        needFullReload = true
        continue} updates.push( ... [...boundaries].map(({ boundary, acceptedVia }) = > ({
            type: `${boundary.type}-update` as Update['type'].// Update type
            timestamp, / / timestamp
            path: boundary.url, // The file that depends on the file
            acceptedPath: acceptedVia.url, // Current file}})))Copy the code

First call invalidate, which looks up layer by layer, modifies lastHMRTimestamp and clears transformResult. If the upper module does not accept the hot update of the current module; The invalidate call continues, passing in the corresponding ModuleNode object for the upper module. Check whether the upper layer accepts the hot update of the upper layer module. Modify lastHMRTimestamp and transformResult of the ModuleNode corresponding to the upper module. If the upper echelons don’t accept, look up.

The purpose of this is that, for upper modules, if there is no listening for submodule updates, when the submodule updates, the upper module also needs to reload. You need to update the timestamp and flush the cached code to prevent the cached code from being returned again.

If you listen for submodule updates, you do not need to update itself, but you can re-execute the submodule export through the listening callback. So there’s no need to update the timestamp and clean up the code.

function invalidate(mod: ModuleNode, timestamp: number, seen: Set<ModuleNode>) {
    // Prevent dead loops
    if (seen.has(mod)) {
        return
    }
    seen.add(mod)
    // Set the modification time
    mod.lastHMRTimestamp = timestamp
    mod.transformResult = null
    // Iterate over all the files that import the file
    mod.importers.forEach((importer) = > {
        If the acceptedHmrDeps of the upper-layer file does not contain the current file, the upper-layer file does not define a callback to accept updates to the current file
        // The invalidate method is called again on the upper file
        if(! importer.acceptedHmrDeps.has(mod)) { invalidate(importer, timestamp, seen) } }) }Copy the code

Once you’ve done that, go down and define a boundaries variable and call the propagateUpdate function with the ModuleNode object and boundaries variables for the current module

function propagateUpdate(
    node: ModuleNode,
    boundaries: Set<{
        boundary: ModuleNode
        acceptedVia: ModuleNode
    }>,
    currentChain: ModuleNode[] = [node]
) :boolean/ *hasDeadEnd* /{
    // If you are listening on yourself, add to boundaries and return false
    if (node.isSelfAccepting) {
        boundaries.add({
            boundary: node,
            acceptedVia: node,
        })
        // ...

        return false
    }
    // The current module is not imported by any modules, returns true, that is, all updates
    if(! node.importers.size) {return true
    }
    // The current file is not a CSS file, and only the CSS file imported the current module, returns true
    if(! isCSSRequest(node.url) && [...node.importers].every((i) = > isCSSRequest(i.url))) {
        return true
    }
    // Iterate over all module objects that import this module, looking up
    for (const importer of node.importers) {
        const subChain = currentChain.concat(importer)
        // If the upper module of the current module receives updates from the current module, add to boundaries
        if (importer.acceptedHmrDeps.has(node)) {
            boundaries.add({
                boundary: importer, // Import the module object of the current module
                acceptedVia: node, // Current module
            })
            continue
        }
        // Repeated introductions, if not returned, will cause an infinite loop
        if (currentChain.includes(importer)) {
            return true
        }
        Call propagateUpdate recursively, collect boundaries, and look up
        if (propagateUpdate(importer, boundaries, subChain)) {
            return true}}return false
}
Copy the code

This function gets all the modules to be updated and decides whether to reload the entire page. Move up the import chain until you find the module that received self-updates or submodule updates, add this module to boundaries; And return true, or false otherwise

  • Suppose there are four modules A, B, C, and D, and their reference relationship is A -> B -> C -> D, where module A receives updates from module B. Returns when module D is modifiedfalseAnd collect module A toboundariesIn the
  • Suppose there are four modules A, B, C, and D, and their reference relationship is A -> B -> C -> D, where module C receives updates from module D. Returns when module D is modifiedfalseAnd collect module C toboundariesIn the
  • Suppose there are four modules A, B, C and D, their reference relationship is A -> B -> C -> D, and all modules do not accept hot update. Returns when module D is modifiedtrue.boundariesIs empty

Go back to the updateModules function and merge the collected module array with updates

updates.push( ... [...boundaries].map(({ boundary, acceptedVia }) = > ({
        type: `${boundary.type}-update` as Update['type'].// Update the type js/ CSS
        timestamp, / / timestamp
        path: boundary.url, // Import the module of this module
        acceptedPath: acceptedVia.url, // Current module})))Copy the code

When the loop is complete, the message is sent to the client. Determine the update mode according to needFullReload; If propagateUpdate returns true, the page needs to be reloaded. The reverse is to update the module.

if (needFullReload) {
    ws.send({
        type: 'full-reload'})},else {
    ws.send({
        type: 'update',
        updates,
    })
}
Copy the code

The overall process is as follows

The client receives the message

Take a look at the flow chart

As analyzed earlier, the client registered WebSocket listening

socket.addEventListener('message'.async ({ data }) => {
  handleMessage(JSON.parse(data))
})
Copy the code

When the client receives a message from the server, it calls the handleMessage function

async function handleMessage(payload: HMRPayload) {
    switch (payload.type) {
        case 'connected':
            console.log(`[vite] connected.`)
            setInterval(() = > socket.send('ping'), __HMR_TIMEOUT__)
            break
        case 'update':
            // Call vite:beforeUpdate event callback
            notifyListeners('vite:beforeUpdate', payload)
            // ...

            payload.updates.forEach((update) = > {
                if (update.type === 'js-update') {
                    queueUpdate(fetchUpdate(update))
                } else {/ *... * /}})break
        case 'custom': {
            // ...
            break
        }
        case 'full-reload':
            // Call vite:beforeFullReload callback for the event
            notifyListeners('vite:beforeFullReload', payload)
            if (payload.path && payload.path.endsWith('.html')) {
                // if html file is edited, only reload the page if the browser is currently on that page.
                const pagePath = location.pathname
                const payloadPath = base + payload.path.slice(1)
                if (pagePath === payloadPath || (pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)) {
                    location.reload()
                }
                return
            } else {
                location.reload()
            }
            break
        case 'prune':
            // ...
            break
        case 'error': {/ *... * /}
        default: {/ *... * /}}}Copy the code

HandleMessage follows different logic depending on the type passed by the server, so we’ll only look at the update logic here.

Iterates through the array of modules that need to be updated. If jS-update, execute queueUpdate(fetchUpdate(update)) on the module.

async function fetchUpdate({ path, acceptedPath, timestamp }: Update) {
    // path is the module that receives hot updates
    // mod: {id: file address, callbacks: [{deps: file path array, fn: defined callback function}]}
    const mod = hotModulesMap.get(path)
    if(! mod) {return
    }

    const moduleMap = new Map(a)// Update by itself
    const isSelfUpdate = path === acceptedPath

    // make sure we only import each dep once
    const modulesToUpdate = new Set<string> ()if (isSelfUpdate) {
        // self update - only update self
        modulesToUpdate.add(path)
    } else {
        // Deps stores direct dependencies accepted by the current module
        // This code logic says that if the current module receives a dependency containing the acceptedPath module, the path will be added to the modulesToUpdate
        for (const { deps } of mod.callbacks) {
            deps.forEach((dep) = > {
                if (acceptedPath === dep) {
                    modulesToUpdate.add(dep)
                }
            })
        }
    }

    // Get the qualified callback
    // Filter if elements in mod.callbacks. Deps exist in modulesToUpdate, return true
    const qualifiedCallbacks = mod.callbacks.filter(({ deps }) = > {
        return deps.some((dep) = > modulesToUpdate.has(dep))
    })

    await Promise.all(
        Array.from(modulesToUpdate).map(async (dep) => {
            // Get the callback import.meta.hot.dispose in the side effects of module Settings
            const disposer = disposeMap.get(dep)
            // import.meta.hot.data objects are persisted between different instances of the same update module. It can be used to pass information from the previous version of a module to the next.
            / / call the disposer
            if (disposer) await disposer(dataMap.get(dep))
            const [path, query] = dep.split(`? `)
            try {
                // Concatenate the path to request a new file
                const newMod = await import(
                    base + path.slice(1) + `? import&t=${timestamp}${query ? ` &${query}` : ' '}`
                )
                // The exported contents of the file are added to the moduleMap
                moduleMap.set(dep, newMod)
            } catch (e) {}
        })
    )

    return () = > {
        for (const { deps, fn } of qualifiedCallbacks) {
            // Call the callback defined in import.meta.hot.accept and pass the exported contents of the file into the callback
            fn(deps.map((dep) = > moduleMap.get(dep)))
        }
        const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
        console.log(`[vite] hot updated: ${loggedPath}`)}}Copy the code

The fetchUpdate function collects the module path that needs to be updated and the callback function from import.meta.hot.accept, and calls import.meta.hot. Concatenated update module path, will hang import and timestamp; Load the concatenated path with import(). Finally returns a function that is the callback function in import.meta.hot.accept and prints the update.

The function returned is received by the queueUpdate function; QueueUpdate collects functions returned by fetchUpdate; And triggers all callbacks in the next task queue.

// fetchUpdate calls the callback function defined in import.meta.hot.accept and passes in the exported contents of the request file as arguments
async function queueUpdate(p: Promise"The (() = >void) | undefined>) {
    queued.push(p)
    if(! pending) { pending =true
        await Promise.resolve()
        pending = false
        constloading = [...queued] queued = [] ; (await Promise.all(loading)).forEach((fn) = > fn && fn())
    }
}
Copy the code

conclusion

Vite’s hot update principle can be summarized in the following steps

Pre –

  • When the server starts up, it creates a WebSocket instance and listens for file changes through Chokidar
  • Inject client hot update code to the requested HTML file
  • When loading the client hot update code, create a WebSocket instance and register a listener
  • When the requested file hasimport.meta.hot.acceptIs injected into the fileimport.meta.hot.acceptdefine

update

  • When a file is updated, a callback to the file modification is triggered.
  • If it is a configuration file, custom plug-in,.envFile modification directly restart the server
  • Otherwise, look up according to the module path; Collect the modules that receive the current dependency update and determine whether the page is refreshed
  • If the page is refreshed, send a message to the client to refresh the page, otherwise send an update message, and send the module that receives the update of the current dependency to the client
  • After the client receives it, it retrieves the module path to be updated and the hot update callback passesimport()Request to update the module’s path at the URLHang up theimportAnd time stamp; And triggers a hot update callback on the next task queue.

For example,

Suppose there are four modules A, B, C, and D, and their reference relationship is A -> B -> C -> D

There are modules that accept dependency updates
  • Module A receives update of module B; When modifying module D, this is a partial update, modify the timestamp of module B, C, D and clear the source code cache; Collect module A toboundariesIn the. The server returns A message containing information about module A. After receiving the message, the client looks for the module A that received the hot update. That’s module B. Splice the path of module B and rerequest module B. Vite will hang the import path (module C) in module BtParameter to force the browser to rerequest. After module B returns, it requests module C and also hangs the import path (module D)tParameters. Finally, the hot update callback in module A is called.
  • Module C receives update of module D; When modifying module D, this is a partial update, modify the timestamp of module D and clear the source code cache; Collect module C toboundariesIn the. The server returns a message containing information about module C. After the client receives the message, it looks for module C, the module that received the hot update. That’s module D. Splice the path of module D and rerequest module D. When module D returns, the hot update callback in module C is called.
The module receives self-update
  • Suppose module D receives self-update; When module D is modified, it is also a partial update, and module D itself is collectedboundariesIn the. The server returns a message containing information about module D. After receiving the message, the client looks for module D because it is receiving self-update. Splice the path of module D and rerequest module D. When module D returns, the hot update callback in module D is called.
  • Suppose module A receives self-update; When modifying module D, which is a partial update, modify the timestamp of module B, C and D and clear the source code cache; Collect module A toboundariesIn the. The server returns A message containing information about module A. When the client receives the message, it looks for module A because it is receiving self-update. Splice the path of module A and request module A again. It also changes the path hang of imported modules B, C, and DtParameters. When module A returns, the hot update callback in module A is called.
No module receives hot updates

When module D is modified, since no module receives hot updates, it will directly send the message of page reloading to the client. After receiving the message, the client will directly refresh the page.

Custom hook

Vite HMR has 4 custom hooks that automatically trigger at different times:

  • 'vite:beforeUpdate'When an update is about to be applied (for example, a module will be replaced)
  • 'vite:beforeFullReload'When a full reload is about to occur
  • 'vite:beforePrune'When modules that are no longer needed are about to be culled
  • 'vite:error'When errors occur (for example, syntax errors)

You can also register new hook functions through the handleHotUpdate hook function.

handleHotUpdate({ server }) {
  server.ws.send({
    type: 'custom'.event: 'special-update'.data: {}})return[]}Copy the code

How do I pass a callback

Through the import. Meta. Hot. On

const hot = {
    on: (event: string, cb: (data: any) = >void) = > {
      const addToMap = (map: Map<string.any[] >) = > {
        const existing = map.get(event) || []
        existing.push(cb)
        map.set(event, existing)
      }
      addToMap(customListenersMap)
      addToMap(newListeners)
    }
}
Copy the code

When import.meta.hot.on was executed, the addToMap function was called twice, passing customListenersMap first and newListeners the second time. And put the incoming callback into these two variables.

CustomListenersMap is created by executing the /@vite/client module

/** * stores the HMR hook that the requested file listens on * key: event name * value: array of event callback functions */
const customListenersMap = new Map<string, ((data: any) = > void) [] > ()Copy the code

NewListeners are created by executing createHotContext.

const newListeners = new Map()
ctxToListenersMap.set(ownerPath, newListeners)
Copy the code

NewListeners are ultimately stored in ctxToListenersMap

/** * holds the HMR hook * key: current file address * value: is a Map, the key in the Map is the event name, valye is an array of callback functions */
const ctxToListenersMap = new Map<
  string.Map<string, ((data: any) = > void) [] > > ()Copy the code

The difference between ctxToListenersMap and customListenersMap is:

  • customListenersMapThe stored structure is: event name: Event callback array
  • ctxToListenersMapThe stored structure is: filename: Map< event name, event callback array >

When createHotContext is executed, there is also a piece of code that empties out outdated event callbacks. Because if the current file is requested again, a new Context is created, and the previous Context is useless and needs to be cleared.

// Get all the events that the current module listens for
const staleListeners = ctxToListenersMap.get(ownerPath)
if (staleListeners) {
  // Iterate over the callback that the current module listens for
  for (const [event, staleFns] of staleListeners) {
    // Clear all callbacks in staleFns from customListenersMap
    const listeners = customListenersMap.get(event)
    if (listeners) {
      customListenersMap.set(
        event,
        listeners.filter((l) = >! staleFns.includes(l)) ) } } }Copy the code

Hook call timing

The client calls the handleMessage hook function after listening for a server message.

async function handleMessage(payload: HMRPayload) {
  switch (payload.type) {
      case 'update':
      	notifyListeners('vite:beforeUpdate', payload)
      	// ...
      	break;
      case 'custom': {
        notifyListeners(payload.event as CustomEventName<any>, payload.data)
        break
      }
    case 'full-reload':
      notifyListeners('vite:beforeFullReload', payload)
      break;
  }
Copy the code

As you can see from the source code above, different hook functions are called when the server returns different message types

function notifyListeners(event: string, data: any) :void {
  const cbs = customListenersMap.get(event)
  if (cbs) {
    cbs.forEach((cb) = > cb(data))
  }
}
Copy the code

Get all the callbacks from the customListenersMap based on the event name and execute them