性能利器,经过Vue3深度解析webpack热更新原理

原文发布于BestVue3社区vue

最近在解决 Vue3 的 JSX 不支持热更新的问题,因此较为深度地研究了 Webpack 的热更新的原理,以及应该如何实现 Vue3 的组件热更新, 本文就来深度分析一下关于 Webpack 热更新的原理和实现。须要注意的是热更新不是 Webpack 的专利,其余的打包工具也是有的,而且会有一些区别。 本文主要关注 Webpack。react

我已经给 Vue3 的 babel-jsx 插件提了PR,有兴趣能够看一下我是如何实现的。webpack

参考资料:git

Webpack 的热更新

具体的 webpack 热更新流程以下:github

  • 应用让 HRM 运行时来检查是否具备热更新
  • 运行时同步下载代码而且通知应用
  • 应用告诉运行时须要执行更新
  • 运行时同步地执行更新

咱们在启动 webpack-dev-server 以后,应该都有看到过在浏览器命令行或者 network 区域有一些websocket相关的内容, 曾经我不止一次奇怪为何我好像没有加入任何 websocket 相关代码,哪来的这些提醒呢? 这就是由于 webpack 的热更新的须要。web

在咱们修改了项目代码以后,webpack 会监听到文件内容的变化,而且从新进行编译等工做,而后会把新的代码经过 websocket 发送给浏览器。 浏览器获取到新的代码以后会从新执行模块代码,而且替换模块的内容。须要注意咱们本文不讨论 webpack 如何替换模块内容浏览器

咱们须要在 webpack 的配置中开启热更新,才会让 webpack 可以执行以上的操做,如何开启:微信

{
    devServer: {
        hot: true
    }
}
复制代码

在开启了热更新以后,咱们代码中的module上会有热更新相关的属性,最多见的就是这样的代码:babel

if (module.hot) {
    // 关于如何处理这个模块的代码
    module.hot.accept()
}
复制代码

大部分热更新的插件都会经过这种方式来判断当前是否开启了热更新。websocket

假如咱们有以下的 Vue3 组件文件代码:

export const Comp1 = defineComponent({
    setup() {
        return () => <div>Hello Comp1</div>
    },
})

export const Comp2 = defineComponent({
    setup() {
        return () => <div>Hello World</div>
    },
})
复制代码

咱们通常会使用一些插件来添加热更新的代码,好比咱们这里会用 Vue3 的 babel-jsx 插件,这个插件在编译代码的时候会往这个文件增长相似以下代码:

// ... 组件代码

if (module.hot) {
    module.hot.accept()
}
复制代码

咱们这里执行module.hot.accept()来通知 webpack 的 HMR 咱们接收了这个模块,HMR 并不须要再从新执行模块的替换。 若是咱们执行了这句代码,咱们就须要自行替换这个文件的模块代码,来达到运行的应用更新模块的目的。 若是在这里你没有去执行一些其余更新模块功能的代码,那么这个模块并不会被更新。 而若是你不执行这句代码,那么这个模块的全部代码都会被更新。在这里例子里面,咱们若是把Comp2的内容改成Hello Vue3, 咱们若是不执行module.hot.accept()那么Comp1Comp2都会被从新渲染。

这勉强可以达到热更新的目的,可是追求精益求精的咱们,确定不知会知足于此。

Vue3 的 HMR

Vue3 专门实现了热更新的功能,Vue3 在 window 上会挂载一个__VUE_HMR_RUNTIME__对象,来提供组件从新挂载渲染的功能。 其源码在runtime-core/src/hmr.ts,你们有兴趣能够去看一下实现。

咱们能够经过__VUE_HMR_RUNTIME__.reload来从新渲染挂载一个组件,经过__VUE_HMR_RUNTIME__.createRecord来记录一个组件, 还有__VUE_HMR_RUNTIME__.rerender来从新渲染某个组件。

而后咱们来看看,咱们但愿中的 HMR 的最终目的是啥呢?

  • 只有代码更新了的组件才会被从新渲染
  • 从新渲染的组件可以保持组件以前的状态(state)
  • 若是当前文件并非只 export 组件,那么须要彻底地更新全部模块

只从新渲染更新的组件

第一条不少同窗可能不太好理解,尤为是没有用过 JSX 来进行开发的同窗,若是你以前都是用 SFC 来写组件的,你可能认为一个文件只能 export 一个组件。 可是若是咱们使用 JSX 来进行开发,就像上面的例子,一个文件 export 多个组件是很正常的。 在这种状况下,若是咱们只改了一个组件的代码,可是全部组件都从新渲染,这其实就变得没有必要。

那么咱们如何实现这个功能呢?在使用 babel 插件进行代码编译的时候,咱们给全部的组件计算一个 ID,而且根据组件的源码计算其hash值,编译以后的代码大体以下:

Comp1.__id = 'comp1'
Comp1.__hash = 'xxxxx'
复制代码

而后咱们声明一个全局对象,来存储组件的 Id 和 hash 的映射:

