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

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

本节,我们暂时将渲染器限定在 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 下,即后续渲染中的旧 vnodecontainer._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 的 APIconst { createElement, setElementText, insert } = optionsfunction 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,再通过支持个性化配置的能力来实现跨平台。

完整代码如下:

// 调用createRenderer函数,创建渲染器
const renderer = createRenderer({createElement(tag) {console.log(`创建元素 ${tag}`)return document.createElement(tag)},setElementText(el, text) {console.log(`设置${JSON.stringify(el)}的文本内容:${text}`)el.textContent = text},insert(el, parent, anchor = null) {console.log(`${JSON.stringify(el)}添加到:${JSON.stringify(parent)}`)parent.insertBefore(el, anchor)}
})// 渲染器函数
function createRenderer(options) {const { createElement, setElementText, insert } = options// 挂载元素function mountElement(vnode, container) {// 创建元素const el = createElement(vnode.type)// 处理子节点,如果子节点是字符串,只需要设置textContent的值即可if (typeof vnode.children === 'string') {setElementText(el, vnode.children)}// 将元素插入到容器中insert(el, container)}// 更新(打补丁)// n1:旧 vnode// n2:新 vnode// container:容器function patch(n1, n2, container) {// 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载if (!n1) {mountElement(n2, container)} else {// n1 存在,意味着打补丁,暂时省略}}// 渲染function render(vnode, container) { if (vnode) {patch(container._vnode, vnode, container)} else {if (container._vnode) {container.innerHTML = ''}}container._vnode = vnode}return {render}
}// 测试
const vnode = {type: 'h1',children: 'hello'
}
renderer.render(vnode, document.getElementById('app'))

效果如下:
请添加图片描述

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

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

相关文章

用js快速生成一个简单的css原子库 例如: .mr-18 .pl-18

第三方css原子库的缺点 比如 tailwindcss&#xff0c;有学习成本最开始写的时候效率可能还没有我们自己手写效率高&#xff0c;需要配置&#xff0c;会有原始样式被覆盖的问题&#xff1b;总之就是一个字重 自己搓的优点 学习成本低灵活不会有副作用 <!DOCTYPE html>…

Linux 终端操作命令(2)内部命令

Linux 终端操作命令 也称Shell命令&#xff0c;是用户与操作系统内核进行交互的命令解释器&#xff0c;它接收用户输入的命令并将其传递给操作系统进行执行&#xff0c;可分为内部命令和外部命令。内部命令是Shell程序的一部分&#xff0c;而外部命令是独立于Shell的可执行程序…

Kafka API与SpringBoot调用

文章目录 首先需要命令行创建一个名为cities的主题&#xff0c;并且创建该主题的订阅者。 1、使用Kafka原生API1.1、创建spring工程1.2、创建发布者1.3、对生产者的优化1.4、批量发送消息1.5、创建消费者组1.6 消费者同步手动提交1.7、消费者异步手动提交1.8、消费者同异步手动…

Qt与电脑管家2

1.竖线的添加与样式的修改&#xff1a; color: rgb(238, 238, 238); 2. 通过修改之前的自定义btn类代码&#xff0c;可以比较容易地创造出各种各样的按钮类 头像的样式表代码&#xff1a; QPushButton{ border-image: url(:/images_up/icon.jpg); border-radius:20px; } QPu…

Expo项目 使用Native base UI库

装包&#xff1a; yarn add native-base expo install react-native-svg12.1.1 Index.js: import React from react import { View, Text } from react-native import useList from ./useList import { NativeBaseProvider, Button, Box } from native-base import styles f…

ArcGIS Pro技术应用(暨基础入门、制图、空间分析、影像分析、三维建模、空间统计分析与建模、python融合)

GIS是利用电子计算机及其外部设备&#xff0c;采集、存储、分析和描述整个或部分地球表面与空间信息系统。简单地讲&#xff0c;它是在一定的地域内&#xff0c;将地理空间信息和 一些与该地域地理信息相关的属性信息结合起来&#xff0c;达到对地理和属性信息的综合管理。GIS的…

Spannable配合AnimationDrawable实现TextView中展示Gif图片

辣的原理解释&#xff0c;反正大家也不爱看&#xff0c;所以直接上代码了 长这样&#xff0c;下面两个图是gif&#xff0c;会动的。 package com.example.myapplication;import android.content.Context; import android.graphics.Bitmap; import android.graphics.drawable…

[静态时序分析简明教程(九)]多周期路径set_multicycle_path

静态时序分析简明教程-多周期路径 一、写在前面1.1 快速导航链接 二、多周期路径2.1 多周期路径的SDC命令2.2 路径常规约束2.3 建立/保持规格2.4 位移量2.5 多时钟周期案例 三、总结 一、写在前面 一个数字芯片工程师的核心竞争力是什么&#xff1f;不同的工程师可能给出不同的…

