likes
comments
collection
share

Vue3源码学习-2 | 框架设计的核心要素

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

Vue3源码学习-2 | 框架设计的核心要素

Vue3源码学习 | 框架设计的核心要素

框架设计要比想象得复杂多,并不是说只把功能开发完成,能用就算大功告成了,这里面还有很多学问。我们针对框架设计抛出几个问题:

  • 当用户没有以预期的方式使用框架时,是否应该打印合适的警告信息从而提供更好的开发体验?
  • 开发版本的构建和生产版本的构建有何区别?
  • 当框架提供了多个功能,而用户只需要其中几个功能时,用户是否选择关闭其他功能从而减少最终资源的打包体积?

(建议:阅读本节内容,大家最好对常用的模块打包工具有一定的经验,eg:rollup.js / webpack,咳咳,只是建议)

2.1 提升用户的开发体验

衡量一个框架是否足够优秀的指标之一就是看它的开发体验如何

实例说明

// Vue3.js
createApp(App).mount('#not-exist')

当我们创建一个Vue.js应用并试图将其挂载到一个不存在的DOM节点时,就会收到一条警告信息

[Vue warn]:Faild to mount app:mount target selector "#not-exist" returned null.

这条信息告诉我们挂载失败了,并说明了失败的原因:Vue.js根据我们提供的选择器无法找到相应的DOM元素(返回null),这条信息让我们能够清晰且快速地定位问题。

所以在框架的设计和开发中,提供友好的警告信息不仅能够帮助用户快速定位问题,节省用户时间,还能够让框架收获良好的口碑,让用户认可框架的专业性

Vue3源码

在Vue.js源码中,经常能够看到warn函数的调用,上述的警告信息就是由下面这个warn函数调用打印的:

warn(
    `Faild to mount app:mount target selector "${container}" return null.`
   )

对于 warn 函数来说,由于它选哟尽可能提供有用的信息,因此它需要收集当前发生错误的组件栈信息。如果你去看源码,就会发现有些复杂,但其实始终就是调用了 console.warn 函数

除了必要的警告信息外,还有很多其他方面可以作为切入口,进一步提升用户的开发体验

2.2 控制框架代码的体积

框架的大小也是衡量框架的标准之一

实例说明

如果让我们去看 Vue3 的源码,就会发现每一个 warn 函数的调用会配合 __DEV__ 常量的检查,例如:

if(__DEV__ && !res) {
    warn(
        `Faild to mount app:mount target selector "${container}" return null.`
       )
}

可以看出,打印警告信息的前提是:__DEV__这个常量一定要为true,这里的__DEV__常量就是达到目的的关键。 这时候你就会问:

  • 明明这个直接if(!res)就可以直接判断是否存在错误,__DEV__的存在是不是有点多余,没错!我刚开始也是这么想的,但是!存在即合理!接着看!

这里先说明__DEV__怎么来的:Vue.js使用 rollup.js 对项目进行构建,这里的__DEV__常量实际上是通过rollup.js的插件配置预定义的,其功能类似于 webpack 中的 DefinePlugin 插件

开发环境 与 生产环境

可能有的读者看到这里会对开发环境生产环境产生疑惑:这两个环境到底是个什么玩意儿?

  • 开发环境:是程序猿们专门用于开发的服务器,配置可以比较随意, 为了开发调试方便,一般打开全部错误报告。(程序员接到需求后,开始写代码,开发,运行程序,看看程序有没有达到预期的功能)
  • 生产环境:指正式提供对外服务的,一般会关掉错误报告,打开错误日志。(就是线上环境,发布到对外环境上,正式提供给客户使用的环境)
Vue.js 在输出资源的时候,会输出两个版本,其中一个用于开发环境,如vue.global.js,另一个用于生产环境,如vue.global.prod.js,通过文件名我们也能够区分。

代码在不同环境的运行

  • 当Vue.js构建用户开发环境资源时,会把__DEV__常量设置为true,这时上面那段输出警告的代码就等价于:
       if(true && !res) {
          warn(
              `Faild to mount app:mount target selector "${container}" return null.`
             )
      }
    

