vuejs 设计与实现 - 渲染器的设计

news2024/9/23 1:29:26

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

本节,我们暂时将渲染器限定在 DOM 平台。既然渲染器用来渲染真实 DOM 元素,那么严格来说,下面的函数就是一个合格的渲染器:

// 渲染器:
function renderer(domString, container) {
   container.innerHTML = domString
}

使用渲染器:

renderer('<h1>Hello</h1>', document.getElementById('app'))

如果页面中存在 id 为 app 的 DOM 元素,那么上面的代码就会将

hello

插入到该 DOM 元素内。

当然,我们不仅可以渲染静态的字符串,还可以渲染动态拼接的 HTML 内容,如下所示:

let count = 1
renderer(`<h1>${count}</h1>`, document.getElementById('app'))

这样,最终渲染出来的内容将会是

1

。注意上面这段 代码中的变量 count,如果它是一个响应式数据,会怎么样呢?这让 我们联想到副作用函数和响应式数据。 利用响应系统,我们可以让整 个渲染过程自动化:

const count = ref(1)

 effect(() => {
   renderer(`<h1>${count.value}</h1>`,document.getElementById('app'))
 })

 count.value++

在这段代码中,我们首先定义了一个响应式数据 count,它是一 个 ref,然后在副作用函数内调用 renderer 函数执行渲染。副作用 函数执行完毕后,会与响应式数据建立响应联系。当我们修改 count.value 的值时,副作用函数会重新执行,完成重新渲染。所以 上面的代码运行完毕后,最终渲染到页面的内容是 <h1>2</h1>。

这就是响应系统和渲染器之间的关系。我们利用响应系统的能 力,自动调用渲染器完成页面的渲染和更新。这个过程与渲染器的具 体实现无关,在上面给出的渲染器的实现中,仅仅设置了元素的 innerHTML 内容。

渲染器的每本概念

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

渲染器把虚拟 DOM 节点渲染为真实 DOM 节点的过程叫作 挂载(mount)。
Vue.js 组件中的 mounted 钩子就会在挂载完成时触发。这就意味着,在 mounted 钩子中可以访问真实DOM 元素。

那么,渲染器把真实 DOM 挂载到哪里呢?其实渲染器并不知道应该把真实 DOM 挂载到哪里因此,渲染器通常需要接收一个挂载点作为参数,用来指定具体的挂载位置。这里的“挂载点”其实就是一个DOM 元素,渲染器会把该 DOM 元素作为容器元素,并把内容渲染到其中。我们通常用英文 container 来表达容器。


// createRenderer:创建渲染器
function createRenderer() {
	
	// render:渲染函数
	// vnode:真实dom
	// container:具体的挂载位置
	function render(vnode, container) {
	}
	
	return render
}

有了渲染器,我们就可以用它来执行渲染任务了,如下面的代码所示:


// createRenderer 函数创建 一个渲染器
const renderer = createRenderer()

// 首次渲染
// renderer.render 函数执行渲染
renderer.render(vnode, document.querySelector('#app'))

渲染器除了要执行挂载动作外,还要执行更新动作。例如:

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 {
             // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
             if (container._vnode) {
                 unmount(container._vnode)
             }
         }
         // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
         container._vnode = vnode
     }
}

上面这段代码给出了 render 函数的基本实现.我们可以配合下 面的代码分析其执行流程,从而更好地理解 render 函数的实现思路。假设我们连续三次调用 renderer.render 函数来执行渲染:

const renderer = createRenderer()

// 首次渲染
renderer.render(vnode1, document.querySelector('#app'))

// 第二次渲染
renderer.render(vnode2, document.querySelector('#app'))
// 第三次渲染
renderer.render(null, document.querySelector('#app'))
  • 在首次渲染时,渲染器会将 vnode1 渲染为真实 DOM。渲染完成 后,vnode1 会存储到容器元素的 container._vnode 属性 中,它会在后续渲染中作为旧 vnode 使用
  • 在第二次渲染时,旧 vnode 存在,此时渲染器会把 vnode2 作为 新 vnode,并将新旧 vnode 一同传递给 patch 函数进行打补 丁
  • 在第三次渲染时,新 vnode 的值为 null,即什么都不渲染。但 此时容器中渲染的是 vnode2 所描述的内容,所以渲染器需要清 空容器。从上面的代码中可以看出,我们使用unmount 卸载节点。

