VUE 学习笔记(三) Vue 渲染流程详解

news/2024/10/18 16:52:21/

在 Vue 里渲染一块内容,会有以下步骤及流程:

第一步,解析语法,生成AST

第二步,根据AST结果,完成data数据初始化

第三步,根据AST结果和DATA数据绑定情况,生成虚拟DOM

第四步,将虚拟DOM 生成真正的DOM插入到页面中,进行页面渲染。


那怎么理解这个流程呢?


一、解析语法生成AST


AST 语法树,实际就是抽象语法树(Abstract Syntax Tree),是指通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。

DOM 结构树,也是AST中的一种,把HTML DOM语法解析并生成最终页面。


我们详细看看这个过程:


1、捕获语法

在生成AST的过程中,会涉及到编译器的原理, 会经过以下过程:


(1)、语法分析


语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语。如 :程序、语句、表达式等。语法分析程序判断源程序在结构上是否正确, 如 v-if` / v-for 这样的指令 ,也有``这样的自定义 DOM 标签,还有`click`/`props 这样的简化绑定语法。需要将它们一一解析出来,并相应地进行后续处理。

(2)、语义分析


语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息,一般类型检查也会在这个过程中进行。如我们绑定了某个不存在的变量或者事件,又或者是使用了某个未定义的自定义组件等,都会在这个阶段进行报错提示。


(3) 、生成 AST


在Vue 里,语法分析、语义分析基本上是通过正则的方式来处理,生成 AST其实就是将解析出来的元素、指令、属性、父子节点关系等内容进行处理,得到一个 AST 对象,以下是简化后的源码:


/***  HTML编译成AST对象*/
export function parse(template: string,options: CompilerOptions
): ASTElement | void 
{// 返回AST对象// 篇幅原因,一些前置定义省略// 此处开始解析HTML模板parseHTML(template, {expectHTML: options.expectHTML,isUnaryTag: options.isUnaryTag,shouldDecodeNewlines: options.shouldDecodeNewlines,start(tag, attrs, unary) {// 一些前置检查和设置、兼容处理此处省略// 此处定义了初始化的元素AST对象const element: ASTElement = {type: 1,tag,attrsList: attrs,attrsMap: makeAttrsMap(attrs),parent: currentParent,children: []};// 检查元素标签是否合法(不是保留命名)if (isForbiddenTag(element) && !isServerRendering()) {element.forbidden = true;process.env.NODE_ENV !== "production" &&warn("Templates should only be responsible for mapping the state to the " +"UI. Avoid placing tags with side-effects in your templates, such as " +`<${tag}>` +", as they will not be parsed.");}// 执行一些前置的元素预处理for (let i = 0; i < preTransforms.length; i++) {preTransforms[i](element, options);}// 是否原生元素if (inVPre) {// 处理元素元素的一些属性processRawAttrs(element);} else {// 处理指令,此处包括v-for/v-if/v-once/key等等processFor(element);processIf(element);processOnce(element);processKey(element); // 删除结构属性// 确定这是否是一个简单的元素element.plain = !element.key && !attrs.length;// 处理ref/slot/component等属性processRef(element);processSlot(element);processComponent(element);for (let i = 0; i < transforms.length; i++) {transforms[i](element, options);}processAttrs(element);}// 后面还有一些父子节点等处理,此处省略}// 其他省略});return root;
}

2、DOM 元素捕获

假如我们需要捕获一个<div>元素,再生成一个<div>元素。


有一段模板,我们可以对它进行捕获:


<div><a>111</a><p>222<span>333</span> </p>
</div>

捕获后我们可以得到这样一个对象:


divObj = {dom: {type: "dom",ele: "div",nodeIndex: 0,children: [{type: "dom",ele: "a",nodeIndex: 1,children: [{ type: "text", value: "111" }]},{type: "dom",ele: "p",nodeIndex: 2,children: [{ type: "text", value: "222" },{type: "dom",ele: "span",nodeIndex: 3,children: [{ type: "text", value: "333" }]}]}]}
};

