Vue是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。
权衡的艺术
命令式和声明式
命令式:代码严格按照顺序,从上往下,按照每一次的命令执行,重过程声明式:代码已经内部封装好了,我们可直接调用,重结果
//01 - 获取 id 为 app 的 div 标签
//02 - 它的文本内容为 hello world
//03 - 为其绑定点击事件
//04 - 当点击时弹出提示:ok
//命令式
const div = document.querySelector('#app') // 获取 div
div.innerText = 'hello world' // 设置文本内容
div.addEventListener('click', () => { alert('ok') }) // 绑定点击事件
// 声明式
<div @click="() => alert('ok')">hello world</div>
声明式的代码性能不优于命令式的代码性能
当我们需要修改其中的某一个字段,hello world-> hello vue3
命令式:直接可以再对应的命令中进行修改
div.innerText = 'hello vue3'
声明式:需要先找到更新的位置,再更新修改部分
<div @click="() => alert('ok')">hello vue3</div>
所以vue的声明式就多个查找的过程。但如果有许多的dom的操作,命令式就需要把所有的的代码操作都罗列出来,不易维护。而为了把性能损失降到最小,就是使用到了虚拟dom
虚拟dom的性能
虚拟DOM是通过对象渲染创建所有DOM元素innerHTML是通过渲染HTML字符串创建所有DOM元素- 可以通过表格的方式更直观的对比出
虚拟DOM和innerHTML的创建页面性能
| 虚拟DOM | innerHTML | |
|---|---|---|
| js创建方式 | 创建javaScript对象 | 渲染HTML字符串 |
| DOM渲染创建 | 创建所有DOM元素 | 创建所有DOM元素 |
| 更新DOM | 创建新的js对象,对比新旧对象,更新差异 | 销毁所有DOM元素,再重新创建 |
| 性能影响因素 | 与数据的变化量相关(数据变化的越多,进行计算的越多) | 与模板的大小相关(页面DOM越多,进行销毁,再创建性能越差) |
运行时和编译时
纯运行时: 直接通过虚拟dom树的render函数操作创建、更新和删除dom- 没有编译过程,每次都需要手写dom树形结构,不能直观的表达html标签
纯编译时:将HTML标签编译成js的dom形式进行操作,纯命令式- 有失灵活性,只能根据提供的内容,编译之后才能使用
运行时+编译时:把HTML标签先编译成虚拟dom树,再通过render函数操作创建、更新和删除dom- 尽管会对性能产生一定的开销,但是能更直观,更灵活的表达
综合一下虚拟dom的通过可维护性,性能,开发(创建,更新,修改,删除dom元素)这些方面,相对而言是最优的选择
框架设计的核心要素
提升用户体验
- 对一个框架来说,对开发者有更好的体验是重中之重。
createApp(App).mount('#not-exist')
当我们创建一个vue,挂载到一个不存在的DOM节点时,就会在控制台收到一条警告信息,表示无法找到相对应的DOM元素
- 友好的警告信息不仅能够帮助用户快速定位问题,还能节省用户的时间
控制框架代码体积
控制代码的体积。在实现同样功能的情况下,当然是用的代码越少越好,这样体积就会越小,最后浏览器加载资源的时间也就越少,而需要良好的开发体验,就需要完善的警告信息,这需要编写大量的警告代码,,这不是与控制代码体积相悖吗?没错,所以我们要想办法解决这个问题
- Vue.js 在输出资源的时候,会输出两个版本,其中一个用于开发环境,如
vue.global.js,另一个用于生产环境,如vue.global.prod.js - 当把
__DEV__ = true时,下面这段代码在开发环境中就存在,反之,生产环境就不存在,这段永远不会 执行的代码称为dead code,它不会出现在最终产物中,在构建资源的时候就会被移除 - 这样在开发环境中为用户提供友好的警告信息的同时,不会增加生产环境代码的体积
if (__DEV__ && !res) {
warn(
`Failed to mount app: mount target selector "${container}"turned null.`
)
}
良好的Tree-Shaking支持
在vue内部有许多的组件,如果在我们所创建的项目中没有使用一些组件,那就在打包的时候就不需要,那就用到了Tree-Shaking
- Tree-Shaking 指的就是消除那些永远不会被执行的代码,也就是排除 dead code,现在无论是 rollup.js 还是 webpack,都支持Tree-Shaking
- 想要实现
Tree-Shaking,必须满足一个条件,即模块必须是ESM(ES Module),因为Tree-Shaking依赖 ESM 的静态结构
特性开关
可以灵活选择使用某些特性,对于用户关闭的特性,我们可以利用Tree-Shaking机制让其不包含在最终的资源中
- 在一个v3的项目中,如果不需要v2的特性,就可以配置一个特性开关,v2这部分代码就不会包含在最终的资源中,从而减小资源体积
// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: JSON.stringify(false) // 关闭特性
})
vue.js3的设计思路
vue.js通过编译器将模板编译成渲染函数(虚拟DOM),再通过渲染器将渲染函数(虚拟DOM)渲染成真实DOM
初识渲染器
渲染器是将虚拟DOM渲染成真实DOM
// 虚拟dom
let obj = {
tag: "div",
props: {
onclick: () => alert("hello"),
},
children: "click me",
};
// 渲染函数,一个简单的dom渲染器
function render(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(
// toLowerCase()将字母都变成小写
key.substr(2).toLowerCase(), // 事件名称
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);
}
// 执行渲染函数
render(obj, document.body);
编译器
编译器是将模板编译成渲染函数,也就是一个虚拟DOM
// 模板
<div @click="handler">click me</div>
// 编译后的渲染函数
render() {
return h('div', { onClick: handler }, 'click me')
}
组件就是一组 DOM 元素的封装,通过编译器编译成一个渲染函数
所以,无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程