我从Vuejs中学到了什么(一)

115 阅读5分钟

Vuejs的框架设计

设计一个框架有哪些注意事项

框架设计是一个比较复杂的过程,并不是说把代码写完就算完成了,框架本身还需要提供给开发者更好的开发体验。下面简单总结一下除了框架本身的代码之外,还有哪些注意事项:

  1. 提升用户开发体验:比如开发者代码有误,框架层在浏览器展示的错误提示
  1. 控制框架整体的代码体积,比如代码中的__DEV__常量控制
  1. 框架本身要做好 tree shaking
  1. 控制好输出产物:ESM、CJS、IIFE等
  1. 特性开关及,比如哪些功能开发者用不到,可以在配置阶阶段选择关闭,比如说__FEATURE_OPTIONS_API__,可以通过rollup预定义的常量来看是否来开启optional api的选项等
  1. 错误处理,比如vuejs自带的 callWithErrorHandling函数
  1. 良好的TypeScript支持
  1. ...

关于第3、4、5点来说,vuejs通过rollup就可以很好的做到了这些。

Vuejs是什么样的框架

作为vue的使用者,我们在学习框架的时候要从全局的角度对框架的设计拥有清晰的认知,否则会很容易被细节困住,看不清全貌。

编程范式

视图层的框架一般分为命令式和声明式,最早的jQuery是命令式框架的代表,命令式框架的特点就是关注过程:

// jquery实现
$("#app") // 获取div
    .text("hello jquery") // 给div设置文本内容
    .on("click", () => {console.log("hello jquery")}) // 给div绑定点击事件
 
 // 原生实现   
 const app = document.getElementById("app")
 app.innerText = "hellol jquery"
 app.addEventListener("click", () => {console.log("hello jquery")})

在我们编写js代码的大部分时间,我们都是来编写命令式的代码。

声明式框架(范式)是不关注过程但关注结果的。以vuejs为例:

<div @click="() => {console.log('clicked')}">hello vuejs</div>

针对上面的代码来说,其实是整个vuejs的底层帮助开发者封装了过程,也就是说,vuejs底层一定是命令式的,暴露给开发者的是声明式的。

需要做个简单的总结来说,命令式代码的性能是优于声明式代码的。

虚拟DOM

我们在讨论虚拟DOM的时候总是在讨论虚拟DOM的性能好坏,总会觉得用了虚拟DOM后,框架就会变快。但实际上采用了虚拟DOM更新技术的性能理论上是不会比你直接写原生JS的性能要快的,以数据说话:dom-benchmark 可以来这个网站测一测各大框架以及naive vanilla js的性能比较。

生成一个dom节点,我们从心智负担、维护性和性能方面来做一下比较:

流程图 (1).jpg

// innerHTML 直接操作dom
const html = `
    <div><span>hello vuejs</span></div>
`
div.innerHTML = html
// 虚拟DOM
const html = {
    tag: 'div',
    children: [
        tag: 'span',
        children: 'hello vuejs'
    ]
}

// vue的声明式UI描述
<template>
    <div><span>hello vuejs</span></div>
</template>
// naive vanalla js
const div = document.createElement('div')
const span = document.createElement('span')
div.appendChildren(span.appendChildren('hello vuejs'))
编译时vs运行时

设计框架的时候一般会采用三种选择:纯运行时、纯编译时、运行时+编译时。具体的框架实现,要根据目标框架的特征、期望等做出适当的决策。

以纯运行时

const obj = {
    tag: 'div',
    children: [
        {tag: 'span', children: 'hello vuejs'}
    ]
}
const render = (obj, root) => {
    const el = document.createElement(obj.tag) {
        if (typeof obj.children === 'string') {
            const text = document.createTextNode(obj.children)
            el.append(text)
        } else {
            obj.children.forEach((child) => render(child, el))
        }
    }
    root.appendChildren(el)
}

直接运行上面的代码就可以看到我们想要的东西。

纯编译时的框架

目前市面上也是有纯编译时的框架,比如svelte。svelte是一个,纯编译时+ no virtual dom + truly reactive的框架,简单来说下原理就是将模版代码编译为命令式的dom操作,比如:

// 模版代码
<a>{{ msg }}</a>
// 编译后的代码
function renderMainFragment (root, component, target) {
    var a = document.createElement('a')
    var text = document.createTextNode( root.msg )
    a.appendChild(text)
    target.appendChild(a)
    return {
    update: function (changed, root) {text.data = root.msg},
    teardown: function (detach) {
        if (detach) a.parentNode.removeChild( a )
        }
      }
    }

由于svelte没有采用virtual dom 所以少了patch 和 diff 操作,也就少了这部分算法的代码,所以svelte的打包体积会非常小,同时采用了命令式的直接操作dom的方式,这也就显的svelte的性能非常好。

编译时+运行时

vuejs就是编译时+运行时框架的代表了,以上面纯运行的代码为例,我们不可能让开发者每次都手写所谓的dom object,然后手动render,这样的框架我想没人愿意去用。那能不能通过某种手段,可以通过把模版代码即html标签编译为dom object,然后再去render的方式呢?

// 伪代码
// 开发者手写模版代码
const html = `
    <div>
        <span>hello vuejs</span>
    </div>
`
// 实际通过编译后的代码
const obj = {
    tag: 'div',
    children: [
        tag: 'span',
        children: 'hello vuejs'
    ]
}

// 整个过程就分为了两部分
// 1. 编译
const domObj = Complier(html)
// 2. 渲染
render(domObj, document.body)

上面的伪代码中,字符串模版经过编译器的转换,转换为dom object, 然后再由渲染函数render为真实的dom节点。上面的complier函数将字符串编译成了一个dom对象,那么同样可以编译成命令式的代码,那么其实我们可以不要render函数了,直接将complier 和 render函数二合一为一个complier即可。

以上vuejs就是采用了运行时+编译时的策略,是vuejs成为了一个即保证了代码的可维护性高,对开发者心智负担小,又拥有了不输svelte性能的优秀框架。

vuejs的本质

上面我们简单的了解了一下编程范式,虚拟DOM,以及几个别框架的区别,那么最后让我们看看vuejs的本质是什么:

  1. 声明式的描述UI,即template模版代码
  1. 拥有编译器,将模版代码编译成可描述dom节点的dom object
  1. 拥有渲染器,通过渲染器来进行虚拟dom比较来进行真实dom的创建、更新等操作
  1. DOM元素的封装,即组件的本质
  1. 响应式的数据更新
  1. 其他额外的功能及细节处理

所以对vuejs框架的组成来说,看起来就像是以下公式:

vuejs = declarative UI + complier(complie + render) + reactive data

再来简单的聊一下上面提到的编译器,看下代码示例:

<template>
    <div @click="handler">click me </div>
</template>
<script>
export default {
    data() {/*...*/},
    methods: {
        handler: () => {/*...*/}
    }
}
</script>
//通过编译器最终会被编译为一个对象,而模版代码都会编译成对应的render函数
//编译后
export default {
    data() {/*...*/},
    methods: {
        handler: () => {/*...*/}
    },
    render() {
        return h('div', {onClick: handler}, 'click me')
    }
}

实际上编译过程比上面要复杂很多,我们以后有机会讲一下。