关于前端:vite4源码dev模式整体流程浅析二

本文基于vite 4.3.0-beta.1版本的源码进行剖析

前言

在「vite4源码」dev模式整体流程浅析(一)的文章中,咱们曾经剖析了预构建、申请拦挡以及常见的插件源码,在本文中,咱们将详细分析vite开发模式下的热更新逻辑

5. 热更新HMR

5.1 服务器启动

启动热更新WebSocketServer服务,启动文件监控

  • createWebsocketServer()启动websocket服务
  • 应用chokidar.watch()监听文件变动

当文件变动时,最终触发handleHMRUpdate()办法

async function createServer(inlineConfig = {}) {
    const ws = createWebSocketServer(httpServer, config, httpsOptions);
    const watcher = chokidar.watch(
        [root, ...config.configFileDependencies, path$o.join(config.envDir, '.env*')],
        resolvedWatchOptions);
    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);
        await onHMRUpdate(file, false);
    });
}
const onHMRUpdate = async (file, configOnly) => {
    if (serverConfig.hmr !== false) {
        await handleHMRUpdate(file, server, configOnly);
    }
};

5.2 服务器拦挡浏览器申请而后注入代码

5.2.1 拦挡index.html注入@vite/client.js

在初始化createServer()中,先注册了中间件middlewares.use(indexHtmlMiddleware(server))
在浏览器加载初始化页面index.html时,会触发indexHtmlMiddleware()viteIndexHtmlMiddleware()index.html进行拦挡:

  • 先应用fsp.readFile(filename)读取index.html文件内容
  • 而后应用transformIndexHtml(),也就是createDevHtmlTransformFn()重写index.html文件内容
  • 最终将重写实现的index.html文件返回给浏览器进行加载
async function createServer(inlineConfig = {}) {
    const middlewares = connect();
    const server = {
        ...
    }
    server.transformIndexHtml = createDevHtmlTransformFn(server);
    if (config.appType === 'spa' || config.appType === 'mpa') {
        middlewares.use(indexHtmlMiddleware(server));
    }
    return server;
}
function indexHtmlMiddleware(server) {
    return async function viteIndexHtmlMiddleware(req, res, next) {
        if (res.writableEnded) {
            return next();
        }
        const url = req.url && cleanUrl(req.url);
        if (url?.endsWith('.html') && req.headers['sec-fetch-dest'] !== 'script') {
            const filename = getHtmlFilename(url, server);
            // 读取index.html文件
            let html = await fsp.readFile(filename, 'utf-8');
            // 改写index.html文件
            html = await server.transformIndexHtml(url, html, req.originalUrl);
            // 返回index.html文件
            return send$1(req, res, html, 'html', {
                headers: server.config.server.headers,
            });
        }
        next();
    };
}

改写index.html的办法transformIndexHtml()尽管逻辑非常简单,然而代码十分简短,因而这里不会具体到每一个办法进行剖析

外围逻辑为从resolveHtmlTransforms()中拿到很多hooks,而后应用applyHtmlTransforms()遍历所有hook,依据hook(html,ctx)执行后果,进行数据在index.html的插入(插入到<head>或者插入到<body>),而后返回革新后的index.html

function createDevHtmlTransformFn(server) {
    const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms(server.config.plugins);
    return (url, html, originalUrl) => {
        return applyHtmlTransforms(html, [
            preImportMapHook(server.config),
            ...preHooks,
            htmlEnvHook(server.config),
            devHtmlHook,
            ...normalHooks,
            ...postHooks,
            postImportMapHook(),
        ], {
            path: url,
            filename: getHtmlFilename(url, server),
            server,
            originalUrl,
        });
    };
}
async function applyHtmlTransforms(html, hooks, ctx) {
    for (const hook of hooks) {
        const res = await hook(html, ctx);
        //...省略对res类型的判断逻辑
        html = res.html || html;
        tags = res.tags;
        //..依据类型tags进行数据的组装,判断是要插入<head>还是插入<body>
        html = injectToHead(html, headPrependTags, true);
        html = injectToHead(html, headTags);
        html = injectToBody(html, bodyPrependTags, true);
        html = injectToBody(html, bodyTags);
    }
    return html;
}

咱们通过调试晓得,咱们inject的内容是@vite/client,那么是在哪个办法进行注入的呢?

devHtmlHook()这个hook中,咱们进行html的解决,而后返回数据{html, tags}
其中返回的tags数据中就蕴含了咱们的/@vite/client以及对应要插入的地位和一些属性,最终会触发下面剖析的applyHtmlTransforms()->injectToHead()办法

const devHtmlHook = async (html, { path: htmlPath, filename, server, originalUrl }) => {
    //...
    await traverseHtml(html, filename, (node) => {
        if (!nodeIsElement(node)) {
            return;
        }
        // 解决<script>标签,增加工夫戳?t=xxx,以及触发预加载preTransformRequest()
        // 解决<style>标签,增加到styleUrl数组中
        // 解决其它attrs标签
    });
    await Promise.all(styleUrl.map(async ({ start, end, code }, index) => {
        const url = `${proxyModulePath}?html-proxy&direct&index=${index}.css`;
        // 解决缓存
        const mod = await moduleGraph.ensureEntryFromUrl(url, false);
        ensureWatchedFile(watcher, mod.file, config.root);
        // 转化style数据,触发vite:css插件进行transform()
        const result = await server.pluginContainer.transform(code, mod.id);
        // 重写s字符串
        s.overwrite(start, end, result?.code || '');
    }));
    html = s.toString();
    return {
        html,
        tags: [
            {
                tag: 'script',
                attrs: {
                    type: 'module',
                    src: path$o.posix.join(base, "/@vite/client"),
                },
                injectTo: 'head-prepend',
            },
        ],
    };
};

injectToHead()的具体代码如下所示,实质也是应用正则表达式进行index.html内容的替换,将对应的tagtypesrc增加到指定地位中

function injectToHead(html, tags, prepend = false) {
    if (tags.length === 0)
        return html;
    if (prepend) {
        // inject as the first element of head
        if (headPrependInjectRE.test(html)) {
            return html.replace(headPrependInjectRE, (match, p1) => `${match}\n${serializeTags(tags, incrementIndent(p1))}`);
        }
    }
    else {
        // inject before head close
        if (headInjectRE.test(html)) {
            // respect indentation of head tag
            return html.replace(headInjectRE, (match, p1) => `${serializeTags(tags, incrementIndent(p1))}${match}`);
        }
        // try to inject before the body tag
        if (bodyPrependInjectRE.test(html)) {
            return html.replace(bodyPrependInjectRE, (match, p1) => `${serializeTags(tags, p1)}\n${match}`);
        }
    }
    // if no head tag is present, we prepend the tag for both prepend and append
    return prependInjectFallback(html, tags);
}
function serializeTags(tags, indent = '') {
  if (typeof tags === 'string') {
      return tags;
  }
  else if (tags && tags.length) {
      return tags.map((tag) => `${indent}${serializeTag(tag, indent)}\n`).join('');
  }
  return '';
}

