第一篇:框架设计概览
第一章:权衡的艺术
命令式和声明式
从范式上来看,视图层框架通常分为命令式和声明式。早年间流行的 Jquery 就是典型的命令式框架
而命令式框架的一大特点就是关注过程,而声明式框架更加关注结果
命令式框架:简单来说自然语言所描述的能够与代码产生一一对应关系,代码本身描述的是做事的过程,就更加符合我们的逻辑知觉
声明式框架:简单来说 Vue.js 帮我们封装了过程, Vue.js 内部实现也是命令式的,而暴露给用户的却更加声明式
性能与可维护的权衡
命令式和声明式各有缺点。在框架设计方面,则体现在性能与可维护性之间的权衡
声明式代码的性能不优于命令式代码的性能,而声明式代码的可维护性更强
这就体现了在框架设计层面上要做出关于可维护性与性能之间的权衡。在采用声明式提示可维护性的同时,性能就会有一定的损失,而框架的设计者要做的就是:在保持可维护性的同时让性能损失最小化
虚拟 DOM 的性能到底如何
声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
因此,如果我们能够最小化找出差异的性能消耗,就可以让声明式代码的性能无限接近命令式代码的性能。而所谓的虚拟 DOM,就是为了最小化找出差异这一步的性能消耗而出现的
运行时和编译时
Vue.js 是一个编译时+运行时的框架,它在保持灵活性的基础上还能够通过编译手段分析用户提供的内容,从而进一步提升更新性能
Compiler 编译时:把 HTML 标签编译成树形结构的虚拟 DOM
Runtime 运行时:将虚拟 DOM 通过渲染函数转换为真实 DOM,渲染在页面上
第二章:框架设计的核心要素
提升用户开发体验
衡量一个框架是否足够优秀的指标之一就是看它的开发体验如何
在框架设计和开发过程中,提供友好的警告信息至关重要。如果这一点都做得不好,那么很可能会收到用户的抱怨。始终提供友好的警告信息不仅能够帮助用户快速定位问题,节省用户的时间,还能够让框架收获良好的口碑,让用户认可框架的专业性
控制框架代码的体积
框架的大小也是衡量框架的标准之一。在实现同样功能的情况下,当然是用的代码越少越好,这样体积就会越小
如果去看 Vue.js 的源码,就会发现每一个 warn 函数的调用都会配合 __DEV__
常量的检查
if (__DEV__ && !res) {
warn(
`Failed to mount app: mount target selector "${container}" returned null.`
);
}
if (__DEV__ && !res) {
warn(
`Failed to mount app: mount target selector "${container}" returned null.`
);
}
在开发环境下 __DEV__
常量设置为 true,上面的代码才有可能去执行
在生产环境下 __DEV__
常量设置为 false,上面的代码永远都不会执行,而这段代码就被称为 Dead Code,它不会出现在最终产物中,在构建资源的时候就会被移除。
这样就做到了 在开发环境中为用户提供良好的警告信息的同时,不会增加生产环境代码的体积
框架要做到良好的 Tree-Shaking
这个概念因为 Rollup.js 而普及。简单来说 Tree-Shaking 指的就是消除那些永远不会被执行的代码
想要实现 Tree-Shaking,必须满足一个条件就是,即模块必须是 ESM,因为 Tree-Shaking 依赖 ESM 的静态结构
框架应该输出怎样的构建产物
Vue.js 构建产物除了有环境上的区分之外,还会根据使用场景的不同而输出其他形式的产物
第一种 IIFE 格式:全称 Immediately Invoked Function Expression 即立即调用的函数表达式。用户希望可以直接在 HTML 页面中使用 <script>
标签引入框架并且使用
<script src="./vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
</script>
<script src="./vue.global.prod.js"></script>
<script>
const { createApp } = Vue;
</script>
第二种 ESM 格式:现在的主流浏览器对原生 ESM 的支持都不错,可以直接引入 ESM 格式的资源
<script src="./vue.global.prod.js" type="module"></script>
<script src="./vue.global.prod.js" type="module"></script>
第三种 CommonJs 格式:是在 Nodejs 环境中运行的,而非浏览器环境。服务端渲染会使用到
const Vue = require("vue");
const Vue = require("vue");
特性开关
在设计框架中,框架会给用户提供诸多特性(或功能),例如我们提供 A、B、C 三个特性给用户。同时还提供了 a、b、c 三个对应的特性开关,用户可以通过设置开关来代表开启或关闭对应的特性,这会带来很多益处
- 对应关闭的特性,可以利用 Tree-Shaking 机制让其不包含在最终的资源中
- 该机制为框架设计带来了灵活性
错误处理
框架错误处理机制的好坏直接决定了用户应用程序的健壮性,还决定了用户开发时处理错误的心智负担
良好的 TypeScript 类型支持
第三章:Vue3 的设计思路
声明式地描述 UI
Vue3 是一个声明式的 UI 框架,意思就是说用户在使用 Vue3 开发页面时是使用声明式描述 UI 的
例如 Vue.js 模板是这样的
<template>
<h1 @click="handler">
<span></span>
</h1>
</template>
<template>
<h1 @click="handler">
<span></span>
</h1>
</template>
那么通过 JavaScript 对象来描述,代码如下所示:
const title = {
tag: "h1",
props: {
onClick: handler,
},
children: [{ tag: "span" }],
};
const title = {
tag: "h1",
props: {
onClick: handler,
},
children: [{ tag: "span" }],
};
而使用 JavaScript 对象来描述,其实就是所谓的虚拟 DOM
Vue3 除了支持使用模板描述 UI 外,还支持使用虚拟 DOM 描述 UI。其实我们在 Vue.js 组件中书写的渲染函数就是使用虚拟 DOM 来描述 UI 的
<script>
import { h } from "vue";
export default {
render() {
// h() 函数就是一个辅助创建虚拟DOM的工具函数
return h("h1", { onClick: handler });
},
};
</script>
<script>
import { h } from "vue";
export default {
render() {
// h() 函数就是一个辅助创建虚拟DOM的工具函数
return h("h1", { onClick: handler });
},
};
</script>
组件的渲染函数:一个组件要渲染的内容是通过渲染函数来描述的,也就是上面代码的 render 函数,Vue.js 会根据组件的渲染函数的返回值拿到虚拟 DOM,然后就可以吧组件的内容渲染出来了
初始渲染器
虚拟 DOM 是如何变成真实的 DOM 并且渲染到浏览器页面中的呢?这就需要用到我们的渲染器
渲染器的作用就是把虚拟 DOM 渲染为真实 DOM
假设我们有如下的虚拟 DOM
const vnode = {
tag: 'div'
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
const vnode = {
tag: 'div'
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
接下来编写一个简单的渲染器,将上面的虚拟 DOM 渲染为真实 DOM
/**
* vnode: 虚拟DOM
* container:挂载的容器
*/
function renderer(vnode, container) {
// 使用 vnode.tag 作为标签名称创建DOM元素
const el = document.createElement(vnode.tag);
for (const key in vnode.props) {
if (/^on/.test(key)) {
al.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)) {
// 递归调用 renderer函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach((child) => renderer(child, el));
}
// 将元素添加到挂载点下
container.appendChild(el);
}
/**
* vnode: 虚拟DOM
* container:挂载的容器
*/
function renderer(vnode, container) {
// 使用 vnode.tag 作为标签名称创建DOM元素
const el = document.createElement(vnode.tag);
for (const key in vnode.props) {
if (/^on/.test(key)) {
al.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)) {
// 递归调用 renderer函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach((child) => renderer(child, el));
}
// 将元素添加到挂载点下
container.appendChild(el);
}
之后就可以调用渲染器函数
renderer(vnode, document.body);
renderer(vnode, document.body);
总结:渲染器的工作原理归根结底,都是使用一些熟悉的 DOM 操作 API 来完成渲染工作
组件的本质
组件就是一组 DOM 元素的封装,一组 DOM 元素就是组件要渲染的内容。因为可以定义一个函数来代表组件,而函数的返回值就代表组件要渲染的内容
const MyComponent = function () {
return {
tag: "div",
props: {
onClick: () => alert("hello"),
},
children: "click me",
};
};
const MyComponent = function () {
return {
tag: "div",
props: {
onClick: () => alert("hello"),
},
children: "click me",
};
};
可以看到,组件的返回值也是虚拟 DOM,它代表组件要渲染的内容
模板的工作原理
无论是手写虚拟 DOM 还是使用模板,都属于声明式描述 UI,那么模板是如何工作的呢?
这就要提到 Vue 的另一个重要组成部分:编译器。而它的作用其实就是将模板编译为渲染函数
<template>
<div @click="handler">Click Me</div>
</template>
<template>
<div @click="handler">Click Me</div>
</template>
通过编译器需要编译成如下代码:
function render() {
return h("div", { onClick: handler }, "Click Me");
}
function render() {
return h("div", { onClick: handler }, "Click Me");
}
无论是模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue 渲染页面的流程
Vue.js 是各个模板组件的有机整体
如前所诉,组件的实现依赖于渲染器,模板的编译依赖于编译器。它们共同构成一个有机的整体,不同模块组件相互配合,进一步提升框架性能