Vue 渲染流程详解

news2024/11/28 20:39:44

在 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-if、v-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 呢? 简单看这段代码感受下, 虽然代码里很多函数没贴出来,但其实看函数名也可以大概理解都是什么作用,例如updateChildren、addVnodes、removeVnodes、setTextContent等。
 

// 对比差异后更新
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,这里假设当前节点是baseDom
  baseDom.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,
  
  // getter
  get: 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.coloradmin.cn/o/793189.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Citespace、vosviewer:揭秘最火热的学术文献绘图神器!

目录 专题一 文献计量学方法与应用简介 专题二 主题确定、检索与数据采集 专题三 VOSviewer可视化绘图精讲 专题四 Citespace可视化绘图精讲 专题五 R语言文献计量学绘图分析 专题六 论文写作 专题七 论文投稿 文献计量学是指用数学和统计学的方法&#xff0c;定量地分析…

【C++从0到王者】第十二站:vector基本使用

文章目录 一、vector基本介绍二、vector的基本使用三、vector\<char> 和string的区别四、vector接口介绍1.vector的模板参数2.构造函数3.迭代器4.size和max_size5.resize和reserve6.operator[]和at7.front和back8.data9.push_back和pop_back10.insert和erase11.assign12.…

C#月数计算器(主要用于社保、医保缴费月数计算)

1、为什么做这个&#xff1f; 工作中&#xff0c;经常需要计算参保人社保、医保缴费月数&#xff0c;之前都是在Excel中写一个DATEDIF公式&#xff0c;修改单元格中的日期&#xff0c;计算间隔的月数&#xff0c;公式如下&#xff1a; DATEDIF(起始日期, 终止日期, 返回类型) …

QT第二讲

思维导图 完善登录框&#xff0c;当登录成功时&#xff0c;关闭登录界面&#xff0c;跳转到新的界面中 loginscuueed.h #ifndef LOGINSUCCEED_H #define LOGINSUCCEED_H#include <QWidget>namespace Ui { class loginSucceed; }class loginSucceed : public QWidget {…

android数据的储存、文件的储存、SharedPreferences储存、SQLite的基本用法

一、文件的储存 1、将数据储存到文件中 Context类中提供了openfileOutput()方法&#xff0c;用来获取一个文件流&#xff0c;这个方法接收两个参数&#xff0c;第一个参数是文件名&#xff0c;在文件创建的时候使用的就是这个名称&#xff0c;注意这里指定的文件名不可以包含…

白话机器学习笔记(一)学习回归

最小二乘法 定义模型 表达式&#xff1a; f θ ( x ) θ 0 θ 1 x f_\theta(x)\theta_0\theta_1x fθ​(x)θ0​θ1​x &#xff08;常用 θ \theta θ表示未知数、 f θ ( x ) f_\theta(x) fθ​(x)表示含有参数 θ \theta θ并且和变量 x x x相关的函数&#xff09; 目标…

【每日一题】—— D. Prefix Permutation Sums (Codeforces Round 888 (Div. 3))

&#x1f30f;博客主页&#xff1a;PH_modest的博客主页 &#x1f6a9;当前专栏&#xff1a;每日一题 &#x1f48c;其他专栏&#xff1a; &#x1f534; 每日反刍 &#x1f7e1; C跬步积累 &#x1f7e2; C语言跬步积累 &#x1f308;座右铭&#xff1a;广积粮&#xff0c;缓称…