插入/@vite/client后革新的index.html为:

5.2.2 vite:import-analysis插件注入热更新代码

name: 'vite:import-analysis',
async transform(source, importer, options) {
    let imports;
    let exports;
    [imports, exports] = parse$e(source);
    for (let index = 0; index < imports.length; index++) {
        const { s: start, e: end, ss: expStart, se: expEnd, d: dynamicIndex,
            n: specifier, a: assertIndex, } = imports[index];
        // resolvedId="/Users/wcbbcc/blog/Frontend-Articles/vite-debugger/node_modules/.vite/deps/vue.js?v=da0b3f8b"
        // url="/node_modules/.vite/deps/vue.js?v=da0b3f8b"
        const [url, resolvedId] = await normalizeUrl(specifier, start);
        if (!isDynamicImport) {
            // for pre-transforming
            staticImportedUrls.add({ url: hmrUrl, id: resolvedId });
        }
    }
    if (hasHMR && !ssr) {
        // inject hot context
        str().prepend(`import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` +
            `import.meta.hot = __vite__createHotContext(${JSON.stringify(normalizeHmrUrl(importerModule.url))});`);
    }
    if (config.server.preTransformRequests && staticImportedUrls.size) {
        staticImportedUrls.forEach(({ url }) => {
            url = removeImportQuery(url);
            transformRequest(url, server, { ssr }).catch((e) => {
            });
        });
    }
}

比方Index.vue示例代码中注入:

import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/Index.vue");

5.2.3 vite:vue插件注入hot.accept热更新代码

对于每一个.vue文件,都会走vite:vue的插件解析,在对应的transform()转化代码中,会注入对应的import.meta.hot.accept热更新代码

name: 'vite:vue',
async transform(code, id, opt) {
    //...
    if (!query.vue) {
        return transformMain(
            code,
            filename,
            options,
            this,
            ssr,
            customElementFilter(filename)
        );
    } else {
        //...
    }
}
async function transformMain(code, filename, options, pluginContext, ssr, asCustomElement) {
    //...解决<script>、<style>、<template>,而后放入到output中
    const output = [
        scriptCode,
        templateCode,
        stylesCode,
        customBlocksCode
    ];

    if (devServer && devServer.config.server.hmr !== false && !ssr && !isProduction) {
        output.push(`_sfc_main.__hmrId = ${JSON.stringify(descriptor.id)}`);
        output.push(
          `typeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(_sfc_main.__hmrId, _sfc_main)`
        );
        if (prevDescriptor && isOnlyTemplateChanged(prevDescriptor, descriptor)) {
          output.push(`export const _rerender_only = true`);
        }
        output.push(
          `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)`,
          `  }`,
          `})`
        );
      }

      //...
    let resolvedCode = output.join("\n");
    return {
        code: resolvedCode
    };
}

比方Index.vue文件就注入:

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)
  }
})

5.3 @vite/client加载后的执行逻辑

当咱们注入@vite/clientindex.html<script>后,咱们会运行@vite/client代码,而后咱们会执行什么逻辑呢?

建设客户端的WebSocket,增加常见的事件: openmessageclose
当文件产生扭转时,会触发message事件回调,而后触发handleMessage()进行解决

socket = setupWebSocket(socketProtocol, socketHost, fallback);

function setupWebSocket(protocol, hostAndPath, onCloseWithoutOpen) {
    const socket = new WebSocket(`${protocol}://${hostAndPath}`, 'vite-hmr');
    let isOpened = false;
    socket.addEventListener('open', () => {
        isOpened = true;
    }, { once: true });
    // Listen for messages
    socket.addEventListener('message', async ({ data }) => {
        handleMessage(JSON.parse(data));
    });
    return socket;
}

5.4 非index.html注入代码,执行部分热更新操作

而在5.2步骤的剖析中,咱们晓得除了在index.html入口文件注入@vite/client后,
咱们还在其它文件注入了热更新代码,这些热更新代码次要为createHotContext()accept()办法,如下所示,从@vite/client获取裸露进去的接口,而后应用@vite/client这些接口进行部分热更新操作

@vite/client加载后有间接运行的代码,进行WebSocket客户端的创立,同时也提供了一些内部能够应用的接口,能够在不同的文件,比方main.jsIndex.vue中应用@vite/client提供的内部接口进行部分热更新

import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/Index.vue");

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(),在createHotContext()的源码中,咱们应用目前文件门路作为key,获取对应的hot对象
createHotContext()获取hot对象并且赋值给import.meta.hot后,会进行import.meta.hot.accept()的监听,最终触发时会执行acceptDeps()办法,进行以后ownerPathcallbacks收集

accept()收集的callbacks什么时候会被触发呢?在上面5.6.1 fetchUpdate将开展剖析

