Vue虚拟DOM理解 | 深入且易懂

文章目录

  • 前言
  • 虚拟DOM理解
    • 什么是虚拟DOM
    • 虚拟DOM转为真实DOM
    • 组件的本质
    • 模板工作原理

前言

本篇文章部分内容来源于霍春阳 《Vue.js设计与实现》这本书的理解, 感兴趣的小伙伴可以自行购买阅读。可以非常明确的感受到作者对 Vue 的深刻理解以及用心, 富含非常全面的 Vue 的知识点, 强烈推荐给大家。

虚拟DOM理解

先抛出一个结论, 这个结论对本篇文章的理解很有帮助: Vue是一个保留了运行时+编译时架构的框架, 编译时, 用户可以提供 HTML 字符串, 我们将其编译为数据对象再交给运行时处理; 运行时, 根据提供的数据对象渲染到页面中。这里不太理解没关系, 下文会逐步帮大家理解。

什么是虚拟DOM

上面提到的这个数据对象其实就是所谓的虚拟DOM, 他是一个 JS 树型结构的数据对象, 通俗易懂的说, 虚拟 DOM 就是一个 JS 普通对象, 这个 JS 对象是真实 DOM 的描述。没有太懂没关系, 看看下面这个例子。

例如, 我们下面这样有一段 HTML 字符串:

const html = `<div id="app"><span>Hello World</span></div>
`

假定有这样 Compiler 一个应用程序作为编译器, 它的作用是在将一个 HTML 字符串转换为树形的数据结构。

const obj = Compiler(html)
// obj 结果如下
obj = {tag: "div",props: {id: 'app'}children: [{tag: "span", children: "Hello World"}]
}

这个转换出来的树形的数据结构 obj 就可以看做是虚拟 DOM, 其中 tag 用来描述标签名称; props 是一个对象, 用来描述标签属性、事件等内容; children 用来描述标签的子节点, 即可以是一个数组, 代表子节点, 也可以是一个字符串, 代表子节点为文本节点。现在我们可以更深刻的感受到, 虚拟 DOM 就是对真实 DOM 的一个描述。

事实上, 你完全可以设计自己设计虚拟 DOM 的结构, 比如使用 tagName 来描述标签名。

虚拟DOM转为真实DOM

上面我们提到过, Vue是一个运行时 + 编译时架构的框架, 我们再来理解一下。上面我们的过程, 就是在编译时进行的, 将 HTML 字符串编译为 JS 数据对象, 也就是虚拟 DOM。那么运行时, 我们就根据这个 JS 数据对象(虚拟 DOM), 渲染元素到页面当中了, 也就是转为真实的 DOM。那么虚拟 DOM 究竟是如何转换为真实 DOM 的? 其实它是通过渲染器实现的。渲染器是非常重要的一个角色, 平时我们使用 Vue.js 就是依赖渲染器进行的工作, 下面我们来简单认识一下渲染器。

我们可以编写一个简单版的渲染器, 将虚拟 DOM 渲染到页面当中。例如我们有下面这样一个虚拟 DOM:

  • tag: 描述标签, 表示渲染一个 <button> 标签
  • props: 描述属性或事件, 表示我们给 <button> 添加一个点击事件
  • children: 用来描述子节点, 这里意思是表示 button 中的文本是"按钮" --> <button>按钮</button>
const vnode = {tag: "button",props: {"onClick": () => alert("Hello World")},children: "按钮"
}

接下来我们就需要一个渲染器 renderer, 将上面这段虚拟 DOM, 转换为真实的 DOM:

function renderer (vnode, container){// 获取标签名, 并创建一个DOM元素const curEl = document.createElement(vnode.tag)// 遍历属性, 将事件或属性添加到DOM元素上for (const key in vnode.props) {// on开头说明是事件, 则为DOM元素添加事件if(/^on/.test(key)) curEl.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])}// 如果子节点为stirng类型, 说明是文本子节点if (typeof vnode.children === "string") curEl.appendChild(document.createTextNode(vnode.children))// 如果子节点为数组类型, 递归调用渲染函数else if (Array.isArray(vnode.children)) vnode.children.forEach(child => renderer(child, curEl));// 将元素添加到挂载点下container.appendChild(curEl)  
}

下面我们传入刚刚的虚拟 DOM, 将它挂载到 body 下, 在浏览器运行代码, 我们就可以在页面中得到一个按钮, 点击按钮就会出现弹出 “Hello World”。

renderer(vnode, document.body)

当然实际上的渲染器 renderer 内部会更为复杂, 这里我们只是做了一个简单实现。更加详情的实现我们可以去查看 Vue 的源码。

