从认识 VNode VDOM 到实现 mini-vue

前言

现有框架几乎都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和 VDOM,那么为什么需要引入虚拟 DOM 呢?下面就一起来了解下吧!!!

VNode & VDOM

VNode 和 VDOM 是什么?

直接看 vue3 中关于 VNode 部分的源码,文件位置:packages\runtime-core\src\vnode.ts

通过源码部分,可以很明显的看到 VNode 本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个真实 dom.

VDOM 其实就是多个 VNode 组成的树结构,这就好比 HTML 元素和 DOM 树之间的关系:多个 HTML 元素能够组成树形结构就称之为 DOM 树.

function _createVNode(type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,props: (Data & VNodeProps) | null = null,children: unknown = null,patchFlag: number = 0,dynamicProps: string[] | null = null,isBlockNode = false
): VNode {...return createBaseVNode(type,props,children,patchFlag,dynamicProps,shapeFlag,isBlockNode,true)
}function createBaseVNode(type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,props: (Data & VNodeProps) | null = null,children: unknown = null,patchFlag = 0,dynamicProps: string[] | null = null,shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,isBlockNode = false,needFullChildrenNormalization = false
) {const vnode = {__v_isVNode: true,__v_skip: true,type,props,key: props && normalizeKey(props),ref: props && normalizeRef(props),scopeId: currentScopeId,slotScopeIds: null,children,component: null,suspense: null,ssContent: null,ssFallback: null,dirs: null,transition: null,el: null,anchor: null,target: null,targetAnchor: null,staticCount: 0,shapeFlag,patchFlag,dynamicProps,dynamicChildren: null,appContext: null} as VNode...return vnode
}

为什么要使用 VDOM ?

既然要使用肯定是因为 虚拟 DOM 拥有一些 真实 DOM 没有的优势:

  • 对真实元素节点抽象成 VNode,减少直接操作 dom 时的性能问题
    • 直接操作 dom 是有限制的,比如:diff、clone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
    • 直接操作 dom 容易引起页面的重绘和回流,但是通过 VNode 进行中间处理,可以避免一些不要的重绘和回流
  • 方便实现跨平台
    • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
    • 而且 Vue 允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台上的渲染

虚拟 DOM 的渲染过程

在这里插入图片描述

Vue 三大核心系统

Vue 中的三大核心系统如下:

  • Compiler 模块:涉及 AST 抽象语法树的内容,再通过 generate 将 AST 生成渲染函数,这里暂不实现
  • Runtime 模块:也可称为 Renderer 模块,将虚拟 dom 生成真实 dom 元素,并渲染到浏览器上
  • Reactivity 模块:响应式系统

三大系统的关系

在这里插入图片描述

实现 Runtime 模块

下面的实现部分只实现最简单、最核心的内容,不涉及各种复杂的边界条件.

createVNode & h

VNode 主要作用就是将外部传入的各种参数组合成一个 JavaScript 对象.

其中 createVNode 就是用于创建 VNode ,而 h 函数(render function)负责将创建好的 VNode 进行返回.

function createVNode(type, props, children) {// vnode ——> js 对象return {type,props,children}
}function h(type, props, children) {return createVNode(type, props, children)
}

mount

得到 VNode 之后,接下来就需要将 VNode变成真实的 dom元素,并渲染到浏览器上.

  • 通过 document.createElement方法将 VNode变成 dom元素

  • 处理传入的 props对象

    • on 开头的默认为事件,通过 addEventListenerdom元素注册事件
    • 其他属性默认为 dom上的属性,通过 setAttributedom元素设置属性
  • 处理 children,只考虑 childrenStringArray的情况

    • childrenString 默认为是文本节点,通过 textContent属性进行设置
    • childrenArray 默认为是多个 VNode集合,通过递归调用 mount方法进行挂载
function mount(vnode, container) {// 1. 获取容器 elementif (container.nodeType !== 1) {container = document.querySelector(container)}// 2. vnode ——> elementconst { type, props, children } = vnodeconst el = document.createElement(type)vnode.el = el// 3. 处理 propsif (props) {for (const key in props) {// 事件if (key.startsWith('on')) {el.addEventListener(key.slice(2).toLowerCase(), props[key]);} else {// 属性el.setAttribute(key, props[key])}}}// 4. 处理 childrenif (typeof children === 'string') {el.textContent = children} else {children.forEach(v => {mount(v, el)});}// 5. 挂载到容器中container.appendChild(el)
}

