13_渲染器的设计

目录

    • 渲染器与响应式系统的结合
    • 渲染器的基本概念
    • 自定义渲染器

渲染器与响应式系统的结合

渲染器与响应式系统是相辅相成的,渲染器负责将响应式系统中的响应式数据渲染到视图中,而响应式系统则负责监听数据的变化并通知渲染器进行更新。

渲染器在浏览器平台中,用它渲染其中的真实 DOM 元素,渲染器不仅能够渲染真实 DOM 元素,它还是 Vue 框架能实现跨平台的关键。

目前我们不考虑跨平台,限定在浏览器中,我们可以先写个简单的渲染器,如下:

function renderer(domString, container){container.innerHTML = domString
}

使用如下:

renderer(`<div>hello world</div>`, document.querySelector('#app'))

这就是一个标准的渲染器实现,它会将获取的 dom 作为容器,并设置他的 html。

而如果搭配响应系统则可以实现数据更新重新渲染视图,如下:

const count = ref(0)
effect(()=>{renderer(`<div>{{count.value}}</div>`, document.querySelector('#app'))
})
count.value++

渲染器的基本概念

通常使用 renderer 表示渲染器,而将 render 表示渲染

渲染器的作用就是把虚拟 DOM 渲染为特定平台上的真实元素,在浏览器平台上,就是渲染真实的 DOM 元素。

虚拟 DOM 通常用英文 virtual DOM 来表示,简写:vdom。虚拟 DOM 和真实 DOM 结构一样,都是由一个个节点组成的树形结构,所以我们也会经常听到“虚拟节点”这样的词汇,即 virtual node,简写:vnode。这棵树中的任何一个 vnode 都可以是一颗子树,因此 vnode 和 vdom 有时可以替换使用,为了避免造成困惑,后续统一采用 vnode 的说法。

渲染器把虚拟 DOM 节点渲染为真实 DOM 节点的过程叫做挂载,通过使用英文 mount 表示。

渲染器把真实 DOM 挂载到哪里?其实渲染器本身并不知道应该吧真实 DOM 挂载到哪里。因此渲染器通常还需要接收一个挂载点作为参数,即容器(container)。

我们使用代码的形式表达这个渲染器,如下:

