1. vue3 整体大纲
1. vue3 整体大纲
霍春阳《Vue.js 设计与实现》的笔记
一、vue 整体方向的选择(权衡)
1. 声明式 or 命令式
命令式(关注过程)
const div = document.querySelector('#app') // 获取 div
div.innerText ='hello world' // 设置文本内容
div.addEventlistener('click',() =>[ alert('ok') )) // 绑定点击事件
声明式(关注结果)
<div @click="() => alert('ok')">hello world</div>
这部分的选择主要基于两个方面:性能和可维护性。
性能 | 可维护性 | |
---|---|---|
对比 | 命令式 > 声明式 | 声明式 >> 命令式 |
- 因为声明式在命令式性能消耗的基础上多出了diff 的性能消耗,所以在性能上会差一些
- 但随之带来的是可维护性的巨大提升
Vue 封装命令式的代码,实现面向用户的声明式
2. 虚拟 dom 的选择
上面提到声明式带来的额外性能消耗——diff 的性能消耗,虚拟 dom 便是为了最小化此消耗采取的措施。其中虚拟 dom 是一个普通的 JS 对象,用来表示对真实 dom 的描述。
const obj = {
tag: 'div',
children: [{ tag: 'span', children: 'hello world' }],
};
3. 运行时 or 编译时
- 运行时
- 利用
render
函数,直接把 虚拟 DOM 转化为 真实 DOM 元素 - 没有编译过程,无法分析用户提供的内容
- 需要我们手写
render
函数(😖)
- 利用
- 编译时
- 直接把 template 模板 中的内容,转化为 真实 DOM 元素
- 可以分析用户提供的内容,理论上性能会更好,但是有损灵活性
- 运行时编译(Vue 选用)
- 先把 template 模板 转化为 render 函数
- 再利用 render 函数,把 虚拟 DOM 转化为 真实 DOM
二、 框架设计的核心要素
提升用户的开发体验
- 引入友好的警告信息,帮助开发者里快速定位问题
- 警告信息详细导致框架体积增大,通过判断所处环境来决定构建的版本是否包含这些警告信息
引入
Tree-Shaking
机制- 配合预定义变量,排除 dead code,缩小打包体积
根据应用环境的不同输出不同的构建产物( 多版本)
提供功能的特性开关,让用户自由选择是否使用
提供统一的错误处理接口:
callWithErrorHandling
提供良好的 TS 类型支持
三、Vue3 的设计思路
1. 描述 UI
Vue3 中支持两种方式来描述 UI。第一种是 模板,即我们常在 template 标签中书写的内容:
<h1 @click="handler"><span></span></h1>
- 这种方式 vue 最终还是会将其编译为
render
函数(通过编译器)
另外一种是直接使用 js 对象来描述 UI,即所谓的 虚拟 DOM。
import { h } from 'vue';
export default {
render() {
return h('h1', { onClick: handler }); // 虚拟 DOM
},
};
render
函数运行返回一个虚拟 DOM 对象- 其中 h 为辅助创建虚拟 DOM 的工具函数
2. 渲染器
渲染器的作用就是 把虚拟 DOM 渲染为真实 DOM,以下是一个不考虑更新节点的简易版本
function renderer(vnode, container) {
// 使用 vnode.tag 作为标签名称创建 DOM 元素
const el = document.createElement(vnode.tag);
// 遍历 vnode.props,将属性、事件添加到 DOM 元素
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果 key 以 on 开头,说明它是事件
el.addEventListener(
key.substr(2).toLowerCase(), // 事件名称 onClick --->click
vnode.props[key] // 事件处理函数
);
}
}
// 处理 children
if (typeof vnode.children === 'string') {
// 如果 children 是字符串,说明它是元素的文本子节点
el.appendChild(document.createTextNode(vnode.children));
} else if (Array.isArray(vnode.children)) {
// 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
vnode.children.forEach((child) => renderer(child, el));
}
// 将元素添加到挂载点下
container.appendChild(el);
}
3. 组件
组件就是一组 DOM 元素的封装,这组 DOM 元素就是组件要渲染的内容。而虚拟 DOM 不光可以用来描述真实 DOM,还可以用来描述组件,只是其中tag
变成了组件。
const vnode = {
tag: MyComponent,
};
//MyComponent是一个对象
const MyComponent = {
render() {
return {
tag: 'div',
props: {
onclick: () => alert('hello'),
},
children: 'click me',
};
},
};
//MyComponent是一个函数
const MyComponent = function () {
return {
tag: 'div',
props: {
onClick: () => alert('hello'),
},
children: 'click me',
};
};
MyComponent
可以是函数,可以是对象,只要最终返回的是一组虚拟 dom 即可。我们根据MyComponent
的类型对渲染器进行相应的修改
function renderer(vnode, container) {
if (typeof vnode.tag === 'string') {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container);
} else if (typeof vnode.tag === 'function') {
//或者=== 'Object'
// 说明 vnode 描述的是组件
mountComponent(vnode, container);
}
}
其中 mountElement 即上述渲染器的部分,而新加的处理组件的函数mountComponent
,大致流程为:
function mountComponent(vnode, container) {
// 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
const subtree = vnode.tag(); //vnode.tag.render()
// 递归地调用 renderer 渲染 subtree
renderer(subtree, container);
}
4. 编译器
编译器的作用是 将模板编译为渲染函数,对于编译器来说,模板就是一个普通的字符串,它会分析该字符 串并生成一个功能与之相同的渲染函数
<template>
<div @click="handler">click me</div>
</template>
<script>
export default {
data() {
/* ... */
},
methods: {
handler: () => {
/* ... */
},
},
};
</script>
<template>
标签里的内容就是模板内容,编译器会把模板内容 编译成渲染函数并添加到 <script>
标签块的组件对象上,所以最终在浏览器里运行的代码就是:
export default {
data() {
/* ... */
},
methods: {
handler: () => {
/* ... */
},
},
render() {
return h('div', { onClick: handler }, 'click me');
},
};