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 下,即后续渲染中的旧 vnode
      container._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
  } = options

  function 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 = nodeOps

let renderer

// 保证渲染器存在
function ensureRenderer() {
  return renderer || (renderer = createRenderer(rendererOptions))
}

export const render = (...args) => {
  ensureRenderer().render(...args)
}

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

const doc = document

export 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'))

结果如图:

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第13篇:无线与移动网络安全

目录 引言 13.1 无线网络的安全威胁 13.2 无线局域网的安全协议 13.3 移动通信中的安全机制 13.4 蓝牙和其他无线技术的安全问题 13.5 无线网络安全的最佳实践 13.6 总结 第13篇&#xff1a;无线与移动网络安全 引言 无线和移动网络的发展为我们的生活带来了极大的便利…

爱快路由器配置腾讯云动态域名DDNS详细说明

直白点说就是让爱快路由器自动配置当前公网IP地址给域名&#xff0c;动态域名DDNS不清楚的请自行百度&#xff0c; 这里就可以看见操作日志&#xff0c;那么我们一步一步来配置它吧&#xff0c;首先登录爱快路由器&#xff0c;如下图&#xff1a; 那么腾讯云我们怎么找到ID和…

使用python自制桌面宠物,好玩!——枫原万叶桌宠,可以直接打包成exe去跟朋友炫耀。。。

大家好&#xff0c;我是小黄。 今天我们使用python实现一个桌面宠物。只需要gif动态图片就行。超级简单容易上手。 #完整源代码可在下方图片免费获取 一&#xff1a;下载相关的库文件。 我们本次使用到的库文件为&#xff1a;tkinter和pyautogui 下载命令&#xff1a; pip…

做一个简单的图片验证码生成,避免被 ai 简单识别出文本

做一个简单的图片验证码生成&#xff0c;避免被 ai 简单识别出文本 缘由腾讯云的收费标准网易的收费标准 编写一个图片验证码生成c# 示例 缘由 在很多场合&#xff0c;我们会对用户进行一个真实性人工验证&#xff0c;避免各种注册机、机器人之类的&#xff0c;对我们的正常工…

力扣143.重排链表

通过画图分析&#xff0c;可以从反转后链表与原链表中按顺序各自取一个结点来构建结果&#xff0c;不过要注意的这两个链表只会用到一半&#xff0c;结合 链表的中间结点 和 反转链表 进行解题。 /*** Definition for singly-linked list.* struct ListNode {* int val;* …

nginx在access日志中记录请求头和响应头用作用户身份标识分析

在应用系统中&#xff0c;有时将请求的用户信息和身份认证信息放到请求头中&#xff0c;服务器认证通过后&#xff0c;通过cookie返回客户端一个标识&#xff0c;在后续的请求时&#xff0c;客户端需要带上这个cookie&#xff0c;通过这个cookie&#xff0c;服务器就知道请求的…

如何将数据从 AWS S3 导入到 Elastic Cloud - 第 2 部分:Elastic Agent

作者&#xff1a;来自 Elastic Hemendra Singh Lodhi 了解将数据从 AWS S3 提取到 Elastic Cloud 的不同选项。 这是多部分博客系列的第二部分&#xff0c;探讨了将数据从 AWS S3 提取到 Elastic Cloud 的不同选项。 在本博客中&#xff0c;我们将了解如何使用 Elastic Agent…

【C++】进阶:类相关特性的深入探讨

⭐在对C 中类的6个默认成员函数有了初步了解之后&#xff0c;现在我们进行对类相关特性的深入探讨&#xff01; &#x1f525;&#x1f525;&#x1f525;【C】类的默认成员函数&#xff1a;深入剖析与应用&#xff08;上&#xff09; 【C】类的默认成员函数&#xff1a;深入剖…

Linux基础知识和常用基础命令

家目录 每个用户账户的专用目录。家目录的概念为用户提供了一个独立的工作空间&#xff0c;它是用户在文件系统中的主要工作区域&#xff0c;包含了用户的个人文件、配置文件和其他数据。 家目录通常位于 /home/用户名 路径下。例如&#xff0c;如果用户名为 1&#xff0c;那…