组件的本质

对于虚拟 DOM 和渲染器我们都有了初步的理解, 那么组件又是什么呢? 组件和虚拟 DOM 之间的关系是什么? 渲染器又是如何渲染组件的?

事实上, 虚拟 DOM 除了可描述真实的 DOM 之外, 还可以用来描述组件, 但是组件毕竟不是一个真实的 DOM 元素, 那么我们该如何进行描述? 在讲述这个问题之前, 我要在抛出一个问题, 组件的本质是什么? 本质: 组件就是一组 DOM 元素的封装, 这组 DOM 元素就是该组件要渲染的内容。那如果这样的话, 我们就可以定义一个函数来代表组件。

例如上一节的 vnode 我们将其定义到一个组件当中, 用一个函数来代表; 函数的返回值就是要渲染的内容, 也就是虚拟 DOM:

function MyComponent() {return {tag: "button",props: {"onClick": () => alert("Hello World")},children: "按钮"}
}

我们已经搞清楚组件的本质, 那么我们就可以使用虚拟 DOM 像描述标签一样来描述组件, 只不过 tag 属性中存放的不再是标签名, 而是组件函数。

const vnode = {tag: MyComponent,
}

当然, 我们也需要让渲染器 renderer 支持组件, 才能渲染, 所以我们需要对上本中的 renderer 函数进行一些修改为如下所示:

function renderer(vnode, container) {// 说明描述的是标签if (typeof vnode.tag === "string") mountElement(vnode, container)// 说明描述的时组件else if(typeof vnode.tag === "function") mountComponent(vnode, container)
}

我们先将上文中的 renderer 函数的名称修改为 mountElement, 让渲染器用来处理标签, 如下:

function mountElement (vnode, container){// 获取标签名, 并创建一个DOM元素const curEl = document.createElement(vnode.tag)// 遍历属性, 将事件或属性添加到DOM元素上for (const key in vnode.props) {// on开头说明是事件, 则为DOM元素添加事件if(/^on/.test(key)) curEl.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])}// 如果子节点为stirng类型, 说明是文本子节点if (typeof vnode.children === "string") curEl.appendChild(document.createTextNode(vnode.children))// 如果子节点为数组类型, 递归调用渲染函数else if (Array.isArray(vnode.children)) vnode.children.forEach(child => renderer(child, curEl));// 将元素添加到挂载点下container.appendChild(curEl)  
}

再实现一个 mountComponent 函数, 让渲染器用来处理组件:

function mountComponent (vnode, container) {// 获取到组件返回的虚拟DOMconst subtree = vnode.tag()// 递归调用renderer渲染组件返回的虚拟DOMrenderer(subtree, container)
}

传入vNode 和 body 作为挂载点, 在浏览器中运行, 同样可以实现上文中的效果。

renderer(vNode, document.body)

这样我们就通过虚拟 DOM 描述组件, 转换为真实渲染到页面当中, 但是组件一定是函数吗? 学习过 react 的小伙伴一定知道, 在 react 中有函数组件, 也有类组件。所以我们完全也可以使用一个 JS 对象来表达组件:

const MyComponent = {render() {return {tag: "button",props: {"onClick": () => alert("Hello World")},children: "按钮"}}
}

当然渲染器 renderer 以及 mountComponent 函数都需要做一些修改, 支持对象组件:

// 渲染器的修改
function renderer(vnode, container) {const type = vnode.tag if (typeof type === "string") mountElement(vnode, container)else if (typeof type === 'function' ||Object.prototype.toString.call(type) === '[object Object]')mountComponent(vnode, container)
}
// mountComponent函数的修改
function mountComponent (vnode, container) {const type = vnode.tag let subtree// 获取到组件返回的虚拟DOMif (typeof type === "function") subtree = vnode.tag()else if (typeof type === "object") subtree = vnode.tag.render()// 递归调用renderer渲染组件返回的虚拟DOMrenderer(subtree, container)
}

我们只做了很小的修改, 就能够满足用对象来表达组件的需求。到这里, 我们就完成了虚拟 DOM 将组件转为真实 DOM 的操作, 并且支持函数表达组件对象表达组件两种方式。

模板工作原理

前面我们知道了虚拟 DOM 是如何渲染为真实 DOM 的, 那么下面我们就来探讨一下模板是怎么工作的? 这也是 Vue 中另一个非常重要的角色: 编译器。我们再来回忆一下, 文章开头提到运行时+编译时。我们已经知道运行时, 是通过渲染器将虚拟 DOM 渲染为真实 DOM; 以及编译时将 HTML 字符串编译成虚拟 DOM。