这个对象保存了我们需要的一些信息:

  • HTML元素里需要绑定哪些变量,因为变量更新的时候需要更新该节点内容。

  • 以怎样的方式来拼接,是否有逻辑指令,如v-ifv-for

  • 哪些节点绑定了什么监听事件,是否匹配一些常用的事件能力支持

Vue 会根据 AST 对象生成一段可执行的代码,我们看看这部分的实现:


// 生成一个元素
function genElement(el: ASTElement): string {// 根据该元素是否有相关的指令、属性语法对象,来进行对应的代码生成if (el.staticRoot && !el.staticProcessed) {return genStatic(el);} else if (el.once && !el.onceProcessed) {return genOnce(el);} else if (el.for && !el.forProcessed) {return genFor(el);} else if (el.if && !el.ifProcessed) {return genIf(el);} else if (el.tag === "template" && !el.slotTarget) {return genChildren(el) || "void 0";} else if (el.tag === "slot") {return genSlot(el);} else {// component或者element的代码生成let code;if (el.component) {code = genComponent(el.component, el);} else {const data = el.plain ? undefined : genData(el);const children = el.inlineTemplate ? null : genChildren(el, true);code = `_c('${el.tag}'${data ? `,${data}` : "" // data}${children ? `,${children}` : "" // children})`;}// 模块转换for (let i = 0; i < transforms.length; i++) {code = transforms[i](el, code);}// 返回最后拼装好的可执行的代码return code;}
}

3、模板引擎赋能


通过以上介绍,或许大家会说,原本就是一个<div>,经过 AST 生成一个对象,最终还是生成一个<div>,这不是多余的步骤吗?


其实 ,在这个过程中我们可以实现一些功能:

  • 排除无效 DOM 元素,并在构建过程可进行报错

  • 使用自定义组件的时候,可匹配出来

  • 可方便地实现数据绑定、事件绑定等功能

  • 为虚拟 DOM Diff 过程打下铺垫

  • HTML 转义预防 XSS 漏洞


通用的模板引擎能处理很多低效又重复的工作,例如浏览器兼容、全局事件的统一管理和维护、模板更新的虚拟 DOM 机制、树状组织管理组件。这样我们知道了模板引擎都做了什么事情后,就可以区分 Vue 框架提供的能力和我们需要自行处理的逻辑,可以更专注于业务开发。


二、虚拟DOM


虚拟 DOM 大概可分成三个过程:

第一步,用 JS 对象模拟 DOM 树,得到一棵虚拟 DOM 树。

第二步,当页面数据变更时,生成新的虚拟 DOM 树,比较新旧两棵虚拟 DOM 树的差异。

第三步,把差异应用到真正的 DOM 树上。


1、用 JS 对象模拟 DOM 树

为什么要用到虚拟 DOM ? 因为一个真正的 DOM 元素非常庞大,拥有很多的属性值,而实际上我们并不是全部都会用到,通常包括节点内容、元素位置、样式、节点的添加删除等方法。所以,我们通过用 JS 对象表示 DOM 元素的方式,可以大大降低了比较差异的计算量。


我们来看一下 VNode 源码,只有以下20来个属性:


tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context fordevtools
fnScopeId: ?string; // functional scope id support

2 、比较新旧两棵虚拟 DOM 树的差异


虚拟 DOM 中,差异对比是很关键的一步,当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。这样的差异需要记录:

  • 需要替换掉原来的节点
  • 移动、删除、新增子节点
  • 修改了节点的属性
  • 对于文本节点的文本内容改变

下图,我们对比两棵 DOM 树,得到的差异有:

  • p 元素插入了一个 span 元素子节点

  • 原先的文本节点挪到了 span 元素子节点下面


在这里插入图片描述


3、应用差异到真正的 DOM 树


通过前面的示例,我们知道差异记录要应用到真正的 DOM 树上,需要进行一些操作,例如节点的替换、移动、删除,文本内容的改变等。


