likes
comments
collection
share

vite hmr实现

作者站长头像
站长
· 阅读数 27

上文讲了vite服务端是如何收集依赖的

这次我们讲讲收集依赖之后如何做到热更新

修改文件后,chokidar监听到了文件改动

watcher.on('change', async (file) => {
    file = normalizePath$3(file);
    if (file.endsWith('/package.json')) {
        return invalidatePackageData(packageCache, file);
    }
    // invalidate module graph cache on file change
    moduleGraph.onFileChange(file);
    if (serverConfig.hmr !== false) {
        try {
            await handleHMRUpdate(file, server);
        }
        catch (err) {
            ws.send({
                type: 'error',
                err: prepareError(err),
            });
        }
    }
});

vite hmr实现 监听到了HelloWorld.vue的改变

然后进到moduleGraph.onFileChange(file)

onFileChange(file) {
    const mods = this.getModulesByFile(file);
    if (mods) {
        const seen = new Set();
        mods.forEach((mod) => {
            this.invalidateModule(mod, seen);
        });
    }
}

拿到HelloWorld的mod,进入到invalidateModule

这里有两个mod,一个是HelloWorld.vue,还有一个是它的样式文件

vite hmr实现

之后递归,将HelloWorld.mod.importers的模块都收集进seen之中

vite hmr实现

invalidateModule(mod, seen = new Set(), timestamp = Date.now(), isHmr = false) {
    if (seen.has(mod)) {
        return;
    }
    seen.add(mod);
    if (isHmr) {
        mod.lastHMRTimestamp = timestamp;
    }
    else {
        // Save the timestamp for this invalidation, so we can avoid caching the result of possible already started
        // processing being done for this module
        mod.lastInvalidationTimestamp = timestamp;
    }
    // Don't invalidate mod.info and mod.meta, as they are part of the processing pipeline
    // Invalidating the transform result is enough to ensure this module is re-processed next time it is requested
    mod.transformResult = null;
    mod.ssrTransformResult = null;
    mod.ssrModule = null;
    mod.ssrError = null;
    mod.importers.forEach((importer) => {
        if (!importer.acceptedHmrDeps.has(mod)) {
            this.invalidateModule(importer, seen, timestamp, isHmr);
        }
    });
}

最终seen共有四个mod:HelloWorld.style.mod,HelloWorld.mod,App.mod,main.mod

接着进入handleHMRUpdate

初始化上下文

vite hmr实现

进入handleHotUpdate插件钩子获取处理结果

vite hmr实现

这个钩子把HelloWorld.style.mod给过滤掉了

hmrContext.modules 只有HelloWorld.mod

接着执行updateModules

vite hmr实现

又重新收集了seen,现在去掉了style.mod,只剩下了3个

vite hmr实现

收集边界

vite hmr实现

边界只有HelloWorld.vue,并且判断不需要reload

websocket派发更新

vite hmr实现

首先看客户端的HelloWorld.vue文件

import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/components/HelloWorld.vue");
import { ref } from '/node_modules/.vite/deps/vue.js?v=1a7200b1'


const _sfc_main = {
  __name: 'HelloWorld',
  props: {
  msg: String,
},
  setup(__props, { expose }) {
  expose();



const count = ref(0)

const __returned__ = { count, ref }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}

}
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, createTextVNode as _createTextVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from "/node_modules/.vite/deps/vue.js?v=1a7200b1"