function createHotContext(ownerPath) {
    function acceptDeps(deps, callback = () => { }) {
        const mod = hotModulesMap.get(ownerPath) || {
            id: ownerPath,
            callbacks: [],
        };
        mod.callbacks.push({
            deps,
            fn: callback,
        });
        hotModulesMap.set(ownerPath, mod);
    }
    const hot = {
        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.`);
            }
        }
    };
    return hot;
}

5.5 文件扭转,服务器解决逻辑

如果扭转的文件是"package.json",触发invalidatePackageData(),将"package.json"缓存在packageCache的数据进行删除,不会触发任何热更新逻辑
如果扭转的不是"package.json",则会触发onHMRUpdate()->handleHMRUpdate()逻辑

function invalidatePackageData(packageCache, pkgPath) {
    packageCache.delete(pkgPath);
    const pkgDir = path$o.dirname(pkgPath);
    packageCache.forEach((pkg, cacheKey) => {
        if (pkg.dir === pkgDir) {
            packageCache.delete(cacheKey);
        }
    });
}
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);
    await onHMRUpdate(file, false);
});
const onHMRUpdate = async (file, configOnly) => {
    if (serverConfig.hmr !== false) {
        await handleHMRUpdate(file, server, configOnly);
    }
};

5.5.1 重启服务|全量更新|部分热更新updateModules

async function handleHMRUpdate(file, server, configOnly) {
    const { ws, config, moduleGraph } = server;
    const shortFile = getShortName(file, config.root);
    const fileName = path$o.basename(file);
    const isConfig = file === config.configFile;
    const isConfigDependency = config.configFileDependencies.some((name) => file === name);
    const isEnv = config.inlineConfig.envFile !== false &&
        (fileName === '.env' || fileName.startsWith('.env.'));
    if (isConfig || isConfigDependency || isEnv) {
        await server.restart();
        return;
    }
    if (configOnly) {
        return;
    }
    //normalizedClientDir="dist/client/client.mjs"
    if (file.startsWith(normalizedClientDir)) {
        ws.send({
            type: 'full-reload',
            path: '*',
        });
        return;
    }
    const mods = moduleGraph.getModulesByFile(file);
    // check if any plugin wants to perform custom HMR handling
    const timestamp = Date.now();
    const hmrContext = {
        file,
        timestamp,
        modules: mods ? [...mods] : [],
        read: () => readModifiedFile(file),
        server,
    };
    for (const hook of config.getSortedPluginHooks('handleHotUpdate')) {
        const filteredModules = await hook(hmrContext);
        if (filteredModules) {
            hmrContext.modules = filteredModules;
        }
    }
    if (!hmrContext.modules.length) {
        // html file cannot be hot updated
        if (file.endsWith('.html')) {
            ws.send({
                type: 'full-reload',
                path: config.server.middlewareMode
                    ? '*'
                    : '/' + normalizePath$3(path$o.relative(config.root, file)),
            });
        }
        return;
    }
    updateModules(shortFile, hmrContext.modules, timestamp, server);
}

什么状况下须要server.restart()isConfigisConfigDependencyisEnv代表什么意思?

  • isConfig代表更改的文件是configFile配置文件
  • isConfigDependency代表更改的文件是configFile配置文件的依赖文件
  • isEnv代表更改的文件是.env.xxx文件,当vite.config.js中配置InlineConfig.envFile=false时,会禁用.env文件

如果是下面三种条件中的文件产生扭转,则间接重启本地服务器

全量更新的条件是什么?

  • (仅限开发)客户端自身不能热更新,满足client/client.mjs就是全量更新的条件须要全量更新
  • 如果没有模块须要更新,并且变动的是.html文件,须要全量更新

当不满足下面两种条件时,有对应的模块变动时,触发updateModules()逻辑

5.5.2 寻找热更新边界

注:acceptedHmrExportsvite 4.3.0-beta.1版本为试验性功能!必须手动配置能力启用!默认不启用!因而个别条件下能够疏忽该逻辑产生的热更新!

updateModules()的代码逻辑看起来是比较简单的

  • 通过propagateUpdate()获取是否须要全量更新的标记位
  • 同时通过propagateUpdate()将更新内容放入到boundaries数据中
  • 最终将boundaries塞入updates数组中
  • ws.send发送updates数据到客户端进行热更新

然而问题来了,propagateUpdate()到底做了什么?什么状况下hasDeadEnd=true?什么状况下hasDeadEnd=false
从热更新的角度来说,都会存在几个常见的问题:

  • 什么类型文件默认开启了热更新?
  • 是否存在不须要热更新的文件或者状况?
  • 一个文件什么状况须要本人更新?
  • vite是否有主动注入一些代码?指定某一个模块作为另一个模块热更新的依赖项?
function updateModules(file, modules, timestamp, { config, ws, moduleGraph }, afterInvalidation) {
    const updates = [];
    const invalidatedModules = new Set();
    let needFullReload = false;
    for (const mod of modules) {
        moduleGraph.invalidateModule(mod, invalidatedModules, timestamp, true);
        if (needFullReload) {
            continue;
        }
        const boundaries = new Set();
        const hasDeadEnd = propagateUpdate(mod, boundaries);
        if (hasDeadEnd) {
            needFullReload = true;
            continue;
        }
        updates.push(...[...boundaries].map(({ boundary, acceptedVia }) => ({
            type: `${boundary.type}-update`,
            timestamp,
            path: normalizeHmrUrl(boundary.url),
            explicitImportRequired: boundary.type === 'js'
                ? isExplicitImportRequired(acceptedVia.url)
                : undefined,
            acceptedPath: normalizeHmrUrl(acceptedVia.url),
        })));
    }
    //...全量更新或者ws.send()
}

在进行propagateUpdate()剖析之前,有几个比拟非凡的变量,咱们须要先剖析下,能力更好了解propagateUpdate()流程

isSelfAccepting解析

isSelfAccepting是什么?isSelfAccepting=true代表什么?

对于vite:css的css文件来说,热更新判断条件如上面代码块所示:

  • 不是CSS modules
  • 没有携带inline字段
  • 没有携带html-proxy字段
const thisModule = moduleGraph.getModuleById(id);
if (thisModule) {
  // CSS modules cannot self-accept since it exports values
  const isSelfAccepting = !modules && !inlineRE.test(id) && !htmlProxyRE.test(id);
}

对于vite:import-analysis,如果存在import.meta.hot.accept(),那么isSelfAccepting=true

name: 'vite:import-analysis',
async transform(source, importer, options) {
    if (!imports.length && !this._addedImports) {
        importerModule.isSelfAccepting = false;
        return source;
    }
    for (let index = 0; index < imports.length; index++) {
        const { s: start, e: end, ss: expStart, se: expEnd, d: dynamicIndex,
            n: specifier, a: assertIndex, } = imports[index];
        const rawUrl = source.slice(start, end);
        // check import.meta usage
        if (rawUrl === 'import.meta') {
            const prop = source.slice(end, end + 4);
            if (prop === '.hot') {
                hasHMR = true;
                const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0);
                if (source.slice(endHot, endHot + 7) === '.accept') {
                    // further analyze accepted modules
                    if (source.slice(endHot, endHot + 14) === '.acceptExports') {
                        lexAcceptedHmrExports(source, source.indexOf('(', endHot + 14) + 1, acceptedExports);
                        isPartiallySelfAccepting = true;
                    }else if (lexAcceptedHmrDeps(source, source.indexOf('(', endHot + 7) + 1, acceptedUrls)) {
                        isSelfAccepting = true;
                    }
                }
            }
            else if (prop === '.env') {
                hasEnv = true;
            }
            continue;
        }
    }
}
产生acceptedHmrExports和importedBindings的起因

在https://github.com/vitejs/vite/discussions/7309 和 https://github.com/vitejs/vite/pull/7324中,咱们能够发现acceptedHmrExportsimportedBindings的相干源码提交记录探讨
源码的提交记录是feat(hmr): experimental.hmrPartialAccept (#7324)

Reactson.jsx文件中,可能存在混合模式,比方上面的代码,export一个组件和一个变量,然而在parent.jsx中只应用Foo这个组件

// son.jsx
export const Foo = () => <div>foo</div>
export const bar = () => 123

在现实状况下,如果咱们扭转bar这个值,那么son.jsx应该触发热更新从新加载!然而parent.jsx不应该热更新从新加载,因为它所应用的Foo并没有产生扭转

// parent.jsx
import { Foo } from './Foo.js'

export const Bar = () => <Foo />

因而须要一个API,在原来的模式:

  • 如果某个文件扭转,无论什么内容,都会触发该文件的accept(()=>{开始更新逻辑})
  • 监听局部import依赖库,当import依赖库产生更新时,会触发该文件的accept(()=>{开始更新逻辑})

还要减少一个监听export {xx}对象触发的热更新,也就是:

export const Bar = ...
export const Baz = ...
export default ...
if (import.meta.hot) {
  import.meta.hot.acceptExports(['default', 'Bar'], newModule => { ... })
}

defaultBar产生扭转时,会触发下面注册的(newModule)=>{开始更新逻辑}办法的执行

importedBindings解析

acceptedHmrExportsimportedBindings配套应用!

node.acceptedHmrExports代表目前文件import.meta.hot.acceptExports监听的模块,比方上面的['default', 'Bar']

export const Bar = ...
export const Baz = ...
export default ...
if (import.meta.hot) {
  import.meta.hot.acceptExports(['default', 'Bar'], newModule => { ... })
}

importer.importedBindings是在vite:import-analysis中解析import语句时,解析该语句是什么类型,而后增加到importedBindings

// parent.jsx
import { Foo } from './Foo.js'

export const Bar = () => <Foo />

如上面代码所示,咱们会传入imports[index]import { Foo } from './Foo.js'importedBindings是一个空的Map数据结构
而后咱们会解析出namespacedImportdefaultImportnamedImports等数据,而后往importedBindings增加对应的字符串,为:

  • bindings.add('*')
  • bindings.add('default')
  • bindings.add(name): import的属性名称,比方"Foo"
if (enablePartialAccept && importedBindings) {
    extractImportedBindings(
        resolvedId,
        source,
        imports[index],
        importedBindings
    )
}

async function extractImportedBindings(
    id: string,
    source: string,
    importSpec: ImportSpecifier,
    importedBindings: Map<string, Set<string>>
) {
    let bindings = importedBindings.get(id)
    if (!bindings) {
        bindings = new Set < string > ()
        importedBindings.set(id, bindings)
    }

    const isDynamic = importSpec.d > -1
    const isMeta = importSpec.d === -2
    if (isDynamic || isMeta) {
        // this basically means the module will be impacted by any change in its dep
        bindings.add('*')
        return
    }

    const exp = source.slice(importSpec.ss, importSpec.se)
    const [match0] = findStaticImports(exp)
    if (!match0) {
        return
    }
    const parsed = parseStaticImport(match0)
    if (!parsed) {
        return
    }
    if (parsed.namespacedImport) {
        bindings.add('*')
    }
    if (parsed.defaultImport) {
        bindings.add('default')
    }
    if (parsed.namedImports) {
        for (const name of Object.keys(parsed.namedImports)) {
            bindings.add(name)
        }
    }
}
acceptedHmrExports和acceptedHmrDeps解析

vite:import-analysis插件中,当咱们剖析文件的import.meta.hot.accept()时,咱们会进行解析source

name: 'vite:import-analysis',
async transform(source, importer, options) {
    for (let index = 0; index < imports.length; index++) {
        // check import.meta usage
        if (rawUrl === 'import.meta') {
            const prop = source.slice(end, end + 4);
            if (prop === '.hot') {
                hasHMR = true;
                const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0);
                if (source.slice(endHot, endHot + 7) === '.accept') {
                    // further analyze accepted modules
                    if (source.slice(endHot, endHot + 14) === '.acceptExports') {
                        lexAcceptedHmrExports(source, source.indexOf('(', endHot + 14) + 1, acceptedExports);
                        isPartiallySelfAccepting = true;
                    } else if (lexAcceptedHmrDeps(source, source.indexOf('(', endHot + 7) + 1, acceptedUrls)) {
                        isSelfAccepting = true;
                    }
                }
            }
            continue;
        }
    }

    for (const { url, start, end } of acceptedUrls) {
        const [normalized] = await moduleGraph.resolveUrl(toAbsoluteUrl(markExplicitImport(url)), ssr);
        normalizedAcceptedUrls.add(normalized);
    }
    await moduleGraph.updateModuleInfo(importerModule, importedUrls, importedBindings, normalizedAcceptedUrls,
        isPartiallySelfAccepting ? acceptedExports : null, isSelfAccepting, ssr);
}

function lexAcceptedHmrDeps(code, start, urls) {
    function addDep(index) {
        urls.add({
            url: currentDep,
            start: index - currentDep.length - 1,
            end: index + 1,
        });
        currentDep = '';
    }
    //...解析code,调用addDep()
}

acceptedHmrDeps

通过调试能够晓得,当咱们应用import.meta.hot.accept(["a", "b"])时,咱们能够失去acceptedUrls=[{url: "a"},{url: "b"}],而后触发updateModuleInfo()传入normalizedAcceptedUrls进行赋值

acceptedHmrExports

通过调试能够晓得,当咱们应用import.meta.hot.acceptExports(["a", "b"])时,咱们能够失去acceptedExports=[{url: "a"},{url: "b"}],而后触发updateModuleInfo()传入acceptedExports进行赋值

acceptedHmrExportsacceptedHmrDeps的数据在updateModuleInfo()办法中进行增加

  • updateModuleInfo()中,通过字符串"a"通过this.ensureEntryFromUrl(accepted)拿到对应的ModuleNode对象,存入到acceptedHmrDeps中,即mod.acceptedHmrDeps.add(this.ensureEntryFromUrl(acceptedModules[i]))
  • mod.acceptedHmrExports=acceptedExports
async updateModuleInfo(mod, importedModules, importedBindings, acceptedModules, acceptedExports, isSelfAccepting, ssr) {
        // update accepted hmr deps
        const deps = (mod.acceptedHmrDeps = new Set());
        for (const accepted of acceptedModules) {
            const dep = typeof accepted === 'string'
                ? await this.ensureEntryFromUrl(accepted, ssr)
                : accepted;
            deps.add(dep);
        }
        // update accepted hmr exports
        mod.acceptedHmrExports = acceptedExports;
        mod.importedBindings = importedBindings;
        return noLongerImported;
}
async ensureEntryFromUrl(rawUrl, ssr, setIsSelfAccepting = true) {
    const [url, resolvedId, meta] = await this.resolveUrl(rawUrl, ssr);
    let mod = this.idToModuleMap.get(resolvedId);
    if (!mod) {
        mod = new ModuleNode(url, setIsSelfAccepting);
        this.urlToModuleMap.set(url, mod);
        mod.id = resolvedId;
        this.idToModuleMap.set(resolvedId, mod);
        const file = (mod.file = cleanUrl(resolvedId));
        let fileMappedModules = this.fileToModulesMap.get(file);
        if (!fileMappedModules) {
            fileMappedModules = new Set();
            this.fileToModulesMap.set(file, fileMappedModules);
        }
        fileMappedModules.add(mod);
    } else if (!this.urlToModuleMap.has(url)) {
        this.urlToModuleMap.set(url, mod);
    }
    return mod;
}
propagateUpdate()详细分析

上面代码为propagateUpdate()的所有代码,咱们能够分为4个局部进行剖析

propagateUpdate()返回true时,阐明无奈找到热更新边界,须要全量更新
propagateUpdate()返回false时,阐明曾经找到热更新边界并且寄存在boundaries

function propagateUpdate(node, boundaries, currentChain = [node]) {
    if (node.id && node.isSelfAccepting === undefined) {
        return false;
    }
    //==========第1局部============
    if (node.isSelfAccepting) {
        boundaries.add({
            boundary: node,
            acceptedVia: node,
        });
        for (const importer of node.importers) {
            if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
                propagateUpdate(importer, boundaries, currentChain.concat(importer));
            }
        }
        return false;
    }
    //==========第2局部============
    if (node.acceptedHmrExports) {
        boundaries.add({
            boundary: node,
            acceptedVia: node,
        });
    } else {
        // 没有文件import目前的node
        if (!node.importers.size) {
            return true;
        }
        // 以后node不是CSS类型,然而CSS文件import目前的node,那么间接全量更新
        if (!isCSSRequest(node.url) &&
            [...node.importers].every((i) => isCSSRequest(i.url))) {
            return true;
        }
    }
    //==========第3局部============
    for (const importer of node.importers) {
        const subChain = currentChain.concat(importer);
        if (importer.acceptedHmrDeps.has(node)) {
            boundaries.add({
                boundary: importer,
                acceptedVia: node,
            });
            continue;
        }
        if (node.id && node.acceptedHmrExports && importer.importedBindings) {
            const importedBindingsFromNode = importer.importedBindings.get(node.id);
            if (importedBindingsFromNode &&
                areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)) {
                continue;
            }
        }
        // 递归调用间接全量更新
        if (currentChain.includes(importer)) {
            // circular deps is considered dead end
            return true;
        }
        // 从node向上寻找,递归调用propagateUpdate收集boundaries
        if (propagateUpdate(importer, boundaries, subChain)) {
            return true;
        }
    }
    return false;
}
第1局部 解决isSelfAccepting

node.isSelfAccepting=true个别产生在.vue.jsx.tsx等响应式组件中,代表该文件变动时会触发外面注册的热更新回调办法,而后执行自定义的更新代码

function propagateUpdate(node, boundaries, currentChain = [node]) {
  if (node.isSelfAccepting) {
      boundaries.add({
          boundary: node,
          acceptedVia: node,
      });
      for (const importer of node.importers) {
          if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
              propagateUpdate(importer, boundaries, currentChain.concat(importer));
          }
      }
      return false;
  }
  //...
}

如果node.isSelfAcceptingtrue,代表它有accept()办法,比方.vue文件中会注入accept()办法,这个时候只有将目前的node退出到boundaries
同时还要判断node.importers是不是CSS申请链接,如果是的话,要持续向上寻找,再次登程propagateUpdate()收集热更新边界boundaries

源码中正文:像Tailwind JIT这样的PostCSS 插件可能会将任何文件注册为CSS文件的依赖项,因而须要检测node.importers是不是CSS申请,本文对这方面不开展具体的剖析,请参考其它文章进行理解

isSelfAccepting=true,最终propagateUpdate()返回false,代表不必全量更新,热更新边界boundaries退出以后的node,完结其它条件语句的执行

第2局部 解决acceptedHmrExports
function propagateUpdate(node, boundaries, currentChain = [node]) {
  //...第1局部
  // 第2局部
  if (node.acceptedHmrExports) {
      boundaries.add({
          boundary: node,
          acceptedVia: node,
      });
  } else {
      // 没有文件import目前的node
      if (!node.importers.size) {
          return true;
      }
      // 以后node不是CSS类型,然而CSS文件import目前的node,那么间接全量更新
      if (!isCSSRequest(node.url) &&
          [...node.importers].every((i) => isCSSRequest(i.url))) {
          return true;
      }
  }
  //...
}
  • node.acceptedHmrExports: 代表目前文件注入了import.meta.hot.acceptExports(xxx)代码,热更新边界boundaries退出以后的node
  • !node.importers.size: 代表没有其它文件import(援用)了目前的node文件,间接全量更新
  • 目前的node文件不是CSS类型,然而其它CSS文件import(援用)了目前的node文件,间接全量更新
第3局部 遍历node.importers
function propagateUpdate(node, boundaries, currentChain = [node]) {
    //...第一局部
    //...第2局部
    // 第3局部
    for (const importer of node.importers) {
        const subChain = currentChain.concat(importer);
        // 逻辑1
        if (importer.acceptedHmrDeps.has(node)) {
            boundaries.add({
                boundary: importer,
                acceptedVia: node,
            });
            continue;
        }
        // 逻辑2
        if (node.id && node.acceptedHmrExports && importer.importedBindings) {
            const importedBindingsFromNode = importer.importedBindings.get(node.id);
            if (importedBindingsFromNode &&
                areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)) {
                continue;
            }
        }
        // 逻辑3
        // 递归调用间接全量更新
        if (currentChain.includes(importer)) {
            // circular deps is considered dead end
            return true;
        }
        // 从node向上寻找,递归调用propagateUpdate收集boundaries
        if (propagateUpdate(importer, boundaries, subChain)) {
            return true;
        }
    }
    //...
}
function areAllImportsAccepted(importedBindings, acceptedExports) {
    for (const binding of importedBindings) {
        if (!acceptedExports.has(binding)) {
            return false;
        }
    }
    return true;
}

从下面的剖析能够晓得

  • acceptedHmrDeps实质就是获取import.meta.hot.accept(xxx)的监听模块
  • acceptedHmrExports实质就是获取import.meta.hot.acceptExports(xxx)的监听模块
  • importedBindings代表目前文件中import的文件的数据

第3局部的代码逻辑次要是遍历以后node.importer,寻找是否须要退出热更新边界boundaries的文件


逻辑1 解决acceptedHmrDeps

如果node.importers[i]注入了import.meta.hot.accept(xxx)的监听模块(如上面代码块所示), 那么热更新边界boundaries退出以后的node.importers[i]

if (importer.acceptedHmrDeps.has(node)) {
    boundaries.add({
        boundary: importer,
        acceptedVia: node,
    });
    continue;
}
// B.js
export const test = "B.js";
// A.js
import {test} from "./B.js";
import.meta.hot.accept("B", (mod)=>{});

逻辑2 解决acceptedHmrExports&importedBindingsFromNode

如上面代码块所示,目前node=B.js,咱们扭转了B.js的内容,触发了热更新
此时importedBindingsFromNode=["test"]acceptedHmrExports=["test"],触发continue,不触发向上寻找热更新边界的逻辑

if (node.id && node.acceptedHmrExports && importer.importedBindings) {
    const importedBindingsFromNode = importer.importedBindings.get(node.id);
    if (importedBindingsFromNode &&
        areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)) {
        continue;
    }
}
function areAllImportsAccepted(importedBindings, acceptedExports) {
  for (const binding of importedBindings) {
      if (!acceptedExports.has(binding)) {
          return false;
      }
  }
  return true;
}

// B.js
const test = "B.js3";
import.meta.hot.acceptExports("test", (mod)=>{
    console.error("B.js热更新触发");
})
const test1 = "B1.js";
export {test, test1}
// A.js
import {test} from "./B.js";
console.info("A.js", test);
export const AExport = "AExport";

那为什么满足areAllImportsAccepted就触发continue呢?意思就是如果目前node文件acceptExports所有export进来的值,就能够不向上解决寻找热更新边界了?

在下面isSelfAccepting的剖析中,咱们能够晓得,acceptExports代表import.meta.hot.acceptExports(xxx)监听的模块数据

exports代表该文件所exports的数据,比方下面示例B.js["test", "test1"]

acceptExports监听的数据曾经齐全覆盖文件所exports的数据时,会强行设置isSelfAccepting=true

name: 'vite:import-analysis',
async transform(source, importer, options) {
    // 当source存在hot.acceptExport字段时,isPartiallySelfAccepting=true
    // 当source存在hot.accept字段时,isSelfAccepting=true
    if (!isSelfAccepting &&
        isPartiallySelfAccepting &&
        acceptedExports.size >= exports.length &&
        exports.every((e) => acceptedExports.has(e.n))) {
        isSelfAccepting = true;
    }
}

isSelfAccepting=true时,当B.js文件发生变化时,就会触发propagateUpdate()的第1局部,热更新边界boundaries退出以后的node,而后间接return false,进行向上解决寻找热更新边界,这样的逻辑也验证了咱们下面的猜测,如果目前node文件曾经acceptExports所有export进来的值,就能够不向上解决寻找热更新边界了

function propagateUpdate(node, boundaries, currentChain = [node]) {
  if (node.isSelfAccepting) {
      boundaries.add({
          boundary: node,
          acceptedVia: node,
      });
      for (const importer of node.importers) {
          if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
              propagateUpdate(importer, boundaries, currentChain.concat(importer));
          }
      }
      return false;
  }
  //...
}

逻辑3 持续向上找热更新的边界

如果存在循环递归的状况,间接返回true,间接全量更新

// 递归调用间接全量更新
if (currentChain.includes(importer)) {
  // circular deps is considered dead end
  return true;
}
// 从node向上寻找,递归调用propagateUpdate收集boundaries
if (propagateUpdate(importer, boundaries, subChain)) {
  return true;
}
propagateUpdate小结

什么状况下才须要向上找热更新的边界?

当初咱们能够依据下面的剖析进行总结:

  • node.isSelfAcceptingfalse,继续执行上面的条件判断
  • importer.acceptedHmrDeps.has(node),即parent有注入accept("A")监听import {A} from "xxx"的值,不持续向上找热更新的边界
  • node.acceptedHmrExportstrue时,间接将以后node退出到热更新边界中

    • 曾经监听所有export进来的值,则不持续向上找热更新的边界
    • 如果没有监听所有export进来的值,则持续向上找热更新的边界propagateUpdate(importer, boundaries)
  • node.acceptedHmrExportsfalse时,持续向上找热更新的边界propagateUpdate(importer, boundaries)
function propagateUpdate(node, boundaries, currentChain = [node]) {
    if (node.id && node.isSelfAccepting === undefined) {
        return false;
    }
    //==========第1局部============
    if (node.isSelfAccepting) {
        boundaries.add({
            boundary: node,
            acceptedVia: node,
        });
        for (const importer of node.importers) {
            if (isCSSRequest(importer.url) && !currentChain.includes(importer)) {
                propagateUpdate(importer, boundaries, currentChain.concat(importer));
            }
        }
        return false;
    }
    //==========第2局部============
    if (node.acceptedHmrExports) {
        boundaries.add({
            boundary: node,
            acceptedVia: node,
        });
    } else {
        // 没有文件import目前的node
        if (!node.importers.size) {
            return true;
        }
        // 以后node不是CSS类型,然而CSS文件import目前的node,那么间接全量更新
        if (!isCSSRequest(node.url) &&
            [...node.importers].every((i) => isCSSRequest(i.url))) {
            return true;
        }
    }
    //==========第3局部============
    for (const importer of node.importers) {
        const subChain = currentChain.concat(importer);
        if (importer.acceptedHmrDeps.has(node)) {
            boundaries.add({
                boundary: importer,
                acceptedVia: node,
            });
            continue;
        }
        if (node.id && node.acceptedHmrExports && importer.importedBindings) {
            const importedBindingsFromNode = importer.importedBindings.get(node.id);
            if (importedBindingsFromNode &&
                areAllImportsAccepted(importedBindingsFromNode, node.acceptedHmrExports)) {
                continue;
            }
        }
        // 递归调用间接全量更新
        if (currentChain.includes(importer)) {
            // circular deps is considered dead end
            return true;
        }
        // 从node向上寻找,递归调用propagateUpdate收集boundaries
        if (propagateUpdate(importer, boundaries, subChain)) {
            return true;
        }
    }
    return false;
}
acceptExports具体示例剖析

通过具体实例明确acceptExports想要达到的成果

B.js中,咱们监听了export数据:test

  • 当咱们扭转test变量时,比方从test="B.js"更改为test="B111.js"时,只会触发B.js热更新,而后触发打印B.js热更新触发
  • 当咱们扭转test1变量,因为B.js中没有监听test1变量,因而会触发B.js热更新 + 向上寻找A.js->向上寻找main.js,最终找到main.js,触发main.js热更新

从这个例子中咱们就能够清晰明确acceptExports的作用,咱们能够监听局部export变量,从而防止过多文件的有效热更新

当监听的acceptExports的字段跟import的字段不一样时,会触发向上寻找热更新边界
当监听的acceptExports的字段跟import的字段一样时,只会触发以后文件的热更新

// main.js
import {AExport} from "./simple/A.js";
import.meta.hot.acceptExports(["aa"]);
// A.js
import {test1} from "./B.js";
console.info("A.js", test1);
export const AExport = "AExport3";
// B.js
const test = "B.js";
import.meta.hot.acceptExports("test", (mod)=>{
  console.error("B.js热更新触发");
})
// 当acceptExports笼罩了所有export数据时,会强行设置isSelfAccepting=true
const test1 = "B432.js";
export {test, test1}

5.5.3 全量更新或者发送热更新模块到客户端

function updateModules(file, modules, timestamp, { config, ws, moduleGraph }, afterInvalidation) {
    for (const mod of modules) {
        //...寻找热更新边界updates,如果找不到,则进行全量更新needFullReload=true
        updates.push(...[...boundaries].map(({ boundary, acceptedVia }) => ({
            type: `${boundary.type}-update`,
            timestamp,
            path: normalizeHmrUrl(boundary.url),
            explicitImportRequired: boundary.type === 'js'
                ? isExplicitImportRequired(acceptedVia.url)
                : undefined,
            acceptedPath: normalizeHmrUrl(acceptedVia.url),
        })));
    }
    if (needFullReload) {
        // 全量更新
        ws.send({
            type: 'full-reload',
        });
        return;
    }
    if (updates.length === 0) {
        // 没有更新,不进行ws.send
        return;
    }
    ws.send({
        type: 'update',
        updates,
    });
}

updates最终的数据结构为:

其中有两个变量须要留神下:pathacceptedPath

  • path: 取的是boundary.url
  • acceptedPath: 取的是acceptedVia.url

在寻找热更新边界propagateUpdate()时,如上面代码所示,咱们晓得

  • node.isSelfAccepting: pathacceptedPath都为node
  • node.acceptedHmrExports: pathacceptedPath都为node
  • importer.acceptedHmrDeps.has(node): pathimporteracceptedPathnode
function propagateUpdate(node, boundaries, currentChain = [node]) {
    //==========第1局部============
    if (node.isSelfAccepting) {
        boundaries.add({
            boundary: node,
            acceptedVia: node,
        });
    }
    //==========第2局部============
    if (node.acceptedHmrExports) {
        boundaries.add({
            boundary: node,
            acceptedVia: node,
        });
    }
    //==========第3局部============
    for (const importer of node.importers) {
        const subChain = currentChain.concat(importer);
        if (importer.acceptedHmrDeps.has(node)) {
            boundaries.add({
                boundary: importer,
                acceptedVia: node,
            });
            continue;
        }

        //...
        if (propagateUpdate(importer, boundaries, subChain)) {
            return true;
        }
    }
    return false;
}

5.6 文件扭转,服务器->客户端触发热更新逻辑

咱们从5.3步骤后晓得,当文件变动,服务器WebSocket->客户端WebSocket后,会触发handleMessage()的执行

如果update.typejs-update,则触发fetchUpdate(update)办法
如果update.type不为js-update,检测是否存在link标签蕴含这个要更新模块的门路,如果存在,则从新加载该文件数据(加载新的link,删除旧的link

Element: after()示意插入新的元素到Elment的前面
Element: remove()示意删除该元素

async function handleMessage(payload) {
    switch (payload.type) {
        case 'update':
            notifyListeners('vite:beforeUpdate', payload);

            await Promise.all(payload.updates.map(async (update) => {
                if (update.type === 'js-update') {
                    return queueUpdate(fetchUpdate(update));
                } else {
                    const el = Array.from(document.querySelectorAll('link')).find((e) => !outdatedLinkTags.has(e) && cleanUrl(e.href).includes(searchUrl));
                    const newPath = `${base}${searchUrl.slice(1)}${searchUrl.includes('?') ? '&' : '?'}t=${timestamp}`;
                    if (!el) {
                        return;
                    }
                    // 应用<link href="门路?t=更新工夫戳">加载文件
                    const newLinkTag = el.cloneNode();
                    newLinkTag.href = new URL(newPath, el.href).href;
                    const removeOldEl = () => {
                        el.remove();
                        console.debug(`[vite] css hot updated: ${searchUrl}`);
                        resolve();
                    };
                    newLinkTag.addEventListener('load', removeOldEl);
                    outdatedLinkTags.add(el);
                    el.after(newLinkTag);
                }

            }));
            notifyListeners('vite:afterUpdate', payload);
            break;
        case 'full-reload':
            notifyListeners('vite:beforeFullReload', payload);
            //...
            location.reload();
            break;
    }
}

5.6.1 fetchUpdate

在寻找热更新边界propagateUpdate()时,咱们晓得

  • node.isSelfAccepting: pathacceptedPath都为node
  • node.acceptedHmrExports: pathacceptedPath都为node
  • importer.acceptedHmrDeps.has(node): pathimporteracceptedPathnode

还有可能触发向上找热更新的边界propagateUpdate(importer, boundaries),此时pathimporteracceptedPathimporter

async function fetchUpdate({ path, acceptedPath, timestamp, explicitImportRequired, }) {
    //依据门路拿到之前收集的依赖更新对象
    const mod = hotModulesMap.get(path);
    const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => deps.includes(acceptedPath));

    // 依据门路从新申请该文件数据
    fetchedModule = await import(
        /* @vite-ignore */
        base +
        acceptedPathWithoutQuery.slice(1) +
        `?${explicitImportRequired ? 'import&' : ''}t=${timestamp}${query ? `&${query}` : ''}`);
    return () => {
        for (const { deps, fn } of qualifiedCallbacks) {
            // 将新申请的数据,应用fn(fetchedModule)进行部分热更新
            fn(deps.map((dep) => (dep === acceptedPath ? fetchedModule : undefined)));
        }
    };
}

如下面代码所示,在fetchUpdate()中,咱们会通过hotModulesMap.get(path)拿到关联的mod

那么hotModulesMap的数据是在哪里初始化的呢?

5.4步骤的非index.html注入代码剖析中,如上面的代码所示,咱们晓得会在文件中进行hot.accept()的调用

  • 当一个.vue文件应用hot.accept()或者hot.accept(()=>{})时,当监听的文件发生变化时上面代码中meta.hot.accept((mod)=>{})mod就是下面fetchUpdate()fetchedModuleconst {default}=fetchedModule也就是申请文件的export default内容
  • 当一个文件应用hot.accept("a")或者hot.accept(["a","b"])时,参数会作为deps存入到mod.callbacks

如下面代码所示,当咱们通过hotModulesMap.get(path)拿到关联的mod,此时的mod对应的path文件所注册的import.meta.hot.accept或者import.meta.hot.acceptExports的回调

而后通过deps.includes(acceptedPath)进行注册回调的筛选,如果hot.accept有显式注册deps,就会依据deps去筛选
如果hot.accept没有显式注册deps,那么此时deps=[ownerPath],即deps=[path]

// .vue文件注入代码
import { createHotContext as __vite__createHotContext } from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/Index.vue");

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)
    }
})
// @vite/client代码
function createHotContext(ownerPath) {
    function acceptDeps(deps, callback = () => { }) {
        const mod = hotModulesMap.get(ownerPath) || {
            id: ownerPath,
            callbacks: [],
        };
        mod.callbacks.push({
            deps,
            fn: callback,
        });
        hotModulesMap.set(ownerPath, mod);
    }
    const hot = {
        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.`);
            }
        },
        acceptExports(_, callback) {
            acceptDeps([ownerPath], ([mod]) => callback === null || callback === void 0 ? void 0 : callback(mod));
        },
    };
    return hot;
}