const $VueCompHashMap$ = (window.$VueCompHashMap$ =
    window.$VueCompHashMap$ || {})
复制代码

接下去,在每次模块更新以后,咱们会执行如下代码来接收模块的更新:

if (module.hot) {
    if ($VueCompHashMap$[Comp1.__id] !== Comp1.__hash) {
        __VUE_HMR_RUNTIME__.reload(Comp1.__id, Comp1)
    }
}
复制代码

那么只要组件的源码不变,他的hash也不会改变,在模块从新执行以后,组件也就不会被从新渲染。

保持组件的 state

Vue3 是提供了方法让咱们可以保持组件的 state 的,前面提到的__VUE_HMR_RUNTIME__.rerender就是用来实现这个目的的。

可是并非全部状况都可以实现保持状态的,好比我我的更喜欢的开发方式就没法实现:

export const Comp1 = defineComponent({
    setup() {
        const state = reactive({
            count: 1,
        })
        return () => <div>{state.count}</div>
    },
})
复制代码

对于这样一个组件,其render函数是来自于setup的返回值的,而其对于state的引用是来自于闭包,这里的state并无挂载到任何的对象上。 若是Comp1更新了,咱们须要从新获取其render函数,就须要从新执行setup,那么闭包就会从新生成。因此目前没有什么好的办法来保持这种类型组件的 state。

可是咱们能够改个写法:

export const Comp1 = defineComponent({
    setup() {
        const state = reactive({
            count: 1,
        })

        return {
            state,
        }
    },
    render() {
        return <div>{this.state.count}</div>
    },
})
复制代码

对于这样的组件,咱们能够直接经过Comp1.render来获取其render函数,而statesetup返回以后,会被 Vue3 挂载到组件的this上, 那么对于这样一个组件,咱们只须要获取其render函数,而后经过__VUE_HMR_RUNTIME__.rerender来执行从新渲染,而保持this上的全部 state。

事实上,SFC 的状态得以保持,就是由于 SFC 组件状态是必然保持在this上。并且由于script部分是单独分离的,对于 SFC 的状态代码是否有更新, 能够直观的根据script部分代码是否有修改来判断。而在 JSX 写的组件上,则相对难以判断setup中的代码是否有更新。

我以前跟尤老师讨论过这个问题,在尤老师关于他完成了 Vite 的 JSX 热更新功能的 twitter 上。

跟尤老师的讨论

他也提到了 Composition API 实现的 state 很难保持,而且他提到了 React hooks 的状态在热更新过程当中能够被保持的缘由, 主要是由于 React hooks 的状态其实在组件的实例上是有保存的,并且是根据 hooks 执行的顺序和类型能够判断状态代码是否有改变。 我在React 源码解析中写过 hooks 可以保持状态的缘由。

尤老师专门提了这一点,果真 Vue 和 React 的对比是永远逃不过的话题。

须要更新整个模块的代码

有些状况下即使只有某个组件更新了,咱们仍是须要让整个模块被更新。主要的状况就是若是这个文件向外 export 了非组件代码,咱们就须要更新整个模块。

由于咱们 export 出去的代码必然是会被其余地方调用的,若是咱们执行了module.hot.accept(), 那么 HMR 运行时并不会更新其余引用了这个文件的模块的代码,这就会有问题了,其余模块在执行的时候可能会使用老的当前模块的代码。 咱们处理了组件的更新,由于 Vue3 提供了这个功能。

而其余的 export 的内容就没有这么幸运,有框架来提供这些功能。这种状况下,你能够实现本身的热更新逻辑来更新这些功能,固然这会比较麻烦。 那么最简单的方式,天然就是让 HMR 运行时直接更新整个模块,因此在这种状况下咱们就不该该执行module.hot.accept()

咱们在 babel-jsx 插件中增长了以下代码:

if (module.hot) {
    if ($isVueHMRAcceptable(module)) {
        module.hot.accept()
    }
}
复制代码

$isVueHMRAcceptable这个函数就是来判断当前模块向外导出的是否都是组件的,只有在都是的状况下才accept

总结

以上就简单明了地向你们讲解了 webpack 的 HMR 的原理。咱们借由实现 Vue3 的热更新的过程,来展现了一次热更新中会经历哪些过程,以及须要考虑哪些问题。

核心的模块更新能力,其实 webpack 已经帮助咱们实现了,咱们更多的实际上是须要考虑咱们使用的框架该如何更小代价地执行更新。

React fast Refresh 也是去年才真正成为官方的热更新方案,以前的 React-Hot-Loader 一直存在一些问题,却也一直活跃在 React 生态中。

此次对于 Vue3 热更新的实现,也是更多参考了 React fast Refresh 的设计。可是很遗憾目前没有找到办法来保持组件状态,指望将来能找到方法解决这个问题吧。

BestVue3社区专一于提供Vue3最新鲜最优质的学习内容,你能够搜索微信公众号 BestVue3 进行关注。

相关文章
相关标签/搜索