初探Vue框架设计

636 阅读8分钟

1、Vue如何做框架权衡的

1.1命令式和声明式

在编程语言中命令式就是关注计算机执行的步骤,通俗一点就是关注过程,而声明式就是通过某种数据结构告诉计算机你要做什么,声明式也叫做注解编程,注重结果,并不关系过程。

命令式实现:

let node = document.querySelector('#app')
node.innerText = 'hello world'
node.addEventListener('click',()=>alert('ok'))

声明式实现:

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

经过上面的对比可以看到Vue其实给我们提供了跟html+css+js的写法让用户通过声明的方式去告诉Vue我们需要得到的结果中间的过程Vue会帮我们处理好,但是可以肯定的是最后Vue也是需要通过命令式去实现的。

1.2 性能跟可维护性的权衡

框架的设计主要体现在性能以及维护性上面,但是声明式跟命令式都有自己的优缺点,性能方面命令式是优于声明式的,但是在可维护性上面项目体量一旦过大,命令式会大大增加维护人员的心智的。

还是以上面的例子举例我们需要修改一段文本时

命令式

node.innerText = 'hello Vue3'

声明式

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

当我们需要需改文本内容时命令式我们知道哪里有修改所以直接修改即可,但是命令式不能做到这一点,因为它只是描述结果的,对于框架来说性能更新只需要找出修改的地方最后调研命令式完成更新。

我们把直接修改性能定义为A,找出差异定义为B

基于以上我们得到结论就是

命令式的更新性能消耗 = A

声明式的更新性能消耗 = A + B

那么声明式会比命令式多出一个寻找差异的性能消耗时间,如果查找时间为0那么两者的性能是一致的,所以才说命令式性能优于声明式。

Vue为什么要选择声明式呢,原因就是命令式在代码实现上面太过复杂维护成本过于高了,那么框架要做的就是在可维护性的基础上去降低性能的损失。

1.3 虚拟DOM性能到底如何

上面的得知声明式消耗 = 差异 + 直接修改,那么虚拟DOM就是用来优化找差异这个过程的。

我们都知道在创建DOM的时候原生调用innerHTML去创建,但是这个过程需要进行字符串拼接计算再进行JavaScript算最后才能创建真实DOM渲染,然而虚拟DOM只需要两步那就是JavaScript进行计算进行真实DOM创建,虽然虚拟DOM这次通过直接进行JavaScript计算比原生innerHTML少了一个DOM计算时间,但是好像看起来也没啥有事,其实不然在页面更新的过程中innerHTML会重新创建新的DOM,然后销毁之前的DOM,再次在页面上生成新的DOM,然而虚拟DOM这是优势就出来了,只需要更新改变的位置即可,所以经过通过以上列子我们得出结论创建DOM的性能对比 原生 > 虚拟DOM > innerHTML..

1.4 编译时和运行时

运行时框架通过约定好的规则进行开发,但是灵活度低,用户开发体验差,然而运行时代码需要有一个提前编译过程期间会产生一定的性能开销,所以Vue3选择了编译时 + 运行时的框架,在保留灵活的基础上,还能通过编译把用户提供内容进行展示,从而达到性能提升。

2、框架设计的核心要素

2.1 提示用户的开发体验

Vue在开发框架的时候,为了提高用户可以快速定位错误问题,当然少不了一下准确的错误提示,所以说一个好的框架错误提示是不可缺少的,这能够提升用户的开发体验,当然除了这些警告Vue3 在控制台直接ref的响应式数据时,并没有直接展示对应的值,而是展示一个响应式对象,这个新手的话可能会一脸蒙蔽,当然Vue3也提供解决方案那就配置浏览器控制台勾选Enable custom formatters 这个选项去激活Vue3 提供能格式化函数。

未勾选:

勾选后:

2.2 控制代码体积

为了提升用户开发体验Vue3代码里面的一些错误提示,也会在打包时增加项目体积,那么Vue3通过对环境区分进行打包,必要的错误提示会根据环境去进行区分是否打包,这样就可以保障开发时的用户体验也能保正上线时项目代码的体积最小化。

2.3 给项目做Tree-Shaking

Vue3给我们提供了很多组件以及Api,但是用户不一定全部都会用到,所以当没有用到的时候这些代码都将成为dead code,对于这些代码都是需要进行剔除的,因为会影响项目的打包体积。

2.4 特性开关

Vue3为了兼容Vue2也有一些相应的兼容代码所以说如果当我们不使用Vue2的optionsAPi时我们可以将它关闭掉可以优化最终的打包体积