另外,在上面给出的代码中,我们注意到 patch 函数的签名,如 下:

patch(container._vnode, vnode, container)

patch 函数是整个渲染器的 核心入口,它承载了最重要的渲染逻辑,我们会花费大量篇幅来详细 讲解它,但这里仍有必要对它做一些初步的解释。
patch 函数至少接 收三个参数:

  • 第一个参数 n1:旧 vnode
  • 第二个参数 n2:新 vnode。
  • 第三个参数 container:容器。
function patch(n1, n2, container) {}

在首次渲染时,容器元素的 container._vnode 属性是不存在的,即 undefined。这意味着,在首次渲染时传递给 patch 函数的第一个参数 n1 也是 undefined。这时,patch 函数会执行挂载动作,它会忽略 n1,并直接将 n2 所描述的内容渲染到容器中。从这一点可以看出,patch 函数不仅可以用来完成打补丁,也可以用来执行挂载。

自定义渲染器

渲染器不仅能够把虚拟 DOM 渲染为浏览器平台上的真实 DOM。通过将渲染器设计为可配置的“通用”渲染器,即可实现渲染到任意目标平台上。本节我们将以浏览器作为渲染的目标平台,编写一个渲染器,在这个过程中,看看哪些内容是可以抽象的,然后通过抽象,将浏览器特定的 API 抽离,这样就可以使得渲染器的核心不依赖于浏览器。在此基础上,我们再为那些被抽离的 API提供可配置的接口,即可实现渲染器的跨平台能力。

案例:
对于这样一个 vnode,我们可以使用 render 函数渲染它,如下面的代码所示:

const vnode = {
	type: 'h1',
	children: 'hello'
}
// 创建一个渲染器
const renderer = createRenderer()

// 调用 render 函数渲染该 vnode
renderer.render(vnode, document.querySelector('#app'))

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

function patch(n1, n2, container) {

	// 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载
	if(!n1) {
		mountElement(n2, container)
	} else {
		// n1 存在,意味着打补丁,暂时省略
	}
}

在上面这段代码中,第一个参数 n1 代表旧 vnode,第二个参数 n2 代表新 vnode。当 n1 不存在时,意味着没有旧 vnode,此时只需 要执行挂载即可。这里我们调用 mountElement 完成挂载,它的实现 如下:

function mountElement(vnode, container) {
	// 创建dom元素
	const el = document.createElement(vnode.type)
	
	// 处理子节点,如果子节点是字符串,代表元素具有文本节点
	if (typeof vnode.children === 'string') {
		// 因此只需要设置元素的 textContent 属性即可
		el.textContent = vnode.children
	}
	
	// 将元素添加到容器中
	container.appendChild(el)
}

挂载一个普通标签元素的工作已经完成。接下来,我们分析这段 代码存在的问题。我们的目标是设计一个不依赖于浏览器平台的通用 渲染器,但很明显,mountElement 函数内调用了大量依赖于浏览器 的 API,例如 document.createElement、el.textContent 以 及 appendChild 等。想要设计通用渲染器,第一步要做的就是将这 些浏览器特有的 API 抽离。怎么做呢?我们可以将这些操作 DOM 的 API 作为配置项,该配置项可以作为 createRenderer 函数的参数, 如下面的代码所示:


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

可以看到,我们把用于操作 DOM 的 API 封装为一个对象,并把 它传递给createRenderer 函数。这样,在 mountElement 等函数 内就可以通过配置项来取得操作 DOM 的 API 了:

 function createRenderer(options) {
	
 	// 通过 options 得到操作 DOM 的 API
  	const { createElement, setElementText, insert } = options
	function mountElement(vnode, container) {}
	function patch(v1, v2, container) {}
	function render(vnode, container) {}
	
	return {
		render
	}
 }

接着,我们就可以使用从配置项中取得的 API 重新实现 mountElement 函数:

function mountElement(vnode, container) {
	// 调用 createElement 函数创建元素
	const el = createElement(vnode.type)
	
	// 处理子节点,如果子节点是字符串,代表元素具有文本节点
	if (typeof vnode.children === 'string') {
		// 调用 setElementText 设置元素的文本节点
		setElementText(el, vnode.children)
	}
	
	// 调用 insert 函数将元素插入到容器内
	insert(el, container)
}