那么 Vue 中的模板就怎样进行工作的呢? 就是通过编译器, 编译器和渲染器一样, 只是一段程序而已, 编译器的作用是将模板编译成渲染函数。对于编译器来说, 模板就是一个普通字符串, 它会对字符串进行分析, 并生成一个功能相同的渲染函数(也就是 h 函数, 不知道 h 函数的可以去网上看看资料, 简单了解一下如何使用即可)。

下面以一个 .vue 文件举个栗子, 有如下所示一个 Vue 文件:

<template><div @click="handler">按钮</div>
</template><script>
export default {data() {/* ... */},methods: {handler: () => {/* ... */}}
}
</script>

其中 template 标签中就是模板的内容, 编译器会把模板内容编译成一个渲染函数, 并添加到 script 标签中。上面代码经过编译器处理, 最终在浏览器中运行的代码如下:

<script>
export default {data() {/* ... */},methods: {handler: () => {/* ... */}},render() {return h('div', { onClick: handler }, '按钮')}
}
</script>

所以无论是我们自己手写渲染函数, 还是使用模板, 它最终渲染的内容都是通过渲染函数产生的, 所以模板我们可以看做是手写渲染函数的一个语法糖。

组件的实现依赖于渲染器,模板的编译依赖于编译器。编译器会将模板内容编译成一个渲染函数, 渲染函数的返回值就是虚拟 DOM, 渲染器再根据返回的这个虚拟 DOM 渲染成真实 DOM。这个过程就是模板的工作原理, 也是 Vue 渲染到页面的流程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/124744.shtml

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

相关文章

面试经典150题——Day26