const _withScopeId = n => (_pushScopeId("data-v-e17ea971"),n=n(),_popScopeId(),n)
const _hoisted_1 = { class: "card" }
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("p", null, [
  /*#__PURE__*/_createTextVNode(" Edit "),
  /*#__PURE__*/_createElementVNode("code", null, "components/HelloWorld.vue"),
  /*#__PURE__*/_createTextVNode(" to test HMR ")
], -1 /* HOISTED */))
const _hoisted_3 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("p", null, [
  /*#__PURE__*/_createTextVNode(" Check out "),
  /*#__PURE__*/_createElementVNode("a", {
    href: "https://vuejs.org/guide/quick-start.html#local",
    target: "_blank"
  }, "create-vue"),
  /*#__PURE__*/_createTextVNode(", the official Vue + Vite starter ")
], -1 /* HOISTED */))
const _hoisted_4 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("p", null, [
  /*#__PURE__*/_createTextVNode(" Install "),
  /*#__PURE__*/_createElementVNode("a", {
    href: "https://github.com/johnsoncodehk/volar",
    target: "_blank"
  }, "Volar"),
  /*#__PURE__*/_createTextVNode(" in your IDE for a better DX ")
], -1 /* HOISTED */))
const _hoisted_5 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("p", { class: "read-the-docs" }, "Click on the Vite and Vue logos to learn more", -1 /* HOISTED */))

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("h1", null, _toDisplayString($props.msg), 1 /* TEXT */),
    _createElementVNode("div", _hoisted_1, [
      _createElementVNode("button", {
        type: "button",
        onClick: _cache[0] || (_cache[0] = $event => ($setup.count++))
      }, "count is " + _toDisplayString($setup.count), 1 /* TEXT */),
      _hoisted_2
    ]),
    _hoisted_3,
    _hoisted_4,
    _hoisted_5
  ], 64 /* STABLE_FRAGMENT */))
}

import "/src/components/HelloWorld.vue?vue&type=style&index=0&scoped=e17ea971&lang.css"

_sfc_main.__hmrId = "e17ea971"
typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)
import.meta.hot.accept(mod => {
  if (!mod) return
  const { default: updated, _rerender_only } = mod
  if (_rerender_only) {
    __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)
  } else {
    __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)
  }
})
import _export_sfc from '/@id/__x00__plugin-vue:export-helper'
export default /*#__PURE__*/_export_sfc(_sfc_main, [['render',_sfc_render],['__scopeId',"data-v-e17ea971"],['__file',"c:/Users/mjgao/vite-project/src/components/HelloWorld.vue"]])

注意看,开头注入了createHotContext('/src/components/HelloWorld.vue')

function createHotContext(ownerPath) {
    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) {
        mod.callbacks = [];
    }
    // clear stale custom event listeners
    const staleListeners = ctxToListenersMap.get(ownerPath);
    if (staleListeners) {
        for (const [event, staleFns] of staleListeners) {
            const listeners = customListenersMap.get(event);
            if (listeners) {
                customListenersMap.set(event, listeners.filter((l) => !staleFns.includes(l)));
            }
        }
    }
    const newListeners = new Map();
    ctxToListenersMap.set(ownerPath, newListeners);
    function acceptDeps(deps, callback = () => { }) {
        const mod = hotModulesMap.get(ownerPath) || {
            id: ownerPath,
            callbacks: [],
        };
        mod.callbacks.push({
            deps,
            fn: callback,
        });
        hotModulesMap.set(ownerPath, mod);
    }
    const hot = {
        get data() {
            return dataMap.get(ownerPath);
        },
        accept(deps, callback) {
            if (typeof deps === 'function' || !deps) {
                // self-accept: hot.accept(() => {})
                acceptDeps([ownerPath], ([mod]) => deps === null || deps === void 0 ? void 0 : deps(mod));
            }
            else if (typeof deps === 'string') {
                // explicit deps
                acceptDeps([deps], ([mod]) => callback === null || callback === void 0 ? void 0 : callback(mod));
            }
            else if (Array.isArray(deps)) {
                acceptDeps(deps, callback);
            }
            else {
                throw new Error(`invalid hot.accept() usage.`);
            }
        },
        // export names (first arg) are irrelevant on the client side, they're
        // extracted in the server for propagation
        acceptExports(_, callback) {
            acceptDeps([ownerPath], ([mod]) => callback === null || callback === void 0 ? void 0 : callback(mod));
        },
        dispose(cb) {
            disposeMap.set(ownerPath, cb);
        },
        prune(cb) {
            pruneMap.set(ownerPath, cb);
        },
        // Kept for backward compatibility (#11036)
        // @ts-expect-error untyped
        // eslint-disable-next-line @typescript-eslint/no-empty-function
        decline() { },
        // tell the server to re-perform hmr propagation from this module as root
        invalidate(message) {
            notifyListeners('vite:invalidate', { path: ownerPath, message });
            this.send('vite:invalidate', { path: ownerPath, message });
            console.debug(`[vite] invalidate ${ownerPath}${message ? `: ${message}` : ''}`);
        },
        // custom events
        on(event, cb) {
            const addToMap = (map) => {
                const existing = map.get(event) || [];
                existing.push(cb);
                map.set(event, existing);
            };
            addToMap(customListenersMap);
            addToMap(newListeners);
        },
        send(event, data) {
            messageBuffer.push(JSON.stringify({ type: 'custom', event, data }));
            sendMessageBuffer();
        },
    };
    return hot;
}