那为什么acceptExports传入的第一个参数不应用呢?间接初始化为[ownerPath]?

咱们在下面的propagateUpdate()的剖析中,咱们晓得

  • node.isSelfAccepting: pathacceptedPath都为node
  • node.acceptedHmrExports: pathacceptedPath都为node
  • importer.acceptedHmrDeps.has(node): pathimporteracceptedPathnode

还有可能触发向上找热更新的边界propagateUpdate(importer, boundaries),此时pathimporteracceptedPathimporter

这里的node代表以后文件的门路!importer代表import以后node文件的那个文件的门路!


从下面的剖析中,import.meta.hot.accept(xxx)则能够设置pathimporteracceptedPathnode,即能够在import以后node文件的那个文件中解决以后node文件的热更新

acceptedHmrExports存在,即import.meta.hot.acceptExports(xxx)存在时,它监听的都是以后node文件的门路,只能在以后node文件中解决以后node文件的热更新,这跟监听export局部数据触发热更新的初衷是合乎的,因而acceptExports传入的第一个参数不应用,间接初始化为以后node的文件门路[ownerPath]

import.meta.hot.accept(xxx)不仅仅能够监听export还能够监听import


那为什么下面剖析的acceptedHmrExports变量就代表import.meta.hot.acceptExports(["a", "b"])所监听的值,即acceptedHmrExports=[{url: "a"},{url: "b"}]呢?