山景DSP芯片可烧录AP8224C2音频处理器方案

AP8224C2高性能32位音频应用处理器AP82系列音频处理器是面向音频应用领域设计的新一代SoC平台产品&#xff0c;适用于传统音响系统、新兴的蓝牙或Wifi 无线音频产品、Sound Bar 和调音台等市场。该处理器在总体架构和系统组成上&#xff0c;充分考虑了音频领域的特点&#xff0…

MYSQL幻读问题

幻读是什么&#xff1f; “Phantom Problem是指在同一事务下&#xff0c;连续执行两次同样的SQL语句可能导致不同的结果&#xff0c;第二次的SQL语句可能会返回之前不存在的行。”摘录来自 MySQL技术内幕&#xff1a;InnoDB存储引擎(第2版) (数据库技术丛书) ​ 通俗来说就是&a…

【积水成渊】9 个CSS 伪元素

大家好&#xff0c;我是csdn的博主&#xff1a;lqj_本人 这是我的个人博客主页&#xff1a; lqj_本人_python人工智能视觉&#xff08;opencv&#xff09;从入门到实战,前端,微信小程序-CSDN博客 最新的uniapp毕业设计专栏也放在下方了&#xff1a; https://blog.csdn.net/lbcy…

Jupyter Notebook 500 : Internal Server Error

1. 这个问题的根本原因在于&#xff1a; pygments 包 版本过高。 安装pygments 2.6.1 2.jupyter版本如下 如果某个版本有冲突&#xff0c;卸载了重新安装一下就行。 安装命令&#xff1a; pip install pygments 2.6.1 -i https://pypi.tuna.tsinghua.edu.cn/simple 另外…

界面控件DevExpress WPF Chart组件——拥有超快的数据可视化库!

DevExpress WPF Chart组件拥有超大的可视化数据集&#xff0c;并提供交互式仪表板与高性能WPF图表库。DevExpress Charts提供了全面的2D / 3D图形集合&#xff0c;包括数十个UI定制和数据分析/数据挖掘选项。 PS&#xff1a;DevExpress WPF拥有120个控件和库&#xff0c;将帮助…

Mysql中插入数据,并返回自增主键的值

创建数据库和表使用 insert into 进行插入数据使用 RETURN_GENERATED_KEYS 进行返回插入的这条数据 具体方法如下&#xff1a; Testvoid addGetPk(){try{Statement stmt conn.createStatement();String sql String.format("insert into t_students values(null,%s,%s,%d…

「已解决」iframe 本地生效 但是在测试环境不生效问题

背景 我有一个表格中一列是个详情&#xff0c;这个详情可被点击&#xff0c;点击后弹出抽屉&#xff0c;抽屉里是后端传给我详情字段的值对应的 url 的 iframe 展示。 问题是&#xff0c;在本地 localhost 下运行&#xff0c;ifame 运行正常&#xff0c;但是部署到测试环境就看…

王道机组难题分析

第四章 指令系统 大端方式&#xff1a;就是高地址存放高位&#xff0c; LSB的意思是&#xff1a;全称为Least Significant Bit&#xff0c;在二进制数中意为最低有效位 MSB的意思是&#xff1a;全称为Most Significant Bit&#xff0c;在二进制数中属于最高有效位 操作数可以理…

JavaWeb-Servlet服务连接器(三)

目录 Response响应对象 1.基本功能 2.重定向 3.路径 4.服务器输出数据到浏览器 Response响应对象 1.基本功能 设置响应行&#xff1a;格式为 HTTP/1.1 200 OK&#xff0c;可以使用 setStatus(int sc) 方法设置状态码为 200 表示成功。 方法名称描述setStatus(int sc)设…

docker下载和案例

文章目录 Docker安装一,根据官方文档安装二,根据我以下方式 Docker配置错误导致漏洞一,CRLF注入漏洞介绍在nginx中该漏洞例子解决方法 目录穿越漏洞介绍解决方法 Docker安装 一,根据官方文档安装 官方文档 二,根据我以下方式 docker安装要求&#xff1a; Docker要求Ce…

用户数据报协议UDP

UDP的格式 载荷存放的是:应用层完整的UDP数据报 报头结构: 源端口号:发出的信息的来源端口目的端口号:信息要到达的目的端口UDP长度:2个字节(16位),即UDP总长度为:2^16bit 2^10bit * 2^6bit 1KB * 64 64KB.所以一个UDP的最大长度为64KBUDP校验和:网络的传输并非稳定传输,…

【leetcode】前缀和

内容摘抄自&#xff1a; 小而美的算法技巧&#xff1a;前缀和数组 | labuladong 的算法小抄 一维数组的前缀和 看这个 preSum 数组&#xff0c;若想求索引区间 [1, 4] 内的所有元素之和&#xff0c; 就可以通过 preSum[5] - preSum[1] 得出。 class NumArray {private:// 前缀…