浅谈vue3设计中的细节

698 阅读3分钟

1. 提供友好的信息

  • 调用了 warn函数, 本质是调用了 console.warn函数, 提供了友好的警告信息

  • 初始化自定义了 formatter 的 initCustomerFormatter函数。(打开DevTools的设置,勾选 “Console” -> "Enable custom formatters"选项), 打印 count.value(vue3)时,内容会变得非常直观

2. 如何支持Tree-Shaking

  • __DEV__常量控制

  • 添加注释 /*#__PURE__*/, 支持摇掉副作用函数

3. 输出的构建产物

    1. 输出 IIFE(立即调用的函数表达式)格式的资源, 满足<script>标签的引入
    <body>
        <script src="xx/vue.js"></script>
    </body>
    
    1. 输出 vue.esm-browser.js ESM 格式的资源, 满足<script type="module" src="xx/vue.esm-browser.js">引入
    1. 输出 vue.runtime.esm-bundler.js 资源, 给 rollup.js 或 webpack 等打包工具使用
    1. 输出 cjs资源, 在服务端使用

4. 特性开关

对于关闭的特性,可以利用 Tree-Shaking 机制摇掉相关代码,使最终打包的资源体积最小化。

特性开关实现: 本质上是利用rollup.js的预定义常量插件来实现。比如

{
    __FEATURE_OPTION_API__: isBundlerESMBuild ? `__VUE_OPITIONS_API__` : true
}

__VUE_OPITIONS_API__ 是个特性开关,通过设置该值来控制是否要包含兼容vue2 选项API的调用方式

5. 错误处理

    1. 统一的错误处理接口
    1. 用户可以注册自定义的错误处理程序
let handleError = null
export default {
    foo(fn) {
        callWithErrorHandling(fn)
    },
    // 用户可以注册自定义的错误处理程序
    registerErrorHandler(fn) {
        handleError = fn
    }
}

function callWithErrorHandling(fn) {
    try {
        fn && fn()
    } catch (e) {
        handleError(e)
    }
}

以上就是vue错误处理的原理

6. 友好的TS支持

7. 声明式描述UI

有两种方式

    1. 模版
<div id="add" :title="title" @click="handle">
    <span></span>
</div>
    1. Js对象
const title = {
    tag: 'h1',
    props: {
        onClick: handler
    },
    children: [
        {
            tag: 'span'
        }
    ]
}

Js对象描述UI比较灵活,其实就是虚拟DOM

组件中手写的渲染函数就是使用虚拟DOM来描述UI的

import { h } from 'vue'

export default {
    render() {
        // h函数返回的是对象
        return h('h1', { onClick: handler }) // 虚拟DOM
    }
}

8. 渲染器

作用:使用DOM操作API,把虚拟DOM渲染为真实DOM

9. 组件的本质

组件是一组DOM元素的封装,这组DOM元素就是组件要渲染的内容

const MyComponent = function() {
    return {
        tag: 'div'.
        props: {
            onClick: () => alert('hello')
        },
        children: 'click me'
    }
}

组件的返回值也是虚拟DOM

10. 模版的工作原理

模版通过编译器, 编译为 渲染函数

<template>
    <div @click="handler">
        click me
    </div>
</template>

<script>
    export default {
        data() {},
        methods: {}
    }
</script>

编译器会将<template>标签里的内容, 编译成渲染函数并添加到 <script>标签块的组件对象上

<script>
    export default {
        data() {},
        methods: {},
        render() {
            return h('div', { onClick: handler }, 'click me')
        }
    }
</script>

总结: 组件(使用模版 或者 手写渲染函数)要渲染的内容都是通过渲染函数产生的,渲染器再把渲染函数返回的虚拟DOM渲染为真实DOM.

11. 渲染器和编译器的配合

模版通过编译器,编译为渲染函数。 这过程中, 会标记哪些是静态属性,哪些是动态属性,把这些信息附加到生成的渲染函数中,让渲染器省去寻早变更点的工作,提升性能

12.为什么选择“声明式”,而不是“命令式”

12.1 命令式关注的是过程,比如“jQuery”,就是典型的命令式框架

分析下面这段jQuery代码

$('#app') // 获取div
 .text('hello world') //设置文本内容
 .on('click', () => { alert('ok') }) //绑定点击事件

描述了 “获取div” -> "设置文本内容" -> "绑定点击事件" 这一过程, 这就是命令式框架的特点:关注过程

12.2 声明式关注的是结果, 比如 “vue”

以上代码用vue实现如下:

<div @click="() => alert('ok')">hello world</div>

可以发现, 以上代码提供的是一个结果,怎么实现这个“结果”,使用者并不关心。实现“结果”的过程,是在Vue.js内部完成的。也就是说, Vue.js帮我们封装了过程, 内部的实现是命令式的,暴露给外部用户的是声明式

12.3 两种范式对比

12.3.1 性能

  • 比如要修改div的内容,命令式代码代码如下

    div.textContent = '你好, 我是修改后的内容'
    

    直接调用相关命令

  • 声明式代码代码如下

    // 修改前的代码
    <div @click="() => alert('ok')">你好, 我是修改前的内容</div>
    
    // 修改后的代码
    <div @click="() => alert('ok')">你好, 我是修改后的内容</div>
    

    找到前后差异,然后更新差异的地方,最终完成跟新的代码仍然是

    div.textContent = '你好, 我是修改后的内容'
    

通过对比可以发现,声明式代码命令式代码 多了一步 找出前后差异的性能消耗

性能: 命令式代码 > 声明式代码

12.3.2 可维护性

命令式代码需要维护整个“过程”,

声明式代码展示最终的结果,不关心过程

声明式代码 > 命令式代码

12.4 总结

vue选择了声明式代码(维护性好),至于性能问题,做到损失最小化,也就是优化找出差异的性能消耗(比如虚拟DOM的出现就是为了最小化性能消耗)

13. 为什么要引入虚拟DOM, 虚拟DOM又是解决什么问题的

前面谈到, 声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,虚拟DOM的出现就是为了最小化找出差异的性能消耗

虚拟DOM要解决的问题是: 写声明式代码且要保证程序的性能

13.1 性能、可维护性、心智负担对比

  • 创建阶段:

    虚拟DOM 和 原生DOM操作, 性能差别不大。

  • 更新阶段:

    虚拟DOM: 创建新的JavaScript对象 + Diff + 必要的DOM更新

    原生DOM操作: 必要的DOM更新

原生DOM操作,需要手动创建、删除DOM元素,所以它的心智负担大,可维护性差,但是性能好

虚拟DOM,不需要手动创建、删除DOM元素,所以它的心智负担小, 可维护性好,加上有Diff,性能虽没原生DOM好,但也不错