从高层设计的角度去探讨框架需要关注的问题。
参考:速读《Vue.js 设计与实现》 - 掘金 (juejin.cn)
系列目录:
标题 | 博客 |
---|---|
第一篇:框架设计概览 | 【Vue.js设计与实现】第一篇:框架设计概览-阅读笔记 |
第二篇:响应系统 | 【Vue.js设计与实现】第二篇:响应系统-阅读笔记 |
第三篇:渲染器 | 【Vue.js设计与实现】第三篇:渲染器-阅读笔记 |
第四篇:组件化 | 【Vue.js设计与实现】第四篇:组件化-阅读笔记 |
第五篇:编译器 | 【Vue.js设计与实现】第五篇:编译器-阅读笔记 |
第六篇:服务端渲染 | 【Vue.js设计与实现】第六篇:服务端渲染-阅读笔记 |
第一篇:框架设计概览
- 第 1 章 权衡的艺术
- 第 2 章 框架设计的核心要素
- 第 3 章 Vue.js 3 的设计思路
文章目录
- 第一章 权衡的艺术
- 1.1 命令式和声明式
- 1.2 性能与可维护性的权衡
- 1.3 运行时和编译时
- 第二章:框架设计的核心要素
- 2.1 `__DEV__`:在开发环境中为用户提供友好的警告信息的同时,不会增加生产环境代码的体积
- 2.2 ` /*#__PURE__*/`与Tree Shaking
- 2.3 框架应该输出怎样的构建产物
- 2.4 错误处理
- 2.5 良好的TypeScript类型支持
- 总结
- 第三章:Vue.js 3 的设计思路
- 3.1 声明式地描述UI
- 3.2 初识渲染器
- 3.3 组件的本质
- 3.4 模板的工作原理
- 3.5 Vue.js 是各个模块组成的有机整体
- 总结
第一章 权衡的艺术
框架的设计,本身就是一种权衡的艺术。
1.1 命令式和声明式
命令式关注过程,声明式关注结果
命令式:
const div=document.querySelector('#app') //获取div div.innerText='hello world' //设置文本内容 div.addElementListener('click',()=>{alert('OK')}) //绑定事件
声明式:
alert('OK')">Hello worldvue的内部是命令式的,而我们使用vue的时候是声明式的。
即,vue封装了命令式的过程,对外暴露出了声明式的结果。
1.2 性能与可维护性的权衡
从性能的角度上看,命令式的性能>声明式的性能。
原因:
命令式的代码通过原生的JS实现,声明式的代码要实现同样功能时,还要再调用相同的命令式代码。
声明式代码的更新性能消耗 = 找出差异的性能消耗+直接修改的性能消耗.
所谓的虚拟 DOM,就是为了最小化找出差异这一步的性能消耗而出现的。
显然命令式的性能更高。此时vue还要对外暴露出声明式的接口,原因是声明式的可维护性,远大于命令式的可维护性
而且,vue在性能优化之下,它并不会比纯命令式的性能差太多。
在前端领域,用JS修改HTML的方式主要有3种:原生JS,innerHTML,虚拟DOM。
心智负担:虚拟DOM < innerHTML< 原生JS
性能:innerHTML < 虚拟DOM < 原生JS
可维护性:原生JS < innerHTML < 虚拟DOM
虚拟DOM的性能并不是最高的,但是vue依然选择虚拟DOM来进行渲染层的构架。
这也是性能与可维护性的权衡。
1.3 运行时和编译时
都是框架设计的一种方式,可单独出现,也可组合使用。
- 运行时:runtime
利用render函数,把虚拟DOM转化为真实DOM的方式。
- 编译时:compiler
把template模板中的内容,转化为真实DOM。
注意,存在编译过程,可以分析用户提供的内容。同时,没有运行时理论上性能会更好。
- 运行时+编译时
过程分两步:
- 先把template模板转化成render函数,即编译时
- 再利用render函数,把虚拟DOM转化为真实DOM,即运行时
两者的结合,可以:
- 在编译时:分析用户提供的内容
- 在运行时,提供足够的灵活性
这也是vue的主要实现方式。
第二章:框架设计的核心要素
2.1 __DEV__:在开发环境中为用户提供友好的警告信息的同时,不会增加生产环境代码的体积
有一个常量__DEV__,存在于所有的console.warn中:
if (__DEV__ && !res) { warn( `Failed to mount app: mount target selector "${container}" returned null.` ); }
在开发环境中__DEV__永远为true,在生产环境中__DEV__永远为false。永远不会执行的代码成为dead code,不会出现在最终产物中。
2.2 /*#__PURE__*/与Tree Shaking
Tree Shaking:消除那些永远不会被执行的代码。
想要实现Tree Shaking,模块必须是ESM。
Tree Shaking的关键点:副作用。如果一个函数调用会产生副作用,那么就不能将其移除。副作用就是,当调用函数的时候会对外部产生影响,例如修改了全局变量。
举个例子:
如果 obj 对象是一个通过 Proxy 创建的代理对象,那么当我们读取对象属性时,就会触发代理对象的 get 夹子(trap),在 get 夹子中是可能产生副作用的,例如我们在 get 夹子中修改了某个全局变量。而到底会不会产生副作用,只有代码真正运行的时候才能知道,JavaScript 本身是动态语言,因此想要静态地分析哪些代码是 dead code 很有难度
静态地分析 JavaScript 代码很困难,所以像 rollup.js 这类工具都会提供一个机制,让我们能明确地告诉 rollup.js:这段代码没有副作用,可以移除。
import {foo} from './utils' /*#__PURE__*/ foo()
注释代码/*#__PURE__*/,就是告诉rollup.js,对于foo的调用不会产生副作用,可以Tree-Shaking。
实际上,通常产生副作用的代码都是模块内函数的顶级调用。
foo() // 顶级调用 function bar(){ foo() // 函数内调用 }
可以看到,对于顶级调用来说,是可能产生副作用的;对于函数内调用来说,只要函数 bar 没有调用,那么 foo 函数的调用自然不会产生副作用。
因此,在 Vue.js 3 的源码中,基本都是在一些顶级调用的函数上使用 /*#__PURE__*/ 注释。此注释也可以应用在语句上。
2.3 框架应该输出怎样的构建产物
用户能够使用
ESM格式的资源中,文件会有一个-browser字样。其实对于ESM格式的资源来说,Vue.js还会输出一个vue.esm-bundler.js 文件。这样做的原因是:在寻找资源时,如果package.json中存在module字段,那么会优先使用module字段指向的资源来代替main字段指向的资源。
如Vue.js源码中的packages/vue/package.json:
{ "main":"index.js", "module":"dist/vue.runtime.esm-bundler.js" }
带有 -bundler 字样的 ESM 资源是给 rollup.js 或 webpack 等打包工具使用的,而带有 -browser 字样的 ESM 资源是直接给
其中 里的内容就是模板内容,编译器会把模板内容编译成渲染函数并添加到
- 运行时+编译时
- 编译时:compiler
- 运行时:runtime