Vue运行机制
# Vue运行机制
面试题:介绍一下 Vue3 内部的运行机制是怎样的?
Vue3 整体可以分为几大核心模块:
- 响应式系统
- 编译器
- 渲染器
如何描述UI
思考🤔:UI涉及到的信息有哪些?
- DOM元素
- 属性
- 事件
- 元素的层次结构
思考🤔:如何在 JS 中描述这些信息?
考虑使用对象来描述上面的信息
<h1 id='title' @click=handler><span>hello</span></h1>
const obj = {
tag: 'h1',
props: {
id: 'title',
onClick: handler
},
children: [
{
tag: 'span',
children: 'hello'
}
]
}
虽然这种方式能够描述出来 UI,但是非常麻烦,因此 Vue 提供了模板的方式。
用户书写模板----> 编译器 ----> 渲染函数 ----> 渲染函数执行得到上面的 JS 对象(虚拟DOM)
虽然大多数时候,模板比 JS 对象更加直观,但是偶尔有一些场景,JS 的方式更加灵活
<h1 v-if="level === 1"></h1>
<h2 v-else-if="level === 2"></h2>
<h3 v-else-if="level === 3"></h3>
<h4 v-else-if="level === 4"></h4>
<h5 v-else-if="level === 5"></h5>
<h6 v-else-if="level === 6"></h6>
let level = 1;
const title = {
tag: `h${level}`
}
编译器
主要负责将开发者所书写的模板转换为渲染函数。例如:
<template>
<div>
<h1 :id="someId">Hello</h1>
</div>
</template>
编译后的结果为:
function render(){
return h('div', [
h('h1', {id: someId}, 'Hello')
])
}
执行渲染函数,就会得到 JS 对象形式的 UI 表达。
整体来讲,整个编译过程如下图所示:
可以看到,在编译器的内部,实际上又分为了:
- 解析器:负责将模板解析为对应的模板 AST(抽象语法树)
- 转换器:负责将模板AST转换为 JS AST
- 生成器:将 JS AST 生成对应的 JS 代码(渲染函数)
Vue3 的编译器,除了最基本的编译以外,还做了很多的优化:
- 静态提升
- 预字符串化
- 缓存事件处理函数
- Block Tree
- PatchFlag
渲染器
执行渲染函数得到的就是虚拟 DOM,也就是像这样的 JS 对象,里面包含着 UI 的描述信息
<div>点击</div>
const vnode = {
tag: 'div',
props: {
onClick: ()=> alert('hello')
},
children: '点击'
}
渲染器拿到这个虚拟 DOM 后,就会将其转换为真实的 DOM
一个简易版渲染器的实现思路:
- 创建元素
- 为元素添加属性和事件
- 处理children
function renderer(vnode, container){
// 1. 创建元素
const el = document.createElement(vnode.tag);
// 2. 遍历 props,为元素添加属性
for (const key in vnode.props) {
if (/^on/.test(key)) {
// 如果 key 以 on 开头,说明它是事件
el.addEventListener(
key.substr(2).toLowerCase(), // 事件名称 onClick --->click
vnode.props[key] // 事件处理函数
);
}
}
// 3. 处理children
if(typeof vnode.children === 'string'){
el.appendChild(document.createTextNode(vnode.children))
} else if(Array.isArray(vnode.children)) {
// 递归的调用 renderer
vnode.children.forEach(child => renderer(child, el))
}
container.appendChild(el)
}
组件的本质
组件本质就是一组 DOM 元素的封装。
假设函数代表一个组件:
// 这个函数就可以当作是一个组件
const MyComponent = function () {
return {
tag: "div",
props: {
onClick: () => alert("hello"),
},
children: "click me",
};
};
vnode 的 tag 就不再局限于 html 元素,而是可以写作这个函数名:
const vnode = {
tag: MyComponent
}
渲染器需要新增针对这种 tag 类型的处理:
function renderer(vnode, container) {
if (typeof vnode.tag === "string") {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container);
} else if (typeof vnode.tag === "function") {
// 说明 vnode 描述的是组件
mountComponent(vnode, container);
}
}
组件也可以使用对象的形式:
const MyComponent = {
render(){
return {
tag: "div",
props: {
onClick: () => alert("hello"),
},
children: "click me",
};
}
}
function renderer(vnode, container) {
if (typeof vnode.tag === "string") {
// 说明 vnode 描述的是标签元素
mountElement(vnode, container);
} else if (typeof vnode.tag === "object") {
// 说明 vnode 描述的是组件
mountComponent(vnode, container);
}
}
响应式系统
总结:当模板编译成的渲染函数执行时,渲染函数内部用到的响应式数据会和渲染函数本身构成依赖关系,之后只要响应式数据发生变化,渲染函数就会重新执行。
面试题:介绍一下 Vue3 内部的运行机制是怎样的?
参考答案:
Vue3 是一个声明式的框架。声明式的好处在于,它直接描述结果,用户不需要关注过程。Vue.js 采用模板的方式来描述 UI,但它同样支持使用虚拟 DOM 来描述 UI。虚拟 DOM 要比模板更加灵活,但模板要比虚拟 DOM 更加直观。
当用户使用模板来描述 UI 的时候,内部的 编译器 会将其编译为渲染函数,渲染函数执行后能够确定响应式数据和渲染函数之间的依赖关系,之后响应式数据一变化,渲染函数就会重新执行。
渲染函数执行的结果是得到虚拟 DOM,之后就需要 渲染器 来将虚拟 DOM 对象渲染为真实 DOM 元素。它的工作原理是,递归地遍历虚拟 DOM 对象,并调用原生 DOM API 来完成真实 DOM 的创建。渲染器的精髓在于后续的更新,它会通过 Diff 算法找出变更点,并且只会更新需要更新的内容。
编译器、渲染器、响应式系统都是 Vue 内部的核心模块,它们共同构成一个有机的整体,不同模块之间互相配合,进一步提升框架性能。
-EOF-