Chapter1 权衡的艺术
从范式上,视图层框架分为命令层和声明式
如果修改性能定义为A, 查找差异的性能消耗定义为B
视图层:jQuery (关注过程)性能更好, A 声明式: vue(只关注结果,过程vue已经封装好)性能差一些, A + B
vue框架如何在设计上做出:把可维护性的同时而又性能损失最小化呢?
使用虚拟dom ,找出最小化差异的性能,但是还是没有原生JavaScript操作dom的效率高
当我们对页面某个文字进行修改时
可以看出结果,性能最高是
综合以上图,我们可以考虑有没有什么办法,既能声明式的描述ui,又具备原生JavaScript的性能呢? chatper2 做详细讲解
设计一个框架时,我们有三个选择:纯运行时、运行时+编译、 纯编译时,以下是讲解纯运行时
1.1 纯运行时
假设我们设计一个框架,他提供render函数,用户可以为该函数提供一个树形结构的数据对象,然后再render函数会根据该对象递归地将数据渲染成dom 元素 ( 纯运行时,传入约束的字段格式,就运行成需要dom)
const obj = {
tag:'div',
children: [{
tag: 'span',
children: 'hello world'
}]
}
function Render (obj, root) {
const el = document.getElement(obj.tag);
if (typeof obj.children === 'string' ){
const text = document.createTextNode(obj.children)
}
else if (typeof obj.children === 'object' ){ //模板时
...mountComponent(obj, root)
}
else if (obj.children) {
// 数组,递归调用 render,使用el作为root 参数
obj.children.forEach((child)=> Render((child,el)))
}
root.appendChild(el)
}
但是某一天,手写的树形结构数据对象太麻烦,而且不直观,使用支持类似于html标签方式求描述树形结构的数据对象呢? 不可能每次都这样去定义吧,太麻烦,于是考虑了再添加一个编译方法,把html标签编译成树形结构的数据对象,这样也可继续使用render函数
1.2 运行时 + 编译时
于是编写了一个compiler方法,他的作用就是将html字符串编译成树形结构的数据对象 (运行时 + 编译时的框架,既支持运行时,用户可以直接提供数据源对象从而无需编译,又支持编译时,用户可以提供html进行处理)
const html = `<div>
<span>hello world</span>
</div>`
//调用 compiler 方法编译得到树形结构的数据对象
const obj = Compiler(html)
//在调用render渲染
Render(obj,document.body)
1.3 纯编译时
既然编译器可以直接把html 字段串编译成数据对象,那可不可以直接编译成命令式代码呢? 这样就只需要一个compiler 函数就可以了
<div>
<span>hello world</span>
</div>
const div = document.createElement('div')
const span = document.createElement('span')
span.innerHtml = 'hello world'
div.appendChild(span)
document.body.appendChild(div)
Chapter2 框架设计的核心要素
2.1 提高用户开发体验
vue 对输出做了更详细的优化: 如:
createApp(App).mount('#app')
warn(`failed to mount app: mount target selector ${container} returned null`)
tips:可以勾选chrome -> setting -> console -> enable custom formatters 查看更详细的console 日志, 如ref(0)
2.2 控制框架代码体积
rollup.js 教程: www.rollupjs.com/tutorial/
vue输出资源时,会输出两本版本,通过环境做区分,DEV__是rollup.js 的插件配件来预定义的, 是如果__DEV 为true,则代表开发环境,才会执行以下代码,这样可以在生产环境中减少代码的提交
if ( __DEV__ && !res) {
warn(`failed to mount app: mount target selector ${container} returned null`)
}
2.3 框架要做到良好的there-shaking
上个内容提到vue使用了__DEV__这种通过环境常量来区分环境,这样可以使生产环境不包含打印的代码,但是仍然不够,比如 vue内建了很多组件,如:, 我们平时根本没有用到这个组件,那还需要在项目里去构建吗?那肯定可以不用内建,所以还是得采用 there-shaking there-shaking 是因rollup.js 普及,webpack也支持, 它的目的就是消除那些永远不会执行的代码,但是要实现,必须是ESM(ES module),因为它依赖esm 结构骂我们可以举个 rollup的列子,看它如何工作
//目录:
|---dom
|------package.json
|------input.js
|------utils.js
//安装rollup.js
yarn add rollp -D //或者 npm install rollp -D
// input.js
import {foo} from './utils'
foo()
//utils.js
export function foo(obj) {
obj && obj.foo
}
export function bar(obj) {
obj && obj.bar
}
//执行
npx rollup input.js -f esm -o bundle.js
//以input.js 为入口,输出esm ,输出文件叫 bundle.js
//bundle.js
function foo(obj) {
obj && obj.foo
}
foo()
可以看到,输出的bundle文件没有包含bar函数,说明there-shaking起了作用,他把dead code 删除了,它删除了对程序也没影响,不过foo方法似乎也没有啥用,因为没有具体执行什么内容,那为甚dead code不删了它,因为这涉及到了proxy的一个关联关系,proxy做代理对象,会触发dialing对象get夹子,如果在夹子修改一个全局对象,js又是动态语言,则会报错,rollup也提供看一个机制,用来告诉你那些代码不会产生副作用,你可以删了它, 就是 /#PURE_/ 注释。
2.4 框架应该输出怎样的架构产物
上文提到vue为开发环境[vue.global.js]和生产环境[vue.global.prod.js]输出了不同的包,生产环境不包含警告信息,实际上vue构建产物除了有环境上的区别,还有使用场景的不同而输出其他形式的产物,这里讨论这些产物的用途以及构建阶段的如何输出这些产物
需要输出IIFE(立即调用的函数表达式)格各式资源
// rollup.config.js
const config = {
input: 'input.js',
output:{
file: 'output.js',
format: 'iife' //指定模块形式,如果输出esm的格式,则需要指定esm,输出则为vue.esm-browser.js,如果是在node环境,则是cjs,工具使用则是bundler
}
}
无论rollup还是webpack,在寻找资源时,如果package.json中存在module字段,他会优先使用module字段来代替main字段指向的资源
2.5 特性开关
在设计框架时,框架会给用户提供诸多特性(或功能),例如我们提供A,B,C三个特性给用户,同时还提供a,b,c三个对应特性的开关,用户可以社渚a,b,c为true和false代表开启或关闭对应的特性,这将带来很多好处:用户可以停放关闭的状态,任意为框架添加新的特性,以及api,我们不用担心包的体积会变大,因为there-shaking 都会通过判断去掉。
我们在rollup.js 的预定义常量插件中,定义一个常量,如同上文的__DEV__,用户通过webpack.DefinePlugin插件来控制这个状态,就像vue3 还继续保持了选项式的API,只需要将__VUE_OPTIONS_API__设置为true,则使用vue2的语法,为fasle则可以使用compostion API来编写代码,这样就可以实现兼容两个版本
2.6 错误处理
通过不断优化函数内部的错误处理,通过注册统一的错误处理函数。 可以在vue源码中搜索到 callWithErrorHandling()
2.7 ts的支持
很多人以为主要是使用ts编写的框架,就等同于对ts类型支持友好,其实是两码事,比如vue源码的runting-core/src/apidefinecomponent.ts文件,整个文件在浏览器运行的只有三行,结果写了差不多200行的代码来为类型支持服务
总结(自己根据文中的所讲的内容绘制了一个流程图):
Chapter3 vue3的设计思路
项目再大。也是存在一条核心思路,围绕着核心思路,本章就是从全局视角了解Vue3的设计思路,工作机制及重要部分展开说明
3.1 声明式的描述UI
vue是声明式的UI框架,如果是你设计,你会怎么思考呢,我们可以以这几点为出发点:
1.dom元素:例如是div标签还是span标签
2.属性:如a标签的href属性,载入id,class的通用属性
3.事件:如click,keydown
4.元素的层级结构:dom树的父子结构
我们除了可以使用模板来声明式描述UI之外,还可以用JavaScript对象来描述,使用js对象描述ui更加灵活,而vue就是用h函数来描述UI的,h函数只是用一个
const level = 3;
const title = {tag: `${level}`} //标签
//如果使用模板描述则是
/*
<h1 v-if="level === 1"></h1>
<h2 v-if="level === 2"></h2>
<h3 v-if="level === 3"></h3>
...
*/
渲染器非常重要,平时编写的vue组件都是通过渲染器来工作,这个知识点非常重要
分析render渲染器思路,总体分为三步:
1.创建元素:vnode.tag作为标签名称来创建dom元素
- 为元素添加属性和事件: 遍历vnode.props 对象,如果key以on字符开头,证明是一个事件,把on截取调调用toloweCase 函数将事件名称小写化,最后调用addEventListener 绑定事件处理函数
3.处理children:数组就递归,字符串则直创建文本节点,并创建到元素内
这个只是渲染器,但是精髓是更新节点阶段
3.2 组件的本质
组件就是一组DOM元素的封装
实现方式: 将虚拟dom的tag属性描述成组件,此时的tag属性不是标签名称,而是组件函数, mountComponent(vnode,cintainer)
3.3 模板的工作原理
无论手写虚拟DOM还是使用模板,都是声明式描述UI, 那模板是如何工作的,这里需要提到另外一个部分:编译器,编译器和渲染器是一样的,只是一段程序而已,不过工作方式不同,编译器作用就是编译为渲染函数, 将 template模板的html内容,编译成渲染函数并添加到script标签块的组件对象上