function createRenderer(){function render(vnode, container){// ...}return render
}

这里来解释一下为什么需要 createRenderer,而不是直接定义 render 即可。

在前面我们提到,渲染器和渲染不是一个概念,渲染器是更加宽泛的概念,它包含渲染,渲染器不仅可以用来渲染,还可以用来激活已有的 DOM 元素,这个过程通常发生在同构渲染的情况下,例如:

function createRenderer(){function render(vnode, container){// ...}function hydrate(vnode, container){// ...}return {render,hydrate}
}

可以看到,这里又额外多了一个 hydrate,在 vue 中,hydrate 是关于服务端渲染的。通过这个案例可以表明,渲染器的案例是非常宽泛的。其中把 vnode 渲染为真实 DOM 的 render 函数只是其中的一部分。在 vue.js3 中,创建应用的 createApp 函数也是渲染器的一部分。

当有了这个渲染器之后,我们就可以用它来执行渲染任务了,如下:

const renderer = createRenderer()
// 首次渲染
renderer.render(vnode, document.querySelector('#app'))

在上面这段代码中,我们首先调用 createRenderer 创建了一个渲染器,然后使用 renderer.render 函数来执行渲染。当首次调用 renderer.render 函数时,只需要创建新的 DOM 元素即可,这个过程只涉及挂载。

而当多次在同一个 container 上调用 renderer.render 函数进行渲染时,渲染器除了要执行挂载的动作为之外,还需要执行更新操作,例如:

const renderer = createRenderer()
// 首次渲染
renderer.render(oldVnode, document.querySelector('#app'))
// 第二次渲染
renderer.render(newVnode, document.querySelector('#app'))

如上面的代码所示,当首次渲染已经将 oldVnode 挂载到 container 内了,当再次调用 // 首次渲染
renderer.render 函数并尝试渲染 newVnode 时,就不能在简单的执行挂载操作,这种全量更新时非常的消耗性能的。这种情况下,渲染器会使用 newVnode 与上一次的 oldVnode 进行比较,试图找到并更新变更点,这个过程叫做“打补丁(或更新)”,英文通常使用 patch 来表示,实际上,挂载的动作也可以看做为一种特殊的打补丁,它的特殊之处就在于旧的 vnode 是不存在的,代码示例如下:

function createRenderer(){function render(vnode, container){if(vnode){// 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁patch(container._vnode, vnode, container)   } else {if(container._vnode){// 旧的 vnode 存在,且新的 vnode 不存在,则表示是卸载(unmount)操作// 只需要将 container 内的 dom 清空即可container.innerHTML = ''}// 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnodecontainer._vnode = vnode}}return {render}
}

根据这段代码中 render 函数的基础实现,我们可以配合下面的代码分析其执行流程,如下:

const renderer = createRenderer()
// 首次渲染
renderer.render(vnode1, document.querySelector('#app'))
// 第二次渲染
renderer.render(vnode2, document.querySelector('#app'))
// 第三次渲染
renderer.render(null, document.querySelector('#app'))

执行过程如下:

  1. 在首次渲染的时,执行挂载,并将 vnode1 存储到容器元素 container._vnode 属性中,在后续作为旧的 vnode 使用。
  2. 在第二次渲染时,旧 vnode 存在,此时渲染器会把 vnode2 作为新 vnode,并将旧的 vnode 一起传递给 patch 函数进行打补丁。
  3. 第三次渲染时,新 vnode 为 null,但是容器中渲染的是 vnode2 所描述的内容,所以会清空容器。代码中使用的是 container.innerHTML = ‘’ 清空,但这只是一个暂时性的表达,实际这样清除会存在一些问题。

此外,在上述的描述中,我们可以初步得到 patch 函数的函数签名,如下:

patch(oldVnode, newVnode, container)

虽然我们还没实现 patch 函数,实际上 patch 作为渲染器的核心入口,存在大量的代码逻辑,这里只对其做一个初步的解释,如下:

function patch(n1, n2, container){//...
}

n1 表示旧节点,n2 表示新节点,container 表示容器。

自定义渲染器

我们从一个普通的 h1 标签开始,使用 vnode 对象来描述一个 h1 标签:

const vnode = {type: 'h1',children: 'hello'
}

观察这个对象,我们使用 type 来表示一个 vnode 的类型,不同类型的 type 属性值可以描述多种类型的 vnode。当 type 为属性值为字符串时,表示一个普通的 html 标签,并使用 type 属性的属性值作为标签的名称。对于这样的一个 vnode,我们可以使用 render 函数来渲染,如下:

const vnode = {type: 'h1',children: 'hello'
}
// 创建一个渲染器
const renderer = createRenderer()
// 调用 render 函数渲染该 vnode
renderer.render(vnode, document.querySelector('#app'))

为了完成这个渲染工作,我们需要补充 patch 函数,如下:

function createRenderer(){function patch(n1, n2, container){// 在这里编写逻辑}function render(vnode, container){if(vnode){patch(container._vnode, vnode, container)} else {if(container._vnode){// 卸载container.innerHTML = ''}container._vnode = vnode}}return {render}
}

patch 函数实现如下:

function patch(n1, n2, container) {// 如果 n1 不存在,则执行挂载,使用 mountElement 函数完成挂载if (!n1) {mountElement(n2, container)} else {//  todo 如果 n1 存在,则执行更新}
}

mountElement 函数如下:

function mountElement(vnode, container) {// 创建 dom 元素const el = document.createElement(vnode.type)// 处理子节点if (isString(vnode.children)) {// 如果是文本节点,则直接设置文本内容el.textContent = vnode.children}// 将 dom 元素添加到容器中container.appendChild(el)
}

我相信上述这些代码大家都是能看懂的,现在我们来分析一下这样处理存在的问题。

我们的目标是设计一个不依赖于浏览器平台的通用渲染器,但是很明显,mountElement 函数内调用了大量依赖浏览器的 API,如果想要这个渲染器变得通用,那么这些可以操作 DOM 的 API 就应该作为配置项,该配置项可以作为 createRender 函数的参数,如下:

const options = {// 创建元素createElement(tag){return document.createElement(tag)},// 设置元素的文本节点setElementText(el, text){el.textContent = text},// 用于在给定的 parent 下添加指定元素insert(el, parent, anchor = null){parent.insertBeforce(el, anchor)}
}const renderer = createRenderer(options)

可以看到,在上述的处理中,我们将操作 DOM 的 API 封装为了一个对象,并传递给 createRenderer,这样在 mountElement 等函数内部就可以通过配置项传递的操作 DOM 的方法来实现,并且这个操作的主动权可以交给用户,这里我们默认是浏览器平台。

而根据这个设计思想并参考 vue3 的源码,我们也需要对我们目前的渲染器做出一些调整,如下:

function createRenderer(options) {return baseCreateRenderer(options)
}function baseCreateRenderer(options) {const {createElement: hostCreateElement,setText: hostSetText,insert: hostInsert} = optionsfunction patch(n1, n2, container) {if (!n1) {mountElement(n2, container)} else {}}function mountElement(vnode, container) {const el = hostCreateElement(vnode.type)if (isString(vnode.children)) {hostSetText(el, vnode.children)}hostInsert(el, container)}function render(vnode, container) {if (vnode) {patch(container._vnode, vnode, container)} else {if (container._vnode) {container.innerHTML = ''}container._vnode = vnode}}return {render}
}

首先我们进行了一层隔离,渲染器逻辑并不直接在 createRenderer 编写,而是通过 baseCreateRenderer 函数返回,这样可以提高灵活性以及扩展性,也是为了后期适配不同平台做一次处理。

那么应该如何使用呢?这里在 vue 中实在 runtime-dom 这个文件夹中来使用的,这个模块表示是专注服务于浏览器平台的,如果你的平台就是浏览器,则直接使用这个默认配置即可,所以我们这里也进行模块的分离,代码如下:

import { createRenderer } from '@vue/runtime-core'
import { nodeOps } from './nodeOps'// 传递给渲染器操作 dom 的配置-目前来说是只包含这点
const rendererOptions = nodeOpslet renderer// 保证渲染器存在
function ensureRenderer() {return renderer || (renderer = createRenderer(rendererOptions))
}export const render = (...args) => {ensureRenderer().render(...args)
}

通过这个导入也可以看出,我们需要去编写一下 nodeOps 的代码,如下:

const doc = documentexport const nodeOps = {createElement(tag) {return doc.createElement(tag)},setText(el, text) {el.textContent = text},insert(el, parent, anchor = null) {parent.insertBefore(el, anchor)}
}

那么我们来编写一段测试代码,看看是否能够正常执行,如下:

const vnode = {type: 'h1',children: 'hello'
}
// 我们已经将 render 函数单独抽离出来,所以我们只需要直接调用即可
render(vnode, document.querySelector('#app'))

结果如图:

在这里插入图片描述

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

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

相关文章

大数据-184 Elasticsearch - 原理剖析 - DocValues 机制原理 压缩与禁用

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

在 Docker 中搭建 PostgreSQL16 主从同步环境

1. 环境搭建 本文介绍了如何在同一台机器上使用 Docker 容器搭建 PostgreSQL 的主从同步环境。通过创建互联网络和配置主库及从库&#xff0c;详细讲解了数据库初始化、角色创建、数据同步和验证步骤。主要步骤包括设置主库的连接信息、创建用于复制的角色、使用 pg_basebacku…

成都跃享未来教育咨询有限公司抖音小店新生态

在数字化浪潮席卷全球的今天&#xff0c;教育行业正经历着前所未有的变革与升级。作为一座历史悠久而又充满活力的城市&#xff0c;成都凭借其深厚的文化底蕴和前瞻性的发展眼光&#xff0c;孕育了众多创新型企业。其中&#xff0c;成都跃享未来教育咨询有限公司&#xff08;以…

计算机专业大学四年的学习路线(非常详细),零基础入门到精通,看这一篇就够了

前言 许多学子选择踏上计算机这条充满挑战与机遇的道路。但在大学四年中&#xff0c;如何规划自己的学习路线&#xff0c;才能在毕业时脱颖而出&#xff0c;成为行业的佼佼者呢&#xff1f; 第一学年&#xff1a;基础知识的奠基 1.1 课程安排 在大学的第一年&#xff0c;重…

spark:Structured Streaming介绍

文章目录 1. Structured Streaming介绍1.1 实时计算和离线计算1.1.1 实时计算1.1.2 离线计算 1.2 有界和无界数据 2. 简单使用3. 编程模型4. 数据处理流程4.1 读取数据Source4.1.1 文件数据处理 4.2 计算操作 Operation4.3 数据输出 Sink4.3.1 输出模式4.3.2 指定输出位置4.3.3…

JVM篇(运行时数据区(实战课程学习总结)

目录 学习前言 一、运行时数据区 1. JVM运行时数据区规范 2. Hotspot运行时数据区 3. 分配JVM内存空间 分配堆的大小 分配方法区的大小 分配线程空间的大小 二、程序计数器 1. 作用 2. 存储的数据 3. 异常 三、Java虚拟机栈 1. 栈帧 1.1. 局部变量表 存储内容 …

【已解决】C# NPOI如何在Excel文本中增加下拉框

前言 上图&#xff01; 解决方法 直接上代码&#xff01;&#xff01;&#xff01;&#xff01;综合了各个大佬的自己修改了一下&#xff01;可以直接规定在任意单元格进行设置。 核心代码方法块 #region Excel增加下拉框/// <summary>/// 增加下拉框选项/// </s…

12. 命令行

Hyperf 的命令行默认由 hyperf/command 组件提供&#xff0c;而该组件本身也是基于 symfony/console 的抽象。 一、安装 通常来说该组件会默认存在&#xff0c;但如果您希望用于非 Hyperf 项目&#xff0c;也可通过下面的命令依赖 hyperf/command 组件。 composer require hype…

使用 Spring 框架构建 MVC 应用程序:初学者教程

Spring Framework 是一个功能强大、功能丰富且设计精良的 Java 平台框架。它提供了一系列编程和配置模型&#xff0c;旨在简化和精简 Java 中健壮且可测试的应用程序的开发过程。 人们常说 Java 太复杂了&#xff0c;构建简单的应用程序需要很长时间。尽管如此&#xff0c;Jav…

Unity/C#使用EPPlus读取和写入Excel

简介&#xff1a;本篇使用EPPlus来将数据写入Excel&#xff0c;如果需要使用NPOI那可以阅读我之前文档使用NPOI创建及写入数据_npoi 模板 写数据-CSDN博客 一、安装EPPlus 这里使用 .unitypackage 文件形式安装 1.1下载NuGetForUnity.unitypackage github进行搜索下载 下载…

vscode设置特定扩展名文件的打开编码格式

用vscode 编辑c语言或者Verilog代码, 由于其它开发工具的文件编码格式无法修改,默认只能是gb2312, 与我们国内奉行的统一 utf8 不一致. 所以只能是更改特殊文件的打开方式. 配置方式如下. 关键配置如下: {"git.openRepositoryInParentFolders": "never",…

【办公类-57-01】美工室材料报销EXCEL表批量插入截图(图片)

背景需求&#xff1a; 我们班分到美工室&#xff0c;需要准备大量材料&#xff0c;根据原始的报销单EXCLE&#xff0c;里面有商品名称、图片、链接、单位、数量等信息 今天我和搭档一起填写新表&#xff0c;发现手机截图的图片插入EXCEL后非常大&#xff0c; 需要手动调整图片…

4 -《本地部署开源大模型》在Ubuntu 22.04系统下部署运行ChatGLM3-6B模型

在Ubuntu 22.04系统下部署运行ChatGLM3-6B模型 大模型部署整体来看并不复杂&#xff0c;且官方一般都会提供标准的模型部署流程&#xff0c;但很多人在部署过程中会遇到各种各样的问题&#xff0c;很难成功部署&#xff0c;主要是因为这个过程会涉及非常多依赖库的安装和更新及…

【AI】人工智能的应用与前景

目录 人工智能的应用与前景1. 人工智能的当前应用1.1 医疗领域1.1.1 医疗影像中的AI应用1.1.2 药物研发中的AI潜力 1.2 企业与商业领域1.2.1 数据驱动的智能决策1.2.2 自动化业务流程 1.3 智能生活领域1.3.1 自动驾驶技术的崛起1.3.2 智能家居中的AI应用 2. 人工智能的未来前景…

Golang | Leetcode Golang题解之第485题最大连续1的个数

题目&#xff1a; 题解&#xff1a; func findMaxConsecutiveOnes(nums []int) (maxCnt int) {cnt : 0for _, v : range nums {if v 1 {cnt} else {maxCnt max(maxCnt, cnt)cnt 0}}maxCnt max(maxCnt, cnt)return }func max(a, b int) int {if a > b {return a}return …

区块链技术在网络安全中的应用研究

摘要&#xff1a; 随着网络技术的快速发展&#xff0c;网络安全问题日益凸显。区块链技术以其去中心化、不可篡改、可追溯等特性&#xff0c;为网络安全提供了新的解决方案。本文深入探讨了区块链技术在网络安全多个领域的应用&#xff0c;包括数据加密与存储、身份认证、网络攻…

Golang | Leetcode Golang题解之第494题目标和

题目&#xff1a; 题解&#xff1a; func findTargetSumWays(nums []int, target int) int {sum : 0for _, v : range nums {sum v}diff : sum - targetif diff < 0 || diff%2 1 {return 0}neg : diff / 2dp : make([]int, neg1)dp[0] 1for _, num : range nums {for j …

HICP--2

在area 0的路由器只生成 area 0 的数据库&#xff0c;只在area 1 的一样。但是既在又在的生成两个 area的 LSDB 一、区域间三类LSA 在OSPF&#xff08;Open Shortest Path First&#xff09;协议中&#xff0c;区域间三类LSA&#xff08;Link-State Advertisement&#xff09…

基于Java+jsp的CRM客户关系管理系统的实现

系统的详细设计和实现 根据上文的功能分析和数据库的分析&#xff0c;在系统的实现阶段上采用当今开源的SSH&#xff08;StrutsHibernateSpring&#xff09;整合框架实现。其目的是降低个模块间的耦合度&#xff0c;使各个模块之间的功能相互独立、模块内部结构清晰。 系统架…

听泉鉴宝在三个月前已布局商标注册!

近日“听泉鉴宝”以幽默的风格和节目效果迅速涨粉至2500多万&#xff0c;连线出现“馆藏文物”和“盗墓现场”等内容&#xff0c;听泉鉴宝早在几个月前已布局商标注册。 据普推知产商标老杨在商标局网站检索发现&#xff0c;“听泉鉴宝”的主人丁某所持股的江苏灵匠申请了三十…