关闭使用options Api

 new webpack.DefinePlugin({
    __VUE_OPTIONS_API: JSON.string(true)
 })

2.5 错误处理

良好的框架当然离不开错误处理,不可能让用户自己去手写try catch自己捕获异常,所以说Vue会包错误异常都放一个封装好的函数里面去执行,这样就能统一捕获异常,只需要给到不同的错误信息即可。

let handleError = null
    export default {
        foo(fn) {
            callWithErrorHandling(fn)
        },
        registerErrorHandler(fn) {
            handleError = fn
        },
    }
    function callWithErrorHandling(fn) {
        try {
            fn && fn()
        } catch (e) {
            console.log(e)
        }
    }

这个封装思路也就是Vue给我们提供的错误注册函数

app.config.errorHandler = ()=>{}

2.6 良好的TypeScript支持

TypeScript编写的程序不一定就是对TS有良好的支持这是两码事,Vue源码中有一个runtime-code有近200行代码就是为了给TS提供类型支持的,所以说类型推到也需要框架层面去进行兼容

3、Vue3的设计思路

3.1声明式的描述UI

在Vue当中我们书写代码基本跟原生差不多,只需要某些地方遵循Vue的规则即可,在用户体验方面Vue做得无疑是三大框架最好的。

标签原生

:或者v-bind 描述动态属性

@描述事件

 <div :class="dynamicId" @click="()=> alert('ok')">{{value}}</div>

当然我们处理模板方式进行声明还可以使用对象来描述但是这对开发中要求较高。

{
  tag:'h1',
    props:{
      onClick:handler
    },
    children:[{tag:'span'}]
}

模板跟JavaScript对象创建的不同对比

列子:根据级别选择不同的模板

模板创建

<h1 v-if="level===1"></h1>
<h2 v-if="level===2"></h2>
<h3 v-if="level===3"></h3>
<h4 v-if="level===4"></h4>

对象创建

let level = 3
{
  tag:`h${level}`,
}

可以发现模板创建的时候我们不得不穷举每一个类别的标签,但是对象我们只需要修改变量即可,对象创建就是所谓的虚拟dom。

其实我们自己手写的渲染函数就是一个虚拟Dom描述UI的过程

 const { createApp, ref, h } = Vue;
    const app = createApp({
        render() {
            return h('h1', { onClick: handle }, 'hello')
        },
        setup() {
            let value = ref('hello Vue3')
            console.log(value)
            return { value } 
        }
    });
    function handle() {
        console.log('hello')
    }
    app.mount('#app')

3.2 初识渲染器

通过上面的例子我们知道了什么是虚拟DOM,那么渲染器的职责就是将虚拟DOM变成真实的DOM

虚拟DOM ---> 渲染器 --->真实的DOM.

渲染器例子:

假设我们有以下结构的虚拟DOM我们如何把它变成真实的DOM呢

手写一个粗糙的渲染器

渲染函数需要提供两个参数第一个就是虚拟dom第二个就是我们的容器。

创建真实dom分为三步

第一步将vnode的tag作为标签来创建

第二步遍历props key如果是on开头的会经过字符串截取之后注册事件

第三部childer处理如果是个字符串就当做文本处理如果数组递归调用rederer函数

const vnode = {
        tag: 'div',
        props: {
            onClick: () => alert('hello Vue3')
        },
        children: 'click me'
    }
    function renderer(vnode, container) {
        const el = document.createElement(vnode.tag)
        for (const key in vnode.props) {
            if (/^on/.test(key)) {
                el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key])
            }
        }
        if (typeof vnode.children === 'string') {
            el.appendChild(document.createTextNode(vnode.children))
        } else if (Array.isArray(vnode.children)) {
            vnode.children.forEach(child => renderer(child, el));
        }
        container.appendChild(el)
    }
    renderer(vnode, document.body)

3.3 组件的本质

组件的本质就是对DOM元素的封装,这组元素就是组件需要展示的内容。

3.4 模板的工作原理

模板的作用就是将我们在template里面的元素提取出来然后通过模板通过编译之后得到渲染函数然后注入到我们的srcipt标签里面也就是我们的render,所以说无论是使用模板还是对象创建UI最后都会返回虚拟DOM经过渲染器转换为真实的DOM,模板还有一个很重要的职责就是给标签打标记,为的就是让渲染器更加高效的去查找那些需要动态更新的地方。