CH_1权衡的艺术
当页面修改
假设HTML模板中有这么一段内容: <div>hello world</div>,我们需要把它改为<div>hello vue3</div>
原生javascript
原生js可以通过手动创建、增加和修改dom元素,如执行div.textContent = 'hello vue3'时候,页面不需要重新渲染,而是直接获取到对应的DOM元素,再进行更改。
innerHTML
- innerHTML 不会比较差异,它只是简单地用新的 HTML 字符串替换掉原有的内容
- 解析 HTML 字符串:浏览器会将传入的 HTML 字符串解析为一系列标记(tokens),包括开始标签、结束标签、属性、文本内容等。
- 创建元素节点:根据开始标签,浏览器会创建相应的元素节点,并设置其属性值。
- 构建父子关系:根据开始标签和结束标签之间的嵌套关系,浏览器会构建出 DOM 树的层次结构,并确定元素节点之间的父子关系
虚拟DOM
- 解析HTML元素并且创造相应DOM元素,是浏览器完成的
- 创建和销毁DOM元素,性能开销比js大得多
- innerHTML:解析HTML字符串->(销毁所有旧DOM)->创建新DOM
- 虚拟DOM引入后:创建新的js对象->(找出最小化差异)-> 必要的DOM更新
虚拟DOM相比于innerHTML,在解析模板字符串时,加入了Diff算法这一js步骤,来代替原本的销毁和创建DOM元素的过程。
总结
- 性能:原生js > 虚拟DOM > innerHTML
- 心智负担:虚拟DOM < innerHTML < 原生js
* 运行时和编译时
- 编译时:代码经过编译之后,直接就可以执行,生成的程序将独立于特定框架。例如,将HTML代码转化为命令式代码。
- 运行时:可以跨平台执行。在程序运行时,动态地依赖框架,生成与运行环境相匹配的可执行程序。
- 运行时+编译时:既支持在运行时,用户提供数据对象而无需编译;又支持在编译时,用户提供字符串,将其编译成数据对象再交给运行时处理(后者其实时运行时编译)。也可在构建时编译好,运行时无需编译。
CH_2 框架设计的核心要素
用户友好:提供警告信息
- 利用
console.warn提供必要且清晰的警告信息 - 利用
custom formatter自定义的数据格式化功能
- 在Vue中,有内置的格式化规则来展示响应式数据。但通过自定义的方式,可以更清晰地对数据对象进行展示,或者隐藏敏感信息。如:
import { enableCustomFormatter } from 'vue';
// 启用自定义格式化
enableCustomFormatter((value) => {
if (typeof value === 'string') {
// 将字符串转为大写
return value.toUpperCase();
}
return value;
});
可以用于将所有的字符串类型的响应式数据在开发工具中展示为大写形式。但是,仅仅在开发环境中有效。
控制框架代码的体积
- warn函数通常配合DEV的常量检查,在开发环境中,DEV的值为true,warn代码被打包;在生产环境中,DEV值为false,warn代码不被打包。具体而言,rollup.js的插件和DefinePlugin一样,在构建过程中,修改常量或者变量为实际的值。
- 使用treeShaking,将不需要的dead code进行敲除。treeshaking只适合ESM这种静态导入模块构建的形式,不适合commonJS这种运行时动态导入的。
构建产物
当在Vue项目中构建产物时,生成一些编译后的 JavaScript、CSS 和其他资源文件,这些构建产物可以用作其他 Vue 项目的依赖。在新的Vue项目中,可以采用三种方式引用构建产物:
-
通过script标签
-
通过npm包
// 通过 npm 或 yarn 来安装它,然后在代码中通过导入语句来使用它。 import SomeComponent from 'your-package-name'; -
通过CDN
为了在项目中引入Vue框架,可以利用不同的构建产物:
1.1 引入方式:script , 构建方式:IIFE
<script src="/path/to/vue.js"></script>
想要在script标签通过这种方式引入vue.js,vue的构建产物必须是一个立即执行函数,因此在rollup.js中,配置 format: 'iife'
1.2 引入方式:script , 构建方式:esm
<script type='module' src="/path/to/vue.esm-browser.js"></script>
在rollup.js中,配置 format: 'esm'。
带有browser字样的js文件,是给 <script type='module'> 使用的
1.3 引入方式:npm , 构建方式:webpack
{
"main" : "index.js",
"module" : "dist/vue.runtime.esm-bundler.js"
}
带有bundler的是给webpack打包工具使用的,打包工具会根据package.json选项,选择适合项目的js文件进行打包(nodeJS使用main作为入口文件,esm使用module)。作用是根据webpack配置动态设置 DEV 的值。
1.4 引入方式:require , 构建方式:cjs
const Vue = require('vue')
在rollup.js中,配置 format: 'cjs'。有时需要在NodeJS中对代码进行服务器端渲染。
特性开关
和_DEV_一样,Vue源码中有很多判断条件。如果满足条件则保留代码,不保留则进行treeshaking减少代码体积。这些常量使用webpack的DefinePlugin插件进行替换。例如, VUE_OPTIONS_API 字段,就是用于决定是否保留Vue2的选项式API。如果关闭特性,最终打包资源会对相应的代码进行敲除。
CH_3 Vue3的设计思路
声明式描述UI
- 描述DOM元素:与HTML描述方式一致,div / a标签
- 描述属性:与HTML描述方式一致 :id='app'
- 动态绑定属性:使用v-bind或者:
- 绑定事件:v-on或者@
- 描述层级:和HTML一样
h函数、render函数
- h函数:描述单个虚拟节点,返回值是虚拟节点。
- render函数:描述层级结构、嵌套关系,可包含多个h函数创建的虚拟节点;返回值是渲染最终组件的虚拟DOM树。
// h函数
import { h } from 'vue'
export default {
render(){
return h('h1', { onClick: handler })
}
}
// render渲染器
export default {
render(){
return {
tag:'h1',
props: { onClick: handler }
}
}
}
渲染器
对于<div>click me</div>,其虚拟DOM描述如下:
const vnode = {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
其中,click me是字符串类型值,代表文本子节点。
对于这样一个结构,renderer函数接受两个参数:
- vnode:虚拟DOM对象
- container:真实DOM元素作为挂载点
做了三件事:
- vnode.tag作为名称创建实际DOM元素
- props中以on开头是事件,为DOM元素添加addEventListener
- children如果是字符串,创建文本节点;如果是数组,递归创建
组件的本质
是一组DOM元素的封装。封装方式:
1. 函数形式:
const MyComponent = function() {
return {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
使用虚拟DOM描述组件:
const vnode = {
tag: MyComponent
}
那么此时,对应的renderer函数也要进行改变:
function renderer(vnode, container) {
if(typeof vnode.tag === 'string') {
mountElement(vnode, container)
} else if (typeof vnode.tag === 'function') {
mountComponent(vnode, container)
}
}
对于mountComponent,也直接调用mountElement即可:
const mountComponent(vnode, container) = {
const subtree = vnode.tag()
renderer(subtree, container)
}
2. 对象形式
const MyComponent = {
render() {
tag: 'div',
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
模板的工作原理
- 编译器根据模版生成渲染函数
render() {
return h('div', { onClick: handler }, 'click me')
}
- 编译器将渲染函数挂载到组件对象上
export defult() {
data(),
methods:{},
render() {
return h('div', { onClick: handler }, 'click me')
}
}
- renderer函数根据render进行渲染
注:
renderer并不仅仅用于根据render函数的返回值进行渲染,它还涵盖了处理组件的其他标签(如data、methods、computed等)以及整个组件的实例化和渲染过程。
Vue.js是各个模块组成的有机整体
编译器和渲染器之间存在着交流,编译器在编译过程中,能识别哪些是可能会变更的动态属性,方便渲染器找到变更点。