文章目录 一、题目二、题解 一、题目 392. Is Subsequence Given two strings s and t, return true if s is a subsequence of t, or false otherwise. A subsequence of a string is a new string that is formed from the original string by deleting some (can be none…

Mac电脑Android Studio和VS Code配置Flutter开发环境(图文超详细)

一、安装Android Studio 官网地址&#xff1a; https://developer.android.google.cn/ 历史版本下载地址&#xff1a; https://developer.android.com/studio/archive?hlzh-cn 二、安装Xcode 到App Store下载安装最新版本&#xff0c;如果MacOS更新不到13.0以上就无法安装…

Go-Python-Java-C-LeetCode高分解法-第十二周合集

前言 本题解Go语言部分基于 LeetCode-Go 其他部分基于本人实践学习 个人题解GitHub连接&#xff1a;LeetCode-Go-Python-Java-C 欢迎订阅CSDN专栏&#xff0c;每日一题&#xff0c;和博主一起进步 LeetCode专栏 我搜集到了50道精选题&#xff0c;适合速成概览大部分常用算法 突…

消息中间件——RabbitMQ(一)Windows/Linux环境搭建(完整版)

前言 最近在学习消息中间件——RabbitMQ&#xff0c;打算把这个学习过程记录下来。此章主要介绍环境搭建。此次主要是单机搭建&#xff08;条件有限&#xff09;&#xff0c;包括在Windows、Linux环境下的搭建&#xff0c;以及RabbitMQ的监控平台搭建。 环境准备 在搭建Rabb…

机器人的触发条件有什么区别,如何巧妙的使用

简介​ 维格机器人触发条件,分为3个,分别是: 有新表单提交时、有记录满足条件时、有新的记录创建时 。 看似3个,其实是能够满足我们非常多的使用场景。 本篇将先介绍3个条件的触发条件,然后再列举一些复杂的触发条件如何用现有的触发条件来满足 注意: 维格机器人所有的…

【Python第三方包】串口通信(pySerial包)

文章目录 前言一、串口的基本使用1.1 配置串口基本信息1.2 读取串口数据1.3 写串口1.4 关闭串口二、示例代码2.1 示例1: 从串口读取数据2.2 示例2: 向串口写入数据总结前言 串口通信是许多嵌入式和物联网应用中的关键组成部分。Python 提供了许多第三方库来简化串口通信的实现…

Linux(Centos7)防火墙端口操作记录

1、nginx -t #Nginx配置文件检查 上述截图代表检查没问题 上述截图检查配置文件配置错误&#xff0c;并提示错误文件位置 2、systemctl restart nginx #重启Nginx 重启Nginx失败 3、systemctl status nginx.service #查看Nginx服务状态 80端口被占导致服务启动失败 4、n…

MYSQL(索引篇)

一、什么是索引 索引是一种数据结构&#xff0c;它用来帮助MYSQL更高效的获取数据 采用索引可以提高数据检索的效率&#xff0c;降低IO成本 通过索引对数据排序&#xff0c;降低数据排序成本&#xff0c;降低CPU消耗 常见的有&#xff1a;B树索引、B树索引、哈希索引。其中Inno…

leetcode 238. 除自身以外数组的乘积

leetcode 238. 除自身以外数组的乘积 题目说明&#xff0c;不能使用除法&#xff0c;没有思路。 答案一&#xff1a;超时&#xff0c;因为left、right和result一开始没有设置数组大小&#xff0c;存取浪费时间。 class Solution { public:vector<int> productExceptSel…

【Python】 Python之markdown模块

Python之markdown模块 为了编辑和维护方便使用的是MD来编辑(数据库和文档都是MD)&#xff0c;但是实际展示中生成的API中&#xff0c;需要HTML标签来展示&#xff0c;故此这里需要一个能转换的工具来帮忙。 1. 其他库&#xff1a; markdown markdown2 snudown 2. 安装&…

99%网工都会遇到的经典面试问题

①问题:介绍TCP连接的三次握手?追问:为什么TCP需要握手三次? 三次握手: 第一步:A向B发送一个SYN报文表示希望建立连接 第二步:B收到A发过来的数据包后&#xff0c;通过SYN得知这是一个建立连接的请求&#xff0c;于是发送ACK确认&#xff0c;由于TCP的全双工模式&#xff…

除自身以外数组的乘积

给你一个整数数组 nums&#xff0c;返回 数组 answer &#xff0c;其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 不要使用除法&#xff0c;且在 O(n) 时间复杂…

代码随想录打卡第五十五天|● 300.最长递增子序列 ● 674. 最长连续递增序列 ● 718. 最长重复子数组

300.最长递增子序列 **题目&#xff1a;**给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0…

深度学习| U-Net网络

U-Net网络 基础知识和CNN的关系反卷积ReLU激活函数 U-Net入门U-Net网络结构图为什么需要跳跃连接U-Net的输入U-Net的应用 基础知识 理解U-Net网络结构需要相关知识点。 和CNN的关系 U-Net也是CNN&#xff08;Convolutional Neural Network&#xff0c;卷积神经网络&#xff…

Flutter PopupMenuButton下拉菜单

下拉菜单是移动应用交互中一种常见的交互方式,可以使用下拉列表来展示多个内容标签,实现页面引导的作用。在Flutter开发中,实现下拉弹框主要有两种方式,一种是继承Dialog组件使用自定义布局的方式实现,另一种则是使用官方的PopupMenuButton组件进行实现。 如果没有特殊的…

Android任务栈和启动模式

Andrcid中的任务栈是一种用来存放Activity实倒的容器。任务最大的特点就是先进后出&#xff0c;它主要有两个基本操作&#xff0c;分别是压栈和出栈。通常Andaid应用程序都有一个任务栈&#xff0c;每打开一个Activity时&#xff0c;该Activity就会被压入任务栈。每销毁一个Act…

Unity Animator cpu性能测试

测试案例&#xff1a; 场景中共有4000个物体&#xff0c;挂在40个animtor 上&#xff0c;每个Animator控制100个物体的动画。 使用工具&#xff1a; Unity Profiler. Unity 版本&#xff1a; unity 2019.4.40f1 测试环境&#xff1a; 手机 测试过程&#xff1a; 没有挂…

VNC图形化远程连接Ubuntu服务器

我的Ubuntu版本22.04.3&#xff0c;带有gnome图形桌面。配置过程参考了几篇博客&#xff0c;大致流程如下。因为是配置完之后才整理的流程&#xff0c;可能有疏漏。 Ubuntu服务器上的配置 1.先在服务器上下载vnc server&#xff08;任何一种版本均可&#xff09; vncserver有…

新电脑第一次重启后蓝屏

新电脑第一次重启后蓝屏 悲惨事故&#xff0c;远程参加插电第一次开机&#xff0c;按“FNShiftF10”启动cmd窗口输入oobe\bypassnro 回车重启跳过网络连接&#xff0c;设置一个用户名密码设置为空&#xff0c;不设密码确定&#xff0c;进入系统软件操作磁盘操作&#xff08;磁盘…

Angular-05:管道

① 介绍1.1 基本用法1.2 管道参数1.3 链式管道1.4 纯管道1.5 非纯管道 ② 内置管道③自定义管道 ① 介绍 管道&#xff1a;pipe 。在angular中处理组件模板的数据格式。管道操作符 | &#xff0c;连接模板表达式中左边输入数据和右边的管道。 1.1 基本用法 例&#xff1a;这里…