patch

实现了能够将 VNode 渲染为真实 DOM 之后,就需要考虑更新时 VNode 间的 diff 比较了,这就属于 patch 的过程.

  • 新旧 VNode 类型不一致,先删除旧节点,用新的替换旧的

  • 新旧 VNode 类型一致

    • 更新 props:更新 dom 属性 & 更新 dom 事件

      • 新旧属性或事件存在且不一致,直接更新
      • 新属性存在 & 旧属性不存在,直接添加
      • 新属性都存在 & 新旧值不一致,直接删除
    • 更新 children

      • 新 children 是字符串,只要和旧的 children 不相等,直接使用 innerHTML 替换旧的内容
      • 新 children 是数组 & 旧 children 是字符串,先清空旧节点的内容,循环调用 mount 新增元素
      • 新旧 children 都是数组,取新旧 children 中最小长度,用于减少循环 patch 次数,若 oldLength < newLength 需要通过 mount 新增元素, 若 oldLength > newLength 需要通过 el.removeChild 删除多余旧元素
/**
* 
* @param {oldVnode} n1 
* @param {newVnode} n2 
*/
function patch(n1, n2) {// 1. 类型不一致if (n1.type !== n2.type) {const parent = n1.el.parentElement// 删除 oldVnode.elparent.removeChild(n1.el)// 渲染 newVnode.elmount(n2, parent)} else {// 2. 类型一致// 2.1 统一 el 对象,因为最终修改的是 oldVnode.el,因此,使用 n1.el 作为最终值const el = n2.el = n1.el// 2.2 处理 propsconst oldProps = n1.propsconst newProps = n2.props// 处理 props 不一致for (const key in newProps) {const newValue = newProps[key]const oldValue = oldProps[key]// 旧的有值,新的没值,移除该属性if (newValue !== oldValue) {// 事件不一致if (key.startsWith('on')) {el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])el.addEventListener(key.slice(2).toLowerCase(), newProps[key])} else {// props 值不一致el.setAttribute(key, newValue)}}}// 删除旧的 propsfor (const key in oldProps) {if (!(key in newProps)) {// 旧事件不存在 newProps 中if (key.startsWith('on')) {el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key])} else {// oldProps 中的属性不存在 newProps 中el.removeAttribute(key)}}}// 2.3 处理 childrenconst oldChildren = n1.childrenconst newChildren = n2.children// 新的子节点是字符串if (typeof newChildren === 'string') {// 新旧子节点不一致,直接使用新节点进行替换旧节点if (newChildren !== oldChildren) el.innerHTML = newChildren} else {// 新的子节点为数组// 旧的子节点为字符串if (typeof oldChildren === 'string') {el.innerHTML = ''newChildren.forEach(v => {mount(v, el)})} else {// 旧的子节点也为数组// 取最小的长度进行最少的循环let commonLength = Math.min(newChildren.length, oldChildren.length)for (let i = 0; i < commonLength; i++) {// 递归调用 patch 新老节点patch(oldChildren[i], newChildren[i])}// 循环结束:oldLength < newLength || oldLength > newLength// oldLength < newLength,需要添加新节点if(oldChildren.length < newChildren.length){newChildren.slice(oldChildren.length).forEach(v => {mount(v, el)})}// oldLength > newLength,需要删除旧节点if(oldChildren.length > newChildren.length){oldChildren.slice(newChildren.length).forEach(v => {el.removeChild(v.el)})}}}}
}

实现 Reactivity 模块

在这里插入图片描述

基于 Object.defineProperty 实现响应式

Object.defineProperty 的优点:

  • 兼容性好,可以兼容到 IE9
    Object.defineProperty 的不足:
  • 不能劫持对象 property添加移除
  • 不能劫持数组变化
    • 通过数组下标修改数组项
    • 修改数组长度