在 Vue 中是怎么进行 DOM Diff 呢? 简单看这段代码感受下, 虽然代码里很多函数没贴出来,但其实看函数名也可以大概理解都是什么作用,例如updateChildrenaddVnodesremoveVnodessetTextContent等。


// 对比差异后更新
const oldCh = oldVnode.children;
const ch = vnode.children;
if (isDef(data) && isPatchable(vnode)) {for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
}
if (isUndef(vnode.text)) {if (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch)updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);} else if (isDef(ch)) {if (process.env.NODE_ENV !== "production") {checkDuplicateKeys(ch);}if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);} else if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1);} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, "");}
} else if (oldVnode.text !== vnode.text) {nodeOps.setTextContent(elm, vnode.text);
}
if (isDef(data)) {if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
}

三、数据绑定


在 Vue 中,最基础的模板语法是数据绑定。

例如:

<div>{{ message }}</div>

这里使用插值表达式{{}}绑定了一个message的变量,开发者在 Vue 实例data中绑定该变量:


new Vue({data: {message: "test"}
});

最终页面展示内容为<div>test</div>。那这是怎么做到的呢?


1、 数据绑定的实现


这种使用双大括号来绑定变量的方式,我们称之为数据绑定。

数据绑定的过程其实不复杂:
(1) 、解析语法生成 AST
(2) 、根据 AST 结果生成 DOM
(3) 、将数据绑定更新至模板


这个过程是 Vue 中模板引擎在做的事情,我们来看看上面在 Vue 里的代码片段<div></div>,我们可以通过 DOM 元素捕获,解析后获得这样一个 AST 对象:


divObj = {dom: {type: "dom",ele: "div",nodeIndex: 0,children: [{ type: "text", value: "" }]},binding: [{ type: "dom", nodeIndex: 0, valueName: "message" }]
};

我们在生成 DOM 的时候,添加对message的监听,数据更新时会找到对应的nodeIndex更新值:


// 假设这是一个生成 DOM 的过程,包括 innerHTML 和事件监听
function generateDOM(astObject) {const { dom, binding = [] } = astObject;// 生成DOM,这里假设当前节点是baseDombaseDom.innerHTML = getDOMString(dom);// 对于数据绑定的,来进行监听更新baseDom.addEventListener("data:change", (name, value) => {// 寻找匹配的数据绑定const obj = binding.find(x => x.valueName == name);// 若找到值绑定的对应节点,则更新其值。if (obj) {baseDom.find(`[data-node-index="${obj.nodeIndex}"]`).innerHTML = value;}});
}// 获取DOM字符串,这里简单拼成字符串
function getDOMString(domObj) {// 无效对象返回''if (!domObj) return "";const { type, children = [], nodeIndex, ele, value } = domObj;if (type == "dom") {// 若有子对象,递归返回生成的字符串拼接const childString = "";children.forEach(x => {childString += getDOMString(x);});// dom对象,拼接生成对象字符串return `<${ele} data-node-index="${nodeIndex}">${childString}</${ele}>`;} else if (type == "text") {// 若为textNode,返回text的值return value;}
}

这样,我们就能在message变量更新的时候,通过该变量关联的引用,来自动更新对应展示的内容。而要知道message变量什么时候进行了改变,我们需要对数据进行监听。


2、数据更新监听


加粗样式
我们能看到,上面的简单代码描述过程中,使用的数据监听方法是用了addEventListener("data:change", Function)的方式。

在 Vue 中,数据更新的时候就执行了模板更新、watch、computed 等一些工作,主要是依赖了Getter/Setter。而 Vue3.0 将使用Proxy的方式来进行:


Object.defineProperty(obj, key, {enumerable: true,configurable: true,// getterget: function reactiveGetter() {const value = getter ? getter.call(obj) : val;if (Dep.target) {dep.depend();if (childOb) {childOb.dep.depend();if (Array.isArray(value)) {dependArray(value);}}}return value;},// setter最终更新后会通知set: function reactiveSetter(newVal) {const value = getter ? getter.call(obj) : val;if (newVal === value || (newVal !== newVal && value !== value)) {return;}if (process.env.NODE_ENV !== "production" && customSetter) {customSetter();}if (getter && !setter) return;if (setter) {setter.call(obj, newVal);} else {val = newVal;}childOb = !shallow && observe(newVal);dep.notify();}
});