可以看到,这里是将__DEV__常量替换成字面量true,所以这段代码在开发环境中是肯定存在的。

  • 当Vue.js用于构建生产环境的资源时,会把__DEV__常量设置为false,这时上面那段输出警告信息的代码就等价于:

       if(false && !res) {
          warn(
              `Faild to mount app:mount target selector "${container}" return null.`
             )
      }
    

    很明显!这是我们发现这段分支代码永远都不会执行!

    这段永远不会执行的代码称为dead code,它不会出现在最终产物中,在构建资源的时候就会被移除,因此在vue.global.prod.js 中是不会存在这段代码的。

这样我们就做到了在开发环境中为用户提供友好警告信息的同时,不会增加生产环境代码的体积。

2.3 Tree-Shaking

为什么要说这个可能陌生的东西?没错,就是因为上面的东西做的不够好,或者说!这才是真正的主角

吹归吹,咳咳,那什么是Tree-Shaking呢?在前端领域,这个概念因 rollup.js 普及。

  • 简单来说,Tree-Shaking指的就是消除那些永远不会被执行的代码,也就是消除dead code,现在无论是rollup.js 还是webpack,都支持 Tree-Shaking。

实例说明

(特殊说明:以下实例以 rollup.js 进行说明Tree-Shaking如何工作)

目录结构

|—— demo
|        └─ package.json
|        └─ input.js   
|        └─ utils.js 

首先安装 rollup.js

  yarn add rollup -D
# 或者 npm install rollup -D

下面是input.js 和 utils.js 文件内容

// input.js
import { foo } from './utils.js'
foo()
// utils.js
export function foo(obj) {
    obj && obj.foo
}
export function bar(obj) {
    obj && obj.bar
}

在utils.js文件中定义并到处两个函数,分别是foo函数和bar函数,然后在input.js中导入foo函数并执行。 注意:我们并没有导入bar函数

接着,我们执行如下命令进行构建

npx rollup input.js -f esm -o bundle.js

这句命令的意思,以input.js文件为入口,输出ESM,输出的文件叫做bundle.js。

ESM:实现Tree-Shaking,必须满足一个条件,即模块必须是ESM(ES Module),因为Three-shaking依赖的静态结构(至于不知道静态结构是啥的小伙伴,自己去查!略略略!) 打开 bundle.js 来查看内容

// bundle.js
function foo(obj) {
    obj && obj.foo
}

可以看出!其中并不包含bar函数,这说明什么!说明!Tree-Shaking起了作用!由于我们并没有使用bar函数,因此它作为dead code被删除了。

但是咱们仔细看看上面的代码会发现,foo函数的执行也没有什么意义,仅仅是读取了对象的值,所以它的执行狮虎没什么必要。那么问题来了:

  • 既然把这段代码删了不会对我们的应用程序产生影响,那么为什么 rollup.js 不把这段代码也作为 dead code 移除呢?
接着往下看 副作用

Tree-Shaking 中的 副作用

这就涉及Tree-shaking中的第二个关键点——副作用。如果一个函数调用会产生副作用,那么就不能将其移除。

  • 那什么是 副作用 呢?

简单来说,副作用就是,当调用函数的时候会对外部产生影响。eg:修改了全局变量

  • 这时你可能会说,上面的代码明显是读取对象的值,怎么会产生副作用呢?

其实是有可能的,试想一下,如果obj对象是一个通过Proxy创建的代理对象,那么当我们读取对象属性时,就会触发代理对象的get夹子(trap),在get夹子中是可能产生副作用的,例如我们在get夹子中修改了某个全局变量。

而到底会不会产生副作用,只有代码真正运行的时候才能知道,JavaScript 本身是动态语言,因此想要静态地分析哪些代码是dead code很有难度,上面只是一个例子!

  • 那么,问题又来了!我们该如何辨别哪些代码是 dead code 呢?

像 rollup.js 这类工具都会提供一个机制,让我们能明确地告诉 rollup.js:“放心吧,这段代码不会产生副作用,你可以移除它。” 具体怎么做呢?那就是 打标识

实例说明

修改 input.js 文件

import { foo } from './utils'

/*__PURE__*/ foo()

想必你也发现了,注释代码/*__PURE__*/,其作用就是告诉 rollup.js,对于 foo 函数地调用不会产生副作用,你可以放心地对其进行 Tree-shaking。