回到HelloWorld的热更新代码

import.meta.hot.accept(mod => {
  if (!mod) return
  const { default: updated, _rerender_only } = mod
  if (_rerender_only) {
    __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)
  } else {
    __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)
  }
})

这里进到了createHotContext.accept之中

function acceptDeps(deps, callback = () => { }) {
    const mod = hotModulesMap.get(ownerPath) || {
        id: ownerPath,
        callbacks: [],
    };
    mod.callbacks.push({
        deps,
        fn: callback,
    });
    hotModulesMap.set(ownerPath, mod);
}

hotModulesMap就是从这里收集到的,回调就是HelloWorld的import.meta.hot.accept的回调

客户端收到消息

vite hmr实现

关键是queueUpdate(fetchUpdate(update))

vite hmr实现

async function fetchUpdate({ path, acceptedPath, timestamp, explicitImportRequired, }) {
    const mod = hotModulesMap.get(path);
    if (!mod) {
        // In a code-splitting project,
        // it is common that the hot-updating module is not loaded yet.
        // https://github.com/vitejs/vite/issues/721
        return;
    }
    let fetchedModule;
    const isSelfUpdate = path === acceptedPath;
    // determine the qualified callbacks before we re-import the modules
    const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => deps.includes(acceptedPath));
    if (isSelfUpdate || qualifiedCallbacks.length > 0) {
        const disposer = disposeMap.get(acceptedPath);
        if (disposer)
            await disposer(dataMap.get(acceptedPath));
        const [acceptedPathWithoutQuery, query] = acceptedPath.split(`?`);
        try {
            fetchedModule = await import(
            /* @vite-ignore */
            base +
                acceptedPathWithoutQuery.slice(1) +
                `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${query ? `&${query}` : ''}`);
        }
        catch (e) {
            warnFailedFetch(e, acceptedPath);
        }
    }
    return () => {
        for (const { deps, fn } of qualifiedCallbacks) {
            fn(deps.map((dep) => (dep === acceptedPath ? fetchedModule : undefined)));
        }
        const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`;
        console.debug(`[vite] hot updated: ${loggedPath}`);
    };
}

先从hotModulesMap拿出对应的mod

vite hmr实现

vite hmr实现

动态import

vite hmr实现

发送请求获取最新的改动文件

vite hmr实现

queueUpdate就是一个任务调度,执行回调

async function queueUpdate(p) {
    queued.push(p);
    if (!pending) {
        pending = true;
        await Promise.resolve();
        pending = false;
        const loading = [...queued];
        queued = [];
        (await Promise.all(loading)).forEach((fn) => fn && fn());
    }
}

这里拿到的mod就是请求回来最新的mod

vite hmr实现

然后就是vue里面执行最新mod的更新了

转载自:https://juejin.cn/post/7210596158934073405
评论
请登录