[Windows] 很火的开源桌面美化工具 Seelen UI v2.0.2

最近&#xff0c;一款来自Github的开源桌面美化工具突然在网上火了起来&#xff0c;引发了大家的关注&#xff0c;不少小伙伴纷纷开始折腾了起来。而折腾的目的&#xff0c;无非是为了一点点乐趣而已&#xff0c;至于结果如何&#xff0c;并不是最要紧的&#xff0c;反倒是体验…

音频声音怎么调大?将音频声音调大的几个简单方法

音频声音怎么调大&#xff1f;在现代生活中&#xff0c;音频内容无处不在&#xff0c;从在线课程和播客到音乐和电影&#xff0c;音频已经成为我们获取信息和娱乐的重要方式。然而&#xff0c;许多人在使用音频时可能会遇到一个常见问题&#xff1a;音频声音太小&#xff0c;无…

组件通信八种方式(vue3)

一、父传子&#xff08;props&#xff09; 关于Props的相关内容可以参考&#xff1a;Props-CSDN博客 父组件通过 props 向子组件传递数据。适合简单的单向数据流。 <!-- Parent.vue --> <template><Child :message"parentMessage" /> </temp…

2018年-2020年 计算机技术专业 程序设计题(算法题)实战_数组回溯法记录图的路径

阶段性总结&#xff1a; 树的DFS存储一条路径采用定义一个栈的形式 图的DFS和BFS&#xff0c;存储一条路径 采用数组回溯法 文章目录 2018年1.c语言程序设计部分2. 数据结构程序设计部分 2019年1.c语言程序设计部分2. 数据结构程序设计部分 2020年1.c语言程序设计部分2. 数据结…

基于微信小程序的智能校园社区服务推荐系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

Kimi 自带的费曼学习器,妈妈再也不用担心我的学习了

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 AI工具集1&#xff1a;大厂AI工具【共23款…

【经管】比特币与以太坊历史价格数据集(2014.1-2024.5)

一、数据介绍 数据名称&#xff1a;比特币与以太坊历史价格数据集 频率&#xff1a;逐日 时间范围&#xff1a; BTC&#xff1a;2014/9/18-2024/5/1 ETH&#xff1a;2017/11/10-2024/5/1 数据格式&#xff1a;面板数据 二、指标说明 共计7个指标&#xff1a;Date、Open…

安装vue发生异常: idealTree:nodejs: sill idealTree buildDeps

一、异常 C:\>npm install vue -g npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIREDnpm ERR! request to https://registry.npm.taobao.org/vue failed, reason: certificate has expired 二、原因 请求 https://registry.npm.taobao.org 失败&#xff0c;证…

通义灵码:融合创新玩法与探索,重塑LeetCode解题策略

文章目录 关于通义灵码安装指南 通义灵码与LeetCode的结合通义灵码给出优化建议通义灵码给出修改建议通义灵码给出自己的思路 总结 大家好&#xff0c;欢迎大家来到工程师令狐小哥的频道。都说现在的时代是AI程序员的时代。AI程序员标志着程序员的生产力工具已经由原来的搜索式…

JavaSE之String类

文章目录 一、String类常用的构造方法二、常见的四种String对象的比较1.使用比较2.使用equals()方法比较3.使用compareTo()方法比较4.使用compareToIgnoreCase()方法比较 三、字符串的查找四、字符串的转化1.数字和字符串间的转化2.大小写转化3.字符串和数组间的转化 五、字符串…

grafana 配置prometheus

安装prometheus 【linux】麒麟v10安装prometheus监控&#xff08;ARM架构&#xff09;-CSDN博客 登录grafana 访问地址&#xff1a;http://ip:port/login 可以进行 Grafana 相关设置&#xff08;默认账号密码均为 admin&#xff09;。 输入账户密码 添加 Prometheus 数据源…