三自由度PUMA机器人非线性控制研究(Matlab代码、Simulink仿真实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Matlab代码、Simulink仿真实现 &#x1f4a5;1 概述 针对三自由度PUMA560机器人的控制问题&#xff0c;可以使用三种不同的非线性控制算法&#xff1a;计算扭矩控制、滑模控制和反步…

RL 实践(5)—— 二维滚球环境【REINFORCE Actor-Critic】

本文介绍如何用 REINFORCE 和 Actor-Critic 这两个策略梯度方法解二维滚球问题参考&#xff1a;《动手学强化学习》完整代码下载&#xff1a;6_[Gym Custom] RollingBall (REINFORCE and Actor-Critic) 文章目录 1. 二维滚球环境2. 策略梯度方法2.1 策略学习目标2.2 策略梯度定…

石子合并(区间dp模板)

题目描述&#xff1a; dp分析&#xff1a; 解题代码&#xff1a; #include<iostream> using namespace std;const int N1e36;int f[N][N]; int a[N]; int s[N];int main(){int n;cin>>n;for(int i1;i<n;i){scanf("%d",&s[i]);s[i]s[i-1];//前缀和…

使用fastjson错误

说明&#xff1a;使用fastjson时&#xff0c;对象解析不成功&#xff0c;一直报错&#xff0c;但是json格式没有错&#xff1b; 错误信息&#xff1a;Method threw ‘com.alibaba.fastjson.JSONException’ exception. json数据是正确的 分析&#xff1a;注意看&#xff0c;fa…

【用IDEA基于Scala2.12.17开发Spark 3.4.1 项目】

目录 使用IDEA创建Spark项目设置sbt依赖创建Spark 项目结构新建Scala代码 使用IDEA创建Spark项目 打开IDEA后选址新建项目 选址sbt选项 配置JDK debug 解决方案 相关的依赖下载出问题多的话&#xff0c;可以关闭idea&#xff0c;重启再等等即可。 设置sbt依赖 将sbt…

数据安全问题防不胜防?教你使用铁威马的321原则

无论是勒索病毒的袭击&#xff0c;还是硬件损坏、人为误删等原因造成的数据丢失的意外让我们防不胜防。为此&#xff0c;我们时常建议大家尤其是企业通过【3-2-1备份原则】的数据保护策略来备份数据&#xff0c;以便在数据受损失时&#xff0c;能快速地从备份介质中完整地恢复数…

git相关

gerrit用户指南&#xff1a; 资料&#xff1a;Gerrit 用户指南 gerrit-user-guide 上述有介绍如何review&#xff0c;review并非修改代码之后如何重新提交等操作 jenkins介绍 Jenkins详细教程 - 知乎 一、jenkins是什么&#xff1f; Jenkins是一个开源的、提供友好操作界…

vite的介绍

Vite&#xff08;法语意为 "快速的"&#xff0c;发音 /vit/&#xff0c;发音同 "veet")是一种新型前端构建工具 优势 &#x1f4a1; 极速的服务启动&#xff0c;使用原生 ESM 文件&#xff0c;无需打包 ⚡️ 轻量快速的热重载&#xff0c;始终极快的模块…

Centos7.6安装RocketMQ4.9.2并配置开机自启

1、下载RocketMQ 编译后的压缩包 wget https://dlcdn.apache.org/rocketmq/4.9.2/rocketmq-all-4.9.2-bin-release.zip2、解压 unzip rocketmq-all-4.9.2-bin-release.zip3、进入解压文件夹 cd rocketmq-all-4.9.2/4、编辑配置文件/usr/local/rocketmq/rocketmq-all-4.9.4-…

Spring Boot 拦截器实现:登录验证 统一异常处理 返回数据规范化

学习 Spring 和 servlet 初期&#xff0c;我们在判断用户身份时&#xff0c;都是在每个方法中获取会话、获取对象&#xff0c;这种方式冗余度高&#xff0c;增加代码复杂度&#xff0c;维护成本也高&#xff0c;因此想到可以使用 AOP 来实现一个公共的方法&#xff0c;这个公共…

Android 自定义跳转到系统 Settings Fragment 的 Intent

以跳转到蓝牙控制面板为例&#xff0c;控制面板如图所示&#xff1a; 其 Fragment 所在的位置是&#xff1a; packages/apps/Settings/src/com/android/settings/connecteddevice/BluetoothDashboardFragment.java 第一步 要在 Settings的主要 Activity 中定义继承同一个父类…

通达信波段王指标公式_通达信公式

使用说明&#xff1a;1、买点&#xff1a;多空分界线变红后出现波段买为做多信号&#xff0c;中间出现波段卖信号为杂波可以不考虑&#xff0c;再结合逃顶信号进行卖出操作&#xff0c;如果没有出现逃顶信号&#xff0c;则可以等多空分界线变绿后结合波段卖信号综合做出判断。2…

基于proteus的纯模拟病房呼叫系统

摘要&#xff1a;无线的病房呼叫系统将病人的呼叫请求迅速传递给医护者&#xff0c;减少了信息传递的过程性&#xff0c;病人无需等待医护巡查才可求助&#xff0c;增加了便捷性与即时性&#xff1b;此外也减轻一定的工作量&#xff0c;加强服务的效率。本文基于proteus软件进行…