Vue 中大多数能力都依赖于模板引擎,包括组件化管理、事件管理、Vue 实例、生命周期等,相信只要理解了 AST、虚拟 DOM、数据绑定相关的机制后,再去翻阅 Vue 源码 ,了解更多的能力就不是问题了。



http://www.ppmy.cn/news/64560.html

相关文章

5个PPT素材、模板网站,免费下载,赶紧马住了~

推荐几个可以免费下载PPT素材的网站&#xff0c;建议收藏&#xff01; 1、菜鸟图库 https://www.sucai999.com/search/ppt/0_0_0_1.html?vNTYwNDUx 菜鸟图库网有非常丰富的免费素材&#xff0c;像设计类、办公类、自媒体类等素材都很丰富。PPT模板种类很多&#xff0c;全部都…

操作系统第二章——进程与线程(下)

东风夜放花千树&#xff0c;更吹落&#xff0c;星如雨 文章目录 2.3.1 进程同步&#xff0c;进程互斥知识总览什么是进程同步什么是进程互斥知识回顾 2.3.2 进程互斥的软件实现方法知识总览如果没有进程互斥单标志法双标志先检查法双标志后检查法Peterson算法知识回顾 2.3.3进程…

《微服务实战》 第五章 Spring Cloud Netflix 之 Ribbon

前言 Spring Cloud Ribbon 是一套基于 Netflix Ribbon 实现的客户端负载均衡和服务调用工具&#xff0c;其主要功能是提供客户端的负载均衡算法和服务调用。 1、负载均衡 负载均衡&#xff08;Load Balance&#xff09; &#xff0c;简单点说就是将用户的请求平摊分配到多个…

MES管理系统有什么功能?前期实施MES需要做些什么

MES系统是在制造业数字化的环境下&#xff0c;围绕生产制造执行而开发的一套生产管理系统。它以车间为管理核心&#xff0c;通过集成各信息系统&#xff0c;整合企业资源&#xff0c;实现从订单下达到产品完成的整个生产制造过程的数字化管理。 MES系统在实施前需要进行各种准备…

5.设计模式之模板方法

前言 模板方法模式是软件开发中经常用得到模式。在很多流行的框架中都能看到他的身影。比如spring&#xff0c;junit&#xff0c;servlet等等。它定义了一个操作中的算法骨架&#xff0c;将某些步骤延迟到子类中实现。 这样&#xff0c;新的子类可以在不改变一个算法结构的前…

操作系统基础知识介绍之可靠性与可用性(包含MTTF、MRBF、MTTR等)

计算机是在不同的抽象层上设计和构建的。 我们可以通过计算机递归地下降&#xff0c;看到组件将自身放大为完整的子系统&#xff0c;直到我们遇到单个晶体管。 尽管有些故障很普遍&#xff0c;例如断电&#xff0c;但许多故障仅限于模块中的单个组件。 因此&#xff0c;一个模块…

Java框架学习04(SpringBoot自动装配原理)

1、什么是 SpringBoot 自动装配&#xff1f; 2、SpringBoot 是如何实现自动装配的&#xff1f;如何实现按需加载&#xff1f; 3、如何实现一个 Starter&#xff1f; 前言 使用过 Spring 的小伙伴&#xff0c;一定有被 XML 配置统治的恐惧。即使 Spring 后面引入了基于注解的配…

【 断电延时继电器 电源监视 导轨安装 JOSEF约瑟 HJZS-E202 AC220V】

品牌&#xff1a;JOSEF约瑟型号&#xff1a;HJZS-E202名称&#xff1a;断电延时继电器额定电压&#xff1a;110、220VDC/AC&#xff1b;100VAC触点容量&#xff1a;250V/5A功率消耗&#xff1a;≤4.2W返回系数&#xff1a;10%额定电压 系列型号&#xff1a; HJZS-E202断电延时…