vue从template模板到真实渲染在页面上发生了什么
首先,Vue将模板(template)编译为渲染函数(render function)
过程主要分为三个核心步骤:
第一阶段:解析阶段(Parsing)
将模板字符串解析为抽象语法树(AST)。该过程通过词法分析和语法分析实现,识别HTML标签、属性、指令等元素,构建树状结构表示模板的层级关系。例如
第二阶段:优化阶段(Optimization)
对AST进行静态标记分析,识别永远不会改变的静态节点和子树。通过标记这些静态内容,Vue在后续更新过程中可以跳过它们的比对,提升虚拟DOM的patch性能
第三阶段:代码生成阶段(Code Generation)
将优化后的AST转换为可执行的JavaScript代码字符串,(嗲用一个函数生成render函数)最终生成render函数。该函数通过调用h()(即createVnode)创建虚拟节点,(也就是执行这个函数,就会像我们原生js一样)描述DOM结构,例如:
function render() {return h('div', {class:'box'}, [msg])
}
其次,执行render函数生成虚拟dom树
虚拟dom树和抽象语法树有本质上的区别,抽象语法树知识将template转为嵌套对象,其中的v-for或者双括号变量等都不会被解析,而是记录为expression:
‘_s(msg)’,text: '{{msg}}'这种未解析的内容。而VNode会将这些指令转换为具体的JavaScript逻辑,
例如节点的v-if会在render函数中被转换为if语句,不会在vnode中出现。
而节点的v-show在render函数中会被转换为vnode的display属性
也就是render做的是将一个抽象语法树进行前置处理,转变为可以用来做渲染的虚拟dom对象
为什么要用虚拟dom?
保证性能下限,在不进行手动优化的情况下,提供过得去的性能。实际上手动优化可以解决很多问题,比如多次数据修改合并这些。如果不使用虚拟dom,也不进行手动优化,则原始的vue可能在每次组件的数据更新都要重建整个组件。
TIPS!!!!!也就是vue1的数据属性的watcher是与具体dom节点进行直接绑定,因此出现了依赖关系庞大的问题,然后vue2将数据属性的watcher改为与组件进行绑定,依赖关系更加简单,但这样的话数据更新会导致整个组件更新,所以vue2同时引入了虚拟dom,通过算法比较计算出最小更新单位
虚拟DOM Diff结果转化 Vue将Diff算法计算的差异转化为原子级DOM操作指令,(最终还是转为原生js语句)主要包括:
createElement / removeChild 节点增删 setAttribute / removeAttribute 属性修改
node.textContent 文本更新 insertBefore 节点位置调整
将虚拟DOM树转换为真实DOM节点
创建基础节点:根据虚拟DOM的tag属性调用document.createElement创建真实DOM元素
设置属性:遍历虚拟DOM的props对象,通过setAttribute或直接属性赋值(如className)为真实DOM添加属性
处理子节点:递归调用转换函数处理子节点,并通过appendChild将子节点挂载到父节点
最终插入页面:最终将生成的真实DOM树通过appendChild插入到指定的容器节点(如#app)中,完成首次渲染
浏览器同步解析css
浏览器会并行解析CSS文件和内联样式,生成CSSOM(树形结构对象模型)
放弃了解是怎么生成的。
给出一个CSSOM的示例
// 单条CSS规则解析后的对象结构
const cssRuleExample = {selectorText: ".container > h1", // 选择器部分style: { // 样式声明部分"font-size": "24px","color": "#1a1a1a","margin-bottom": "20px","display": "flex"},parentRule: null, // 所属父规则(媒体查询等)cssText: `.container > h1 { // 原始CSS文本font-size: 24px;color: #1a1a1a;margin-bottom: 20px;display: flex;}`
};// 浏览器实际CSSOM中的样式表结构
const styleSheetExample = {cssRules: [cssRuleExample], // 规则集合ownerNode: document.querySelector("style"), // 所属DOM节点disabled: false // 是否禁用
};
将CSSOM与真实dom
当节点插入文档流之后,才会触发渲染流程,如果是vue的话相当于页面下载完html之后,解析完css之后,浏览器才会组合DOM和CSSOM生成渲染树
渲染树生成后如何渲染到页面
1. 布局(Layout/Reflow)
浏览器根据渲染树计算每个节点的几何属性(位置、尺寸、边距等),确定元素在屏幕上的精确坐标。
例如:div的宽度、h1的字体大小、margin的偏移量等。
此阶段会触发重排(Reflow),修改布局属性(如width、height)会强制重新计算整个或部分渲染树
2. 分层(Layerization)
为提高渲染效率,浏览器将渲染树拆分为多个图层(Layers),每个图层独立处理:
需要动画的元素、will-change属性、transform等会提升为独立图层
分层后,浏览器可仅重绘受影响的图层,避免全局重绘
3. 绘制(Paint)
浏览器将每个渲染节点转换为像素,填充到对应图层中:
绘制顺序由渲染树的层级决定(如z-index高的元素后绘制)
此阶段仅处理颜色、边框等视觉属性,不涉及布局计算
4. 合成(Composite)
最终,浏览器将各图层合并为完整页面:
合成线程将图层切分为小块(Tile),优先处理可视区域内容
GPU加速合成,通过transform等属性避免重排,提升性能
Tips:虚拟DOM转为真实DOM的过程主要由JavaScript引擎进程完成,而CSS解析确实是由GUI渲染进程处理的
数据改变以后vue做了什么
数据改变后,重新执行render函数,(render函数一次生成多次执行,结果不同是因为传递的参数即template字符串和上下文参数不同)生成新的虚拟dom树,
新的虚拟dom与旧的虚拟dom进行对比!!!!!!!!【待办,后续总结】
Diff算法是虚拟DOM技术的核心,用于高效比较新旧虚拟DOM树的差异并最小化真实DOM操作
第一步:diff算法的入口函数:patch函数,主要判断新旧节点是不是同一个节点,然后交由不同的逻辑进行处理。
export default function patch(oldVnode, newVnode) {// 判断传入的第一个参数,是DOM节点还是虚拟节点if (oldVnode.sel === '' || oldVnode.sel === undefined) {// 传入的第一个参数是DOM节点,此时要包装成虚拟节点oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);}// 判断oldVnode和newVnode是不是同一个节点if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {//是同一个节点,则进行精细化比较patchVnode(oldVnode, newVnode);}else {// 不是同一个节点,暴力插入新的,删除旧的let newVnodeElm = createElement(newVnode);// 将新节点插入到老节点之前if (oldVnode.elm.parentNode && newVnodeElm) {oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);}// 删除老节点oldVnode.elm.parentNode.removeChild(oldVnode.elm);}
}
示例:
是的,AST(抽象语法树)确实是一个树状结构,可以多层嵌套表示模板的层级关系。以下是一个三层模板示例及其对应的AST对象、静态标记和最终生成的render函数:
三层模板示例:
<div class="container"><p>{{ message }}</p><ul><li v-for="item in items" :key="item.id">{{ item.text }}</li></ul>
</div>
对应的AST对象:
{type: 1,tag: "div",attrs: [{ name: "class", value: "container" }],children: [{type: 1,tag: "p",children: [{ type: 2, expression: "_s(message)", text: "{{ message }}" }]},{type: 1,tag: "ul",for: "items",alias: "item",key: "item.id",children: [{type: 1,tag: "li",children: [{ type: 2, expression: "_s(item.text)" }]}]}]
}
静态标记过程
div和ul标记为动态(含动态子节点)
p标记为动态(含插值表达式)
li标记为动态(v-for循环项)
class="container"标记为静态属性
生成的render函数
function render(_ctx) {return _openBlock(), _createBlock("div", { class: "container" }, [_createVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),_createVNode("ul", null, _ctx.items.map(item => _createVNode("li", { key: item.id }, _toDisplayString(item.text), 1 /* TEXT */)))])
}
执行render函数生成的虚拟dom树
{tag: "div",props: { class: "container" },children: [{ tag: "p", children: "Hello" }, // 假设message初始值为"Hello"{tag: "ul",children: [{ tag: "li", key: 1, children: "Item1" },{ tag: "li", key: 2, children: "Item2" }]}]
}
数据更新后的新虚拟DOM树
(假设message变为"World",items新增一项)
{tag: "div",props: { class: "container" },children: [{ tag: "p", children: "World" }, // 更新的文本{tag: "ul",children: [{ tag: "li", key: 1, children: "Item1" }, // 复用{ tag: "li", key: 2, children: "Item2" }, // 复用{ tag: "li", key: 3, children: "Item3" } // 新增]}]
}
Diff算法比对结果
p节点:检测到文本变化("Hello" → "World")
ul节点:通过key比对发现新增第三个li子项
div和ul容器节点:标记为可复用
视图更新结果
// 更新文本节点
pElement.textContent = "World"; // 新增li元素
const newLi = document.createElement('li');
newLi.textContent = "Item3";
ulElement.appendChild(newLi);