那是因为为了失去acceptedHmrExports,是间接拿代码去正则表达式获取数据,而不是办法调用,如上面代码所示,是通过lexAcceptedHmrExports()拿到acceptExports(["a", "b"])"a""b"

name: 'vite:import-analysis',
async transform(source, importer, options) {
    for (let index = 0; index < imports.length; index++) {
        // check import.meta usage
        if (rawUrl === 'import.meta') {
            const prop = source.slice(end, end + 4);
            if (prop === '.hot') {
                hasHMR = true;
                const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0);
                if (source.slice(endHot, endHot + 7) === '.accept') {
                    // further analyze accepted modules
                    if (source.slice(endHot, endHot + 14) === '.acceptExports') {
                        lexAcceptedHmrExports(source, source.indexOf('(', endHot + 14) + 1, acceptedExports);
                        isPartiallySelfAccepting = true;
                    } else if (lexAcceptedHmrDeps(source, source.indexOf('(', endHot + 7) + 1, acceptedUrls)) {
                        isSelfAccepting = true;
                    }
                }
            }
            continue;
        }
    }
}

6. 总结

6.1 预构建原理

  1. 遍历所有的文件,收集所有裸模块的申请,而后将所有裸模块的申请作为esbuild打包的入口文件,将所有裸模块缓存打包到.vite/deps文件夹下,在打包过程中,会将commonjs转化为esmodule的模式,实质是应用一个export default包裹着commonjs的代码,同时利用esbuild的打包能力,将多个内置申请合并为一个申请,避免大量申请引起浏览器端的网络梗塞,使页面加载变得十分迟缓
  2. 在浏览器申请链接时改写所有裸模块的门路指向.vite/deps
  3. 如果想要从新执行预构建,应用--force参数或者间接删除node_modeuls/.vite/deps是比拟快捷的形式,或者扭转一些配置的值能够触发从新预构建