基于这个案例,我们应该明白,在编写框架的时候需要合理使用/*__PURE__*/注释。如果你去搜索Vue3的源码,会发现它大量使用了该注释。例如:

export const isHTMLTag = /*__PURE*/ makeMap(HTML_TAGS)
  • 有人又会开始问,这样子做会不会太麻烦了?等会满屏幕的代码都是 /*__PURE__*/?

其实不会,因为通常产生副作用的代码都是模块内函数的顶级调用

  • 好!问题又来了,顶级调用又是什么玩意儿

直接上代码!

foo() // 顶级调用

function bar() {
    foo() // 函数内调用
}

可以看到,对于顶级调用来看,是可能产生副作用的;但是对于函数内调用来说,只要函数bar没有被调用,那么foo函数的调用自然不会产生副作用。

在Vue3源码中,基本都是在一些顶级调用的函数上使用/PURE/注释,当然,该注释不仅仅作用于函数,它可以应用于任何语句上。该注释也不是只有rollup.js才能识别,webpack以及压缩工具(如terser)都能识别它

2.4 性能开关

在设计框架时,框架会给用户提供诸多性能(或功能),例如我们提供A、B、C三个特性给用户,同时还提供了a、b、c三个对应的特性开关,用户可以通过设置a、b、c为true/fasle来代表开启或关闭对应的特性,这将会带来很多益处。

  • 对于用户关闭的特性,我们可以利用Tree-Shaking机制让其不包含在最终的资源中。
  • 该机制为框架设计带来了灵活性,可以通过特特性开关任意为框架添加新特性,而不用担心资源体积过大。同时,当框架升级时,我们也可以通过特性开关来支持遗留API,这样新用户可以选择不使用遗留API,从而使最终打包的资源最小化。

实现特性开关

其实很简单,原理和上文提到的__DEV__常量一样,本质上利用 rollup.js 的预定义量插件来实现。

实例说明

拿Vue3源码中的一段rollup.js配置来说

{
 _FEATURE_OPTIONS_API_:isBundlerESMBuild ? '_VUE_OPTIONS_API_':true,
 }

其中 _FEATURE_OPTIONS_API_类似于__DEV__。在Vue3源码中可以找到很多类似的判断分支:

特殊说明:看不懂这段代码没关系,注意if的条件,至于这段代码干嘛的下面会说
// support for 2.x options
if(_FEATURE_OPTIONS_API_){
    currentInstance = instance
    pauseTracking()
    applyOptions(instance,Component)
    resetTracking()
    currentInstance = null
}

当Vue构建资时,如果构建的资源时供打包工具使用(即带有-bundler字样的资源),那么上面的代码在资源中会变成:

// support for 2.x options
if(_VUE_OPTIONS_API_){
    currentInstance = instance
    pauseTracking()
    applyOptions(instance,Component)
    resetTracking()
    currentInstance = null
}

其中的_VUE_OPTIONS_API_是一个特性开关,用户通过_VUE_OPTIONS_API_预定义常量的值来控制是否要包含这段代码。通常用户可以使用webpack.DefinePlugin插件来实现:

// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
    _VUE_OPTIONS_API_:JSON.stringfy(true) // 开启特性
})
_VUE_OPTIONS_API_的作用!
  • 在Vue2中,我们编写的组件叫做组件选项API:
export default {
    data(){}, // data选项
    computed:{} // computed 选项
    // 其他选项
}
  • 在Vue3中,推荐使用 Composition API 来编写代码,例如:
export default {
    setup() {
        const count = ref(0)
        const doubleCount = computed(() => count.value * 2) 
    }
}

在Vue3中兼容Vue2的选项API。但是如果明确知道自己不会使用选项API,用户就可以使用_VUE_OPTIONS_API_开关来关闭该特性,这样在打包的时候Vue的这部分代码就不会包含在最终的资源中,从而减小资源体积

2.5 总结

  • 提高友好的警告信息至关重要,这有助于开发者快速定位问题
  • 开发环境生产环境,控制生产环境中不包含开发环境中的一些代码,从而实现线上代码体积的可控性
  • Tree-Shaking 是一种排除 dead code 的机制,与打包工具配合实现打包代码的体积最小
  • 按需引入 Tree-Shaking可以帮助实现

番外

浅提一嘴:使用TS编写框架和框架对TS类型支持友好是两件完全不同的事

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