class Dep {constructor() {this.subscribers = new Set()}depend() {if (activeEffect) {this.subscribers.add(activeEffect)}}notify() {this.subscribers.forEach(effect => {effect()})}
}// 当前正在执行 effect 函数
let activeEffect = nullfunction watchEffect(effect) {activeEffect = effecteffect() // 目的:初始化调用 + 依赖收集activeEffect = null
}// 存储依赖副作用
const targetMap = new WeakMap()// 获取当前的对象的依赖 dep 对象
function getDep(target, key) {// 1. 根据传入的 target 获取对应的 Map 对象let depsMap = targetMap.get(target)// 2. 若 depsMap 不存在,则初始化一个 Map 对象if (!depsMap) {depsMap = new Map()targetMap.set(target, depsMap)}// 3. 获取具体的 dep 对象let dep = depsMap.get(key)// 4. 若 dep 不存在,则实例化一个 Dep 对象if (!dep) {dep = new Dep()depsMap.set(key, dep)}// 5. 返回 dep 实例return dep
}// 数据劫持
function reactive(raw) {Object.keys(raw).forEach(key => {const dep = getDep(raw, key)let value = raw[key]Object.defineProperty(raw, key, {enumerable: true,configurable: true,get() {// 依赖收集dep.depend()return value},set(newValue) {if (value !== newValue) {value = newValue// 通知依赖更新dep.notify()}return true}})})return raw
}

基于 Proxy 实现响应式

Proxy 的优点:

  • Proxy 能监测的类型比 Object.defineProperty 更丰富的类型
    • 能监测对象和数组的变化
    • hasin 操作符的捕获器
    • deletePropertydelete 操作符的捕获器
  • Proxy 作为新标准,将受到浏览器厂商重点持续性的优化

Proxy 的缺点:

  • 不兼容 IE,没有对应的 polyfill
class Dep {constructor() {this.subscribers = new Set()}depend() {if (activeEffect) {this.subscribers.add(activeEffect)}}notify() {this.subscribers.forEach(effect => {effect()})}
}// 当前正在执行 effect 函数
let activeEffect = nullfunction watchEffect(effect) {activeEffect = effecteffect() // 目的:初始化调用 + 依赖收集activeEffect = null
}// 存储依赖副作用
const targetMap = new WeakMap()// 获取当前的对象的依赖 dep 对象
function getDep(target, key) {// 1. 根据传入的 target 获取对应的 Map 对象let depsMap = targetMap.get(target)// 2. 若 depsMap 不存在,则初始化一个 Map 对象if (!depsMap) {depsMap = new Map()targetMap.set(target, depsMap)}// 3. 获取具体的 dep 对象let dep = depsMap.get(key)// 4. 若 dep 不存在,则实例化一个 Dep 对象if (!dep) {dep = new Dep()depsMap.set(key, dep)}// 5. 返回 dep 实例return dep
}// 数据劫持
function reactive(raw) {return new Proxy(raw, {get(target, key) {const dep = getDep(target, key)dep.depend()return Reflect.get(target, key)},set(target, key, newValue) {const dep = getDep(target, key)const result = Reflect.set(target, key, newValue)dep.notify()return result},})
}

createApp() —— Runtime 模块 + Reactivity 模块

如果你对 Vue3 中 createApp 的使用比较熟练或者阅读过相关源码,其实不难发现 createApp 其实会返回一个带有 mount 方法的 JavaScript 对象.

在下面的 mount 方法中针对 VNode 的 mount(挂载) 和 patch(更新) 进行了判断,以便于在响应式数据发生变更时渲染不同的内容.

function createApp(rootComponent) {return {mount(selector) {let isMounted = falselet oldVnode = nulllet newVnode = nullwatchEffect(() => {if (!isMounted) {isMounted = trueoldVnode = rootComponent.render()mount(oldVnode, selector)} else {newVnode = rootComponent.render()patch(oldVnode, newVnode)oldVnode = newVnode}})}}
}

下面是一个简单的计数器案例的实现:

测试代码如下

  <div id="app"></div><script src="./js/renderer.js"></script><script src="./js/reactive.js"></script><script src="./js/createApp.js"></script><script>const App = {data: reactive({count: 0}),render() {return h('div', null, [h('h1', null, `当前计数:${this.data.count}`),h('button', {onClick: () => this.data.count++}, '+1')])}}const app = createApp(App)app.mount('#app')</script>

最后

通过对 VNode 、VDOM 以及 Vue 三大核心系统的认识和实现,最终又通过 createApp 将这些内容串联在一起,可以说是实现了一个小版本的 vue,当然很多场景还是没有进行一一处理,现在只能实现最简单的测试案例.

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

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

相关文章

vue项目实战

1.项目文件夹添加&#xff08;结构如下&#xff09; 2.页面构建 安装路由 npm install react-router-dom 3.页面基本模板 router文件夹下index.js的模板 // 引入组件 import Login from "../views/login"; // 注册路由数组 const routes [{// 首页默认是/path: …

SD-WAN跨境加速专线:打造无缝、高效的全球社交媒体营销网络

在数字化时代&#xff0c;电子商务与社交媒体的融合已成为不可逆转的趋势。亚马逊&#xff0c;作为全球领先的电子商务平台&#xff0c;近期与Facebook、Instagram、Snapchat、Pinterest和TikTok等社交媒体巨头携手&#xff0c;推出了一项革命性的无缝购物体验。这一创新举措不…

yelp商家数据集上使用火算法求解TSP 问题

先简要回顾下什么是TSP问题&#xff0c; 旅行商问题&#xff08;Traveling Salesman Problem&#xff0c;TSP&#xff09;是一个经典的组合优化问题&#xff0c;广泛应用于运筹学、计算机科学和物流等领域。TSP的基本描述如下&#xff1a; 问题描述 定义&#xff1a;假设有一…

【深度学习目标检测|YOLO算法1】YOLO家族进化史:从YOLOv1到YOLOv11的架构创新、性能优化与行业应用全解析...

【深度学习目标检测|YOLO算法1】YOLO家族进化史&#xff1a;从YOLOv1到YOLOv11的架构创新、性能优化与行业应用全解析… 【深度学习目标检测|YOLO算法1】YOLO家族进化史&#xff1a;从YOLOv1到YOLOv11的架构创新、性能优化与行业应用全解析… 文章目录 【深度学习目标检测|YOL…

星期-时间范围选择器 滑动选择时间 最小粒度 vue3

星期-时间范围选择器 功能介绍属性说明事件说明实现代码使用范例 根据业务需要&#xff0c;实现了一个可选择时间范围的周视图。用户可以通过鼠标拖动来选择时间段&#xff0c;并且可以通过快速选择组件来快速选择特定的时间范围。 功能介绍 时间范围选择&#xff1a;用户可以…

Java | Leetcode Java题解之第554题砖墙

题目&#xff1a; 题解&#xff1a; class Solution {public int leastBricks(List<List<Integer>> wall) {Map<Integer, Integer> cnt new HashMap<Integer, Integer>();for (List<Integer> widths : wall) {int n widths.size();int sum 0…

牛客小白月赛104 —— C.小红打怪

C.小红打怪 1.题目&#xff1a; 2.样例 输入 5 1 2 3 4 5 输出 2 说明 第一回合&#xff0c;小红攻击全体怪物&#xff0c;队友1攻击5号怪物&#xff0c;队友2攻击4号和5号怪物&#xff0c;剩余每只怪物血量为[0,1,2,2,2]。 第二回合&#xff0c;小红攻击全体怪物&#…

python画图|text()和dict()初探

【1】引言 在进行hist()函数的学习进程中&#xff0c;了解到了subplot_mosaic()函数&#xff0c;在学习subplot_mosaic()函数的时候&#xff0c;又发现了text()和dict()函数。 经探究&#xff0c;text()和dict()函数有很多一起使用的场景&#xff0c;为此&#xff0c;我们就一…

BUG: scheduling while atomic

▌▌上篇文章的内容还没有结束 中断处理函数中如果执行了调度&#xff0c;会发生什么 ▌这次&#xff0c;我修改了程序&#xff0c;在中断处理函数中调用了msleep 程序执行后&#xff0c;会有这样的日志 ▌关键就是这句 BUG: scheduling while atomic 我们追代码&#xff0c;可…

算法 -选择排序

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【算法】 欢迎点赞&#x1f44d;收藏⭐关注❤️ 文章目录 &#x1f4a1;选择排序1. &#x1f504; 选择排序&#x1f5bc;️示意图&#x1f4d6;简介&#x1f4a1;实现思路1&#x1f4bb;代码实现1&#x1f4a1;实现思路2…

ubuntu 22.04 镜像源更换

双11抢了个云服务器&#xff0c;想要整点东西玩玩&#xff0c;没想到刚上来就不太顺利 使用sudo apt update更新软件&#xff0c;然后发生了如下报错 W: Failed to fetch http://mirrors.jdcloudcs.com/ubuntu/dists/jammy/InRelease 理所当然想到可能是镜像源连接不是很好&…

2016年7月29日至2017年2月21日NASA大气层层析(ATom)任务甲醛(HCHO)、羟基(OH)和OH生产率的剖面积分柱密度

目录 简介 摘要 引用 网址推荐 知识星球 机器学习 ATom: Column-Integrated Densities of Hydroxyl and Formaldehyde in Remote Troposphere ATom&#xff1a; 远对流层中羟基和甲醛的柱积分密度 简介 该数据集提供了甲醛&#xff08;HCHO&#xff09;、羟基&#xff…

一夜吸粉10万!AI妖精变身视频如何做的?5分钟你也能赶上末班车!

本文背景 最近有小伙伴跟我发了一个AI视频&#xff0c;问我是怎么做的&#xff1f; 很多人在各大自媒体平台&#xff0c;像某音、蝴蝶号都刷到过下面这种妖精变身的短视频。 我也常刷到&#xff0c;从这类视频能看到点赞、收藏、评论的数据都特别高&#xff0c;动不动就几千、几…

【JAVA项目】基于jspm的【医院病历管理系统】

技术简介&#xff1a;采用jsp技术、MySQL等技术实现。 系统简介&#xff1a;通过标签分类管理等方式&#xff0c;实现管理员&#xff1b;个人中心、医院公告管理、用户管理、科室信息管理、医生管理、出诊信息管理、预约时间段管理、预约挂号管理、门诊病历管理、就诊评价管理、…

Oasis:首个可玩的AI生成互动游戏

游戏玩法介绍 Oasis 是由AI公司Decart开发的一款实时生成、可交互的Minecraft风格游戏。这款游戏利用生成式AI技术,创造出独特的“开放世界”体验。Oasis基于大量Minecraft游戏视频进行训练,通过键盘和鼠标输入实时生成游戏画面,模拟物理效果、规则及视觉效果。用户在游戏中…

Python网络爬虫入门篇!

预备知识 学习者需要预先掌握Python的数字类型、字符串类型、分支、循环、函数、列表类型、字典类型、文件和第三方库使用等概念和编程方法。 2. Python爬虫基本流程 a. 发送请求 使用http库向目标站点发起请求&#xff0c;即发送一个Request&#xff0c;Request包含&#xf…

【C++】踏上C++学习之旅(五):auto、范围for以及nullptr的精彩时刻(C++11)

文章目录 前言1. auto关键字&#xff08;C11&#xff09;1.1 为什么要有auto关键字1.2 auto关键字的使用方式1.3 auto的使用细则1.4 auto不能推导的场景 2. 基于范围的for循环&#xff08;C11&#xff09;2.1 范围for的语法2.2 范围for的使用条件 3. 指针空值nullptr&#xff0…

科研绘图系列:R语言组合多个不同图形(violin density barplot heatmap)

文章目录 介绍加载R包数据下载函数图1: Boxplots导入数据数据预处理画图图2: Violin导入数据数据预处理画图图3: Density plots per habitat数据预处理画图图4: Density plots per depth数据预处理画图图5: bar plot准备颜色导入数据数据预处理数据预处理画图图6: Mantel Heat…

系统聚类的分类数确定——聚合系数法

breast_cancer数据集分析——乳腺癌诊断 #读取乳腺癌数据 import pandas as pd import numpy as np from sklearn.datasets import load_breast_cancer data load_breast_cancer() X data.data y data.target.. _breast_cancer_dataset:Breast cancer wisconsin (diagnosti…

jsp+sevlet+mysql实现用户登陆和增删改查功能

jspsevletmysql实现用户登陆和增删改查功能 一、系统介绍二、功能展示1.用户登陆2.用户列表3.查询用户信息4.添加用户信息5.修改用户信息6.删除用户信息 四、其它1.其他系统实现 一、系统介绍 系统主要功能&#xff1a; 用户登陆、添加用户、查询用户、修改用户、删除用户 二…