6.2 热更新原理

  1. 应用websocket建设客户端和服务端
  2. 服务端会监听文件变动,而后通过一系列逻辑判断,得出热更新的文件范畴,此时的热更新边界的判断依赖于transform文件内容时的剖析,每一个文件都具备一个对象数据ModuleNode
  3. 客户端接管服务端的热更新文件范畴相干的门路后,进行客户端中热更新代码的调用

6.3 vite与webpack的区别

webpack是先解析依赖,打包构建,造成bundle后再启动开发服务器,当咱们批改bundle的其中一个子模块时,咱们须要对这个bundle从新打包而后触发热更新,我的项目越大越简单,启动工夫就越长
vite的外围原理是利用esmodule进行按需加载,先启动开发服务器,而后再进行import模块,无奈进行整体依赖的解析和构建打包,同时应用esbuild疾速的打包速度进行不会轻易扭转node_modules依赖的预构建,晋升速度,当文件产生扭转时,也会发送对应的文件数据到客户端,进行该文件以及相干文件的热更新,不必从新构建和从新打包,我的项目越大晋升成果越显著

6.4 vite比照webpack优缺点

vite长处

  1. 疾速的服务器启动: 当冷启动开发服务器时,基于打包器的形式启动必须优先抓取并构建你的整个利用,而后能力提供服务,Vite 以 原生 ESM 形式提供源码,让浏览器接管了打包程序的局部工作:Vite 只须要在浏览器申请源码时进行转换并按需提供源码,而后依据情景动静导入代码,即只在以后屏幕上理论应用时才会被解决
  2. 反馈疾速的热更新: 基于打包器启动时,重建整个包的效率很低,并且更新速度会随着利用体积增长而直线降落,在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只须要准确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块自身),使得无论利用大小如何,HMR 始终能放弃疾速更新

vite毛病

  1. 首屏加载较慢: 须要大量http申请和源文件转化操作,首次加载还须要破费工夫进行预构建

    尽管vite曾经改良预构建不会影响本地服务的启动和运行,然而一些预构建的库,比方react.js,还是得期待预构建实现后能力加载react.js,而后进行整体渲染

  2. 懒加载较慢: 和首屏加载一样,动静加载的文件依然须要转化操作,可能会存在大量的http申请(多个业务依赖文件)
  3. 开发服务器和生产环境构建之间输入和行为可能不统一: 开发环境应用esbuild,生产环境应用rollup,有一些插件(比方commonjs的转化)须要辨别开发环境和生产环境

6.5 vite如何解决Typescript、SCSS等语言

  • vite:css插件: 调用预处理器依赖库进行转化解决
  • vite:esbuild插件: .ts.tsx转化.js,用来代替传统的tsc转化性能

Vite 应用 esbuild 将 TypeScript 转译到 JavaScript,约是 tsc 速度的 20~30 倍

参考文章

  1. Vite源码剖析,是时候弄清楚Vite的原理了
  2. Vite原理及源码解析
  3. Vite原理剖析

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理