如上面的代码所示,重构后的 mountElement 函数在功能上没有 任何变化不同的是,它不再直接依赖于浏览器的特有 API 了。这意 味着,只要传入不同的配置项,就能够完成非浏览器环境下的渲染工 作。为了展示这一点,我们可以实现一个用来打印渲染器操作流程的 自定义渲染器,如下面的代码所示:

// 在创建 renderer 时传入配置项
const renderer = createRenderer({
	
	// 用于创建元素
	createElement(tag) {
		console.log(`创建元素 ${tag}`)
		return document.createElement(tag)
	},
	
	// 用于设置元素的文本节点
	setElementText(el.taxt) {
		console.log(`设置${JSON.stringify(el)}的文本内容:${text}`)
		el.textContent = text
	},
	
	// 用于在给定的 parent 下添加指定元素
	insert(el, parent, anchor = null) {
		console.log(`${JSON.stringify(el)}添加到:${JSON.stringify(parent)}`)
		parent.children = el
	}
})

这样,我们就实现了一个自定义渲染器,可以用下面这段代码来检测它的能力:

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

const container = { type: 'root' }
renderer.render(vnode, container)

在浏览器中的运行结果如下:
请添加图片描述

现在,我们对自定义渲染器有了更深刻的认识了。自定义渲染器并不是“黑魔法”,它只是通过抽象的手段,让核心代码不再依赖平台特有的 API,再通过支持个性化配置的能力来实现跨平台。

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

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

相关文章

中级课程-SSRF(CSRF进阶)

文章目录 成因危害挖掘 成因 危害 挖掘

JUC并发编程之线程锁(一)

目录 1.ReentrantLock(互斥锁) 2.ReentRantReaderWriterLock&#xff08;互斥读写锁&#xff09; 3.StampedLock&#xff08;无障碍锁&#xff09; 4.Condition&#xff08;自定义锁&#xff09; 5.LockSupport 问题引出&#xff1a; 由于传统的线程控制需要用到同步机制Sy…

LeetCode--HOT100题(23)

目录 题目描述&#xff1a;206. 反转链表&#xff08;简单&#xff09;题目接口解题思路代码 PS: 题目描述&#xff1a;206. 反转链表&#xff08;简单&#xff09; 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 LeetCode做题链接&…

剑指offer56-II.数组中数字出现的次数II

第一种方法非常简单&#xff0c;就是用一个HashMap&#xff0c;key是数组中元素的值&#xff0c;value是出现的次数&#xff0c;所以遍历一遍数组&#xff0c;如果map中没有这个元素就把它put进去value设为1&#xff0c;否则value加1&#xff0c;然后遍历一遍map&#xff0c;如…

tensorflow 1.14 的 demo 02 —— tensorboard 远程访问

tensorflow 1.14.0&#xff0c; 提供远程访问 tensorboard 服务的方法 第一步生成 events 文件&#xff1a; 在上一篇demo的基础上加了一句&#xff0c;如下&#xff0c; tf.summary.FileWriter("./tmp/summary", graphsess1.graph) hello_tensorboard_remote.py …

Celery嵌入工程的使用

文章目录 1.config 1.1 通过app.conf进行配置1.2 通过app.conf.update进行配置1.3 通过配置文件进行配置1.4 通过配置类的方式进行配置2.任务相关 2.1 任务基类(base)2.2 任务名称(name)2.3 任务请求(request)2.4 任务重试(retry) 2.4.1 指定最大重试次数2.4.2 设置重试间隔时间…

基础排障实验

排障实验要求&#xff1a; 确保重启后服务能正常访问。确保在客户机上&#xff0c;应用能够使用http://www.jxjz.com:8081访问。确保DNS能够解析邮件服务器mail.jxjz.com。确保DHCP能够保留地址192.168.100.200/24给MAC为ff-0a-ac-44-33-22的主机。确保client客户机能正常的分…

【mars3d - 报错】使用mars3d加载时的一些报错和不生效问题

在使用过程中遇到过很多报错&#xff0c;不管大的还是小的&#xff0c;在这里总结下&#xff0c;应该会持续更新&#xff1b; 1、设置贴地之后报错 可能是因为 arcType&#xff1a;Cesium.arcType.NONE 与 clampToGround&#xff1a;true 是相互冲突的&#xff0c;两个都设置就…

Ubuntu常用压缩指令总结

一、tar tar是Linux系统中最常用的压缩工具之一&#xff0c;它的一个优点是它可以保留文件的权限和所有权信息。tar可以创建.tar文件&#xff08;通常称为"tarball"&#xff09;&#xff0c;或者与gzip或bzip2等工具结合使用来创建.tar.gz或.tar.bz2文件。gzip工具的…

考研算法第40天:众数 【模拟,简单题】

题目 本题收获 又是一道比较简单的模拟题&#xff0c;就不说解题思路了&#xff0c;说一下中间遇到的问题吧&#xff0c;就是说cin输入它是碰到空格就停止输入的&#xff0c;详细的看下面这篇博客对于cin提取输入流遇到空格的问题_while(cin) 空格_就是那个党伟的博客-CSDN博…

【动态规划刷题 6】 删除并获得点数 粉刷房子

740. 删除并获得点数 给你一个整数数组 nums &#xff0c;你可以对它进行一些操作。 每次操作中&#xff0c;选择任意一个 nums[i] &#xff0c;删除它并获得 nums[i] 的点数。之后&#xff0c;你必须删除 所有 等于 nums[i] - 1 和 nums[i] 1 的元素。 开始你拥有 0 个点数。…

JavaScript实践:用Canvas开发一个可配置的大转盘抽奖功能

&#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;全栈领域新星创作者✌&#xff0c;阿里云社区专家博主&#xff0c;2023年6月csdn上海赛道top4。 &#x1f3c6;数年电商行业从业经验&#xff0c;历任核心研发工程师&#xff0c;项目技术负责人。 &#x1f3c6;本文已…

基于Qlearning强化学习的路径规划算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 Q值更新规则 4.2 基于Q-learning的路径规划算法设计 4.3 Q-learning路径规划流程 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 MATLAB2022A 3.部分核心程序 ..…

leetcode870. 优势洗牌(java)

优势洗牌 leetcode870. 优势洗牌题目描述双指针 排序代码 滑动窗口 leetcode870. 优势洗牌 难度 - 中等 leetcode870. 优势洗牌 题目描述 给定两个长度相等的数组 nums1 和 nums2&#xff0c;nums1 相对于 nums2 的优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描…

SecOC基础原理详解1

1、SecOC是什么&#xff1f; SecOC官方说法叫安全通讯模块。通俗一点就是发送CAN原始数据的时候进行加密&#xff0c;解析CAN原始数据的时候进行解密。 车上实施SecOC机制数据的ECU&#xff08;ECU是啥&#xff1f;可以百度一下&#xff09;&#xff0c;即使接收到了攻击性的…

如何将NAS空间变为本地磁盘,并拥有与实体硬盘所有常用功能

1.前言 作为一个垃圾佬&#xff0c;用自己的双路E5板子&#xff0c;加持PCIE拆分&#xff0c;4块PM983A齐上阵&#xff0c;气势可嘉。作为一个家庭NAS&#xff0c;如果仅仅用他当作一个普通的存储区存储数据那未免太过于浪费了&#xff1b;另一边&#xff0c;由于工作机硬盘太…

leetcode 子序列问题

718 最长重复子数组 此处求的是连续的子序列&#xff0c;使用动态规划进行求解。 使用dp[i][j]表示第1个序列前i个数字和第2个序列前j个数字的最大的重复子数组长度。 class Solution(object):def findLength(self, nums1, nums2):""":type nums1: List[int]:t…

Kotlin语法

整理关键语法列表如下&#xff1a; https://developer.android.com/kotlin/interop?hlzh-cn官方指导链接 语法形式 说明 println("count ${countnum}")字符串里取值运算 val count 2 var sum 0 类型自动推导 val 定义只读变量&#xff0c;优先 var定义可变变量…

Visual Studio在Release模式下设置代码的调试

Visual Studio在Release模式下设置代码的调试 Debug 模式下模型的加载速度、打开速度会降低很多&#xff0c;这里不推荐使用 Debug 模式进行调试。 Release 模式下可进行调试&#xff0c;使用 Release 模式调试&#xff0c;会提高模型打开速度、 加载速度、编译速度&#xff0c…

ChatGPT 6 月流量下滑 10%大模型遇到增长停滞,背后原因是什么?

近期数据显示&#xff0c;ChatGPT在过去的6个月中流量下滑了10%。这引发了对大模型发展是否达到瓶颈的疑问。我们对此进行了分析&#xff0c;并得出以下观点。首先&#xff0c;ChatGPT在实用性方面存在一些问题。它生成的文本内容往往过于模板化&#xff0c;句式和结构的同质性…