如何搞懂虚拟 DOM?看看这篇文章吧!

reactvue 中都有虚拟 dom,那么我们应该如何理解和掌握虚拟 dom 的精髓呢?下面本篇文章就来带大家深入了解一下虚拟 dom,希望对大家有所帮助!

如何搞懂虚拟 DOM?看看这篇文章吧!

如何理解和掌握虚拟 DOM 的精髓?我推荐大家学习 Snabbdom 这个项目。

Snabbdom 是一个虚拟 DOM 实现库,推荐的原因一是代码比较少,核心代码只有几百行;二是 Vue 就是借鉴此项目的思路来实现虚拟 DOM 的;三是这个项目的设计/实现和扩展思路值得参考。

snabb /snab/,瑞典语,意思是快速的。

调整好舒服的坐姿,打起精神我们要开始啦~ 要学习虚拟 DOM,我们得先知道 DOM 的基础知识和用 JS 直接操作 DOM 的痛点在哪里。

DOM 的作用和类型结构

DOM(Document Object Model)是一种文档对象模型,用一个对象树的结构来表示一个 HTML/XML 文档,树的每个分支的终点都是一个节点(node),每个节点都包含着对象。DOM API 的方法让你可以用特定方式操作这个树,用这些方法你可以改变文档的结构、样式或者内容。

DOM 树中的所有节点首先都是一个 NodeNode 是一个基类。ElementTextComment 都继承于它。
换句话说,ElementTextComment 是三种特殊的 Node,它们分别叫做 ELEMENT_NODE,
TEXT_NODECOMMENT_NODE,代表的是元素节点(HTML 标签)、文本节点和注释节点。其中 Element 还有一个子类是 HTMLElement,那 HTMLElementElement 有什么区别呢?HTMLElement 代表 HTML 中的元素,如: 等,而有些元素并不是 HTML 标准的,比如 。可以用下面的方法来判断这个元素是不是 HTMLElement

document.getElementById('myIMG') instanceof HTMLElement;

为什么需要虚拟 DOM?

浏览器创建 DOM 是很“昂贵”的。来一个经典示例,我们可以通过 document.createElement('p') 创建一个简单的 p 元素,将属性都打印出来康康:

如何搞懂虚拟 DOM?看看这篇文章吧!

可以看到打印出来的属性非常多,当频繁地去更新复杂的 DOM 树时,会产生性能问题。虚拟 DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以创建一个 JS 对象比创建一个 DOM 对象的代价要小很多。

VNode

VNode 就是 Snabbdom 中描述虚拟 DOM 的一个对象结构,内容如下:

type Key = string | number | symbol;interface VNode {  // CSS 选择器,比如:'p#container'。  sel: string | undefined;    // 通过 modules 操作 CSS classes、attributes 等。  data: VNodeData | undefined;      // 虚拟子节点数组,数组元素也可以是 string。  children: Array | undefined;    // 指向创建的真实 DOM 对象。  elm: Node | undefined;    /**   * text 属性有两种情况:   * 1. 没有设置 sel 选择器,说明这个节点本身是一个文本节点。   * 2. 设置了 sel,说明这个节点的内容是一个文本节点。   */  text: string | undefined;    // 用于给已存在的 DOM 提供标识,在同级元素之间必须唯一,有效避免不必要地重建操作。  key: Key | undefined;}// vnode.data 上的一些设置,class 或者生命周期函数钩子等等。interface VNodeData {  props?: Props;  attrs?: Attrs;  class?: Classes;  style?: VNodeStyle;  dataset?: Dataset;  on?: On;  attachData?: AttachData;  hook?: Hooks;  key?: Key;  ns?: string; // for SVGs  fn?: () => VNode; // for thunks  args?: any[]; // for thunks  is?: string; // for custom elements v1  [key: string]: any; // for any other 3rd party module}

例如这样定义一个 vnode 的对象:

const vnode = h(  'p#container',  { class: { active: true } },  [    h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),    ' and this is just normal text']);

我们通过 h(sel, b, c) 函数来创建 vnode 对象。h() 代码实现中主要是判断了 b 和 c 参数是否存在,并处理成 data 和 children,children 最终会是数组的形式。最后通过 vnode() 函数返回上面定义的 VNode 类型格式。

Snabbdom 的运行流程

先来一张运行流程的简单示例图,先有个大概的流程概念:

如何搞懂虚拟 DOM?看看这篇文章吧!

diff 处理是用来计算新老节点之间差异的处理过程。

再来看一段  Snabbdom 运行的示例代码:

import {  init,  classModule,  propsModule,  styleModule,  eventListenersModule,  h,} from 'snabbdom';const patch = init([  // 通过传入模块初始化 patch 函数  classModule, // 开启 classes 功能  propsModule, // 支持传入 props  styleModule, // 支持内联样式同时支持动画  eventListenersModule, // 添加事件监听]);// 

const container = document.getElementById('container');const vnode = h( 'p#container.two.classes', { on: { click: someFn } }, [ h('span', { style: { fontWeight: 'bold' } }, 'This is bold'), ' and this is just normal text', h('a', { props: { href: '/foo' } }, "I'll take you places!"), ]);// 传入一个空的元素节点。patch(container, vnode);const newVnode = h( 'p#container.two.classes', { on: { click: anotherEventHandler } }, [ h( 'span', { style: { fontWeight: 'normal', fontStyle: 'italic' } }, 'This is now italic type' ), ' and this is still just normal text', h('a', { props: { href: ''/bar' } }, "I'll take you places!"), ]);// 再次调用 patch(),将旧节点更新为新节点。patch(vnode, newVnode);

从流程示意图和示例代码可以看出,Snabbdom 的运行流程描述如下:

首先调用 init() 进行初始化,初始化时需要配置需要使用的模块。比如 classModule 模块用来使用对象的形式来配置元素的 class 属性;eventListenersModule 模块用来配置事件监听器等等。init() 调用后会返回 patch() 函数。

通过 h() 函数创建初始化 vnode 对象,调用 patch() 函数去更新,最后通过 createElm() 创建真正的 DOM 对象。

当需要更新时,创建一个新的 vnode 对象,调用 patch() 函数去更新,经过 patchVnode()updateChildren() 完成本节点和子节点的差异更新。

Snabbdom 是通过模块这种设计来扩展相关属性的更新而不是全部写到核心代码中。那这是如何设计与实现的?接下来就先来康康这个设计的核心内容,Hooks——生命周期函数。

Hooks

Snabbdom 提供了一系列丰富的生命周期函数也就是钩子函数,这些生命周期函数适用在模块中或者可以直接定义在 vnode 上。比如我们可以在 vnode 上这样定义钩子的执行:

h('p.row', {  key: 'myRow',  hook: {    insert: (vnode) => {      console.log(vnode.elm.offsetHeight);    },  },});

全部的生命周期函数声明如下:

名称 触发节点 回调参数

prepatch 开始执行noneinitvnode 被添加vnodecreate一个基于 vnode 的 DOM 元素被创建emptyVnode, vnodeinsert元素被插入到 DOMvnodeprepatch元素即将 patcholdVnode, vnodeupdate元素已更新oldVnode, vnodepostpatch元素已被 patcholdVnode, vnodedestroy元素被直接或间接得移除vnoderemove元素已从 DOM 中移除vnode, removeCallbackpost已完成 patch 过程none

其中适用于模块的是:pre, create,update, destroy, remove, post。适用于 vnode 声明的是:init, create, insert, prepatch, update,postpatch, destroy, remove

我们来康康是如何实现的,比如我们以 classModule 模块为例,康康它的声明:

import { VNode, VNodeData } from "../vnode";import { Module } from "./module";export type Classes = Record;function updateClass(oldVnode: VNode, vnode: VNode): void {  // 这里是更新 class 属性的细节,先不管。  // ...}export const classModule: Module = { create: updateClass, update: updateClass };

可以看到最后导出的模块定义是一个对象,对象的 key 就是钩子函数的名称,模块对象 Module 的定义如下:

import {  PreHook,  CreateHook,  UpdateHook,  DestroyHook,  RemoveHook,  PostHook,} from "../hooks";export type Module = Partial;

TS 中 Partial 表示对象中每个 key 的属性都是可以为空的,也就是说模块定义中你关心哪个钩子,就定义哪个钩子就好了。钩子的定义有了,在流程中是怎么执行的呢?接着我们来看 init() 函数:

// 模块中可能定义的钩子有哪些。const hooks: Array = [  "create",  "update",  "remove",  "destroy",  "pre",  "post",];export function init(  modules: Array<Partial>,  domApi?: DOMAPI,  options?: Options) {  // 模块中定义的钩子函数最后会存在这里。  const cbs: ModuleHooks = {    create: [],    update: [],    remove: [],    destroy: [],    pre: [],    post: [],  };  // ...  // 遍历模块中定义的钩子,并存起来。  for (const hook of hooks) {    for (const module of modules) {      const currentHook = module[hook];      if (currentHook !== undefined) {        (cbs[hook] as any[]).push(currentHook);      }    }  }    // ...}

可以看到 init() 在执行时先遍历各个模块,然后把钩子函数存到了 cbs 这个对象中。执行的时候可以康康 patch() 函数里面:

export function init(  modules: Array<Partial>,  domApi?: DOMAPI,  options?: Options) {  // ...    return function patch(  oldVnode: VNode | Element | DocumentFragment,   vnode: VNode  ): VNode {    // ...        // patch 开始了,执行 pre 钩子。    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();        // ...  }}

这里以 pre 这个钩子举例,pre 钩子的执行时机是在 patch 开始执行时。可以看到 patch() 函数在执行的开始处去循环调用了 cbs 中存储的 pre 相关钩子。其他生命周期函数的调用也跟这个类似,大家可以在源码中其他地方看到对应生命周期函数调用的地方。

这里的设计思路是观察者模式。Snabbdom 把非核心功能分布在模块中来实现,结合生命周期的定义,模块可以定义它自己感兴趣的钩子,然后 init() 执行时处理成 cbs 对象就是注册这些钩子;当执行时间到来时,调用这些钩子来通知模块处理。这样就把核心代码和模块代码分离了出来,从这里我们可以看出观察者模式是一种代码解耦的常用模式。

patch()

接下来我们来康康核心函数 patch(),这个函数是在 init() 调用后返回的,作用是执行 VNode 的挂载和更新,签名如下:

function patch(oldVnode: VNode | Element | DocumentFragment, vnode: VNode): VNode {    // 为简单起见先不关注 DocumentFragment。    // ...}

oldVnode 参数是旧的 VNode 或 DOM 元素或文档片段,vnode 参数是更新后的对象。这里我直接贴出整理的流程描述:

调用模块上注册的 pre 钩子。

如果 oldVnodeElement,则将其转换为空的 vnode 对象,属性里面记录了 elm

这里判断是不是 Element 是判断 (oldVnode as any).nodeType === 1 是完成的,nodeType === 1 表明是一个 ELEMENT_NODE,定义在 这里。

然后判断 oldVnodevnode 是不是相同的,这里会调用 sameVnode() 来判断:

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {  // 同样的 key。  const isSameKey = vnode1.key === vnode2.key;    // Web component,自定义元素标签名,看这里:  // https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createElement  const isSameIs = vnode1.data?.is === vnode2.data?.is;    // 同样的选择器。  const isSameSel = vnode1.sel === vnode2.sel;  // 三者都相同即是相同的。  return isSameSel && isSameKey && isSameIs;}

如果相同,则调用 patchVnode() 做 diff 更新。如果不同,则调用 createElm() 创建新的 DOM 节点;创建完毕后插入 DOM 节点并删除旧的 DOM 节点。

调用上述操作中涉及的 vnode 对象中注册的 insert 钩子队列, patchVnode() createElm() 都可能会有新节点插入 。至于为什么这样做,在 createElm() 中会说到。

最后调用模块上注册的 post 钩子。

流程基本就是相同的 vnode 就做 diff,不同的就创建新的删除旧的。接下来先看下 createElm() 是如何创建 DOM 节点的。

createElm()

createElm() 是根据 vnode 的配置来创建 DOM 节点。流程如下:

调用 vnode 对象上可能存在的 init 钩子。

然后分一下几种情况来处理:

如果 vnode.sel === '!',这是 Snabbdom 用来删除原节点的方法,这样会新插入一个注释节点。因为在 createElm() 后会删除老节点,所以这样设置就可以达到卸载的目的。

如果 vnode.sel 选择器定义是存在的:

解析选择器,得到 idtagclass

调用 document.createElement()document.createElementNS 创建 DOM 节点,并记录到 vnode.elm 中,并根据上一步的结果来设置 idtagclass

调用模块上的 create 钩子。

处理 children 子节点数组:

如果 children 是数组,则递归调用 createElm() 创建子节点后,调用 appendChild 挂载到 vnode.elm 下。

如果 children 不是数组但 vnode.text 存在,说明这个元素的内容是个文本,这个时候调用 createTextNode 创建文本节点并挂载到 vnode.elm 下。

阿里云-虚拟数字人 阿里云-虚拟数字人

阿里云-虚拟数字人是什么? …

阿里云-虚拟数字人 2 查看详情 阿里云-虚拟数字人

调用 vnode 上的 create 钩子。并将 vnode 上的 insert 钩子加入到  insert 钩子队列。

剩下的情况就是 vnode.sel 不存在,说明节点本身是文本,那就调用 createTextNode 创建文本节点并记录到 vnode.elm

最后返回 vnode.elm

整个过程可以看出 createElm() 是根据 sel 选择器的不同设置来选择如何创建 DOM 节点。这里有个细节是补一下: patch() 中提到的 insert 钩子队列。需要这个 insert 钩子队列的原因是需要等到 DOM 真正被插入后才执行,而且也要等到所有子孙节点都插入完成,这样我们可以在 insert 中去计算元素的大小位置信息才是准确的。结合上面创建子节点的过程,createElm() 创建子节点是递归调用,所以队列会先记录子节点,再记录自身。这样在 patch() 的结尾执行这个队列时就可以保证这个顺序。

patchVnode()

接下来我们来看 Snabbdom 如何用 patchVnode() 来做 diff 的,这是虚拟 DOM 的核心。patchVnode() 的处理流程如下:

首先执行 vnode 上 prepatch 钩子。

如果 oldVnode 和 vnode 是同一个对象引用,则不处理直接返回。

调用模块和 vnode 上的 update 钩子。

如果没有定义 vnode.text,则处理 children 的几种情况:

如果 oldVnode.childrenvnode.children 均存在并且不相同。则调用 updateChildren 去更新。

vnode.children 存在而 oldVnode.children 不存在。如果 oldVnode.text 存在则先清空,然后调用 addVnodes 去添加新的 vnode.children

vnode.children 不存在而 oldVnode.children 存在。调用 removeVnodes 移除 oldVnode.children

如果 oldVnode.childrenvnode.children 均不存在。如果 oldVnode.text 存在则清空。

如果有定义 vnode.text并且与 oldVnode.text 不同。如果 oldVnode.children 存在则调用 removeVnodes 清除。然后通过 textContent 来设置文本内容。

最后执行 vnode 上的 postpatch 钩子。

从过程可以看出,diff 中对于自身节点的相关属性的改变比如 classstyle 之类的是依靠模块去更新的,这里不过多展开了大家有需要可以去看下模块相关的代码。diff 的主要核心处理是集中在 children 上,接下来康康 diff 处理 children 的几个相关函数。

addVnodes()

这个很简单,先调用 createElm() 创建,然后插入到对应的 parent 中。

removeVnodes()

移除的时候会先调用 destoryremove 钩子,这里重点讲讲这两个钩子的调用逻辑和区别。

destory,首先调用这个钩子。逻辑是先调用 vnode 对象上的这个钩子,再调用模块上的。然后对 vnode.children 也按照这个顺序递归调用这个钩子。remove,这个 hook 只有在当前元素从它的父级中删除才会触发,被移除的元素中的子元素则不会触发,并且模块和 vnode 对象上的这个钩子都会调用,顺序是先调用模块上的再调用 vnode 上的。而且比较特殊的是等待所有的 remove 都会调用后,元素才会真正被移除,这样做可以实现一些延迟删除的需求。

以上可以看出这两个钩子调用逻辑不同的地方,特别是 remove 只在直接脱离父级的元素上才会被调用。

updateChildren()

updateChildren() 是用来处理子节点 diff 的,也是  Snabbdom  中比较复杂的一个函数。总的思想是对  oldChnewCh 各设置头、尾一共四个指针,这四个指针分别是 oldStartIdxoldEndIdxnewStartIdxnewEndIdx。然后在 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) 循环中对两个数组进行对比,找到相同的部分进行复用更新,并且每次比较处理最多移动一对指针。详细的遍历过程按以下顺序处理:

如果这四个指针有任何一个指向的 vnode == null,则这个指针往中间移动,比如:start++ 或 end–,null 的产生在后面情况有说明。

如果新旧开始节点相同,也就是 sameVnode(oldStartVnode, newStartVnode) 返回 true,则用 patchVnode() 执行 diff,并且两个开始节点都向中间前进一步。

如果新旧结束节点相同,也采用 patchVnode() 处理,两个结束节点向中间后退一步。

如果旧开始节点与新结束节点相同,先用 patchVnode() 处理更新。然后需要移动 oldStart 对应的 DOM 节点,移动的策略是移动到 oldEndVnode 对应 DOM 节点的下一个兄弟节点之前。为什么是这样移动呢?首先,oldStart 与 newEnd 相同,说明在当前循环处理中,老数组的开始节点是往右移动了;因为每次的处理都是首尾指针往中间移动,我们是把老数组更新成新的,这个时候 oldEnd 可能还没处理,但这个时候 oldStart 已确定在新数组的当前处理中是最后一个了,所以移动到 oldEnd 的下一个兄弟节点之前是合理的。移动完毕后,oldStart++,newEnd–,分别向各自的数组中间移动一步。

如果旧结束节点与新开始节点相同,也是先用 patchVnode() 处理更新,然后把 oldEnd 对应的 DOM 节点移动 oldStartVnode 对应的  DOM 节点之前,移动理由同上一步一样。移动完毕后,oldEnd–,newStart++。

如果以上情况都不是,则通过 newStartVnode 的 key 去找在 oldChildren 的下标 idx,根据下标是否存在有两种不同的处理逻辑:

如果下标不存在,说明 newStartVnode 是新创建的。通过 createElm() 创建新的 DOM,并插入到 oldStartVnode 对应的 DOM 之前。

如果下标存在,也要分两种情况处理:

如果两个 vnode 的 sel 不同,也还是当做新创建的,通过 createElm() 创建新的 DOM,并插入到 oldStartVnode 对应的 DOM 之前。

如果 sel 是相同的,则通过 patchVnode() 处理更新,并把 oldChildren 对应下标的 vnode 设置为 undefined,这也是前面双指针遍历中为什么会出现 == null 的原因。然后把更新完毕后的节点插入到 oldStartVnode 对应的 DOM 之前。

以上操作完后,newStart++。

遍历结束后,还有两种情况要处理。一种是 oldCh 已经全部处理完成,而 newCh 中还有新的节点,需要对 newCh 剩下的每个都创建新的 DOM;另一种是 newCh 全部处理完成,而 oldCh 中还有旧的节点,需要将多余的节点移除。这两种情况的处理在 如下:

  function updateChildren(    parentElm: Node,    oldCh: VNode[],    newCh: VNode[],    insertedVnodeQueue: VNodeQueue  ) {     // 双指针遍历过程。    // ...          // newCh 中还有新的节点需要创建。    if (newStartIdx <= newEndIdx) {      // 需要插入到最后一个处理好的 newEndIdx 之前。      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;      addVnodes(        parentElm,        before,        newCh,        newStartIdx,        newEndIdx,        insertedVnodeQueue      );    }          // oldCh 中还有旧的节点要移除。    if (oldStartIdx <= oldEndIdx) {      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);    }  }

我们用一个实际例子来看一下 updateChildren() 的处理过程:

初始状态如下,旧子节点数组为 [A, B, C],新节点数组为 [B, A, C, D]:

如何搞懂虚拟 DOM?看看这篇文章吧!

第一轮比较,开始和结束节点都不一样,于是看 newStartVnode 在旧节点中是否存在,找到了在 oldCh[1] 这个位置,那么先执行 patchVnode() 进行更新,然后把 oldCh[1] = undefined,并把 DOM 插入到 oldStartVnode 之前,newStartIdx 向后移动一步,处理完后状态如下:

如何搞懂虚拟 DOM?看看这篇文章吧!

第二轮比较,oldStartVnodenewStartVnode 相同,执行 patchVnode() 更新,oldStartIdxnewStartIdx 向中间移动,处理完后状态如下:

如何搞懂虚拟 DOM?看看这篇文章吧!

第三轮比较,oldStartVnode == nulloldStartIdx 向中间移动,状态更新如下:

如何搞懂虚拟 DOM?看看这篇文章吧!

第四轮比较,oldStartVnodenewStartVnode 相同,执行 patchVnode() 更新,oldStartIdxnewStartIdx 向中间移动,处理完后状态如下:

如何搞懂虚拟 DOM?看看这篇文章吧!

此时 oldStartIdx 大于 oldEndIdx,循环结束。此时 newCh 中还有没处理完的新节点,需要调用 addVnodes() 插入,最终状态如下:

如何搞懂虚拟 DOM?看看这篇文章吧!

总结

到这里虚拟 DOM 的核心内容已经梳理完毕,Snabbdom 的设计和实现原理我觉得挺好的,大家有空可以去康康源码的细节再细品下,其中的思想很值得学习。

更多编程相关知识,请访问:编程视频!!

以上就是如何搞懂虚拟 DOM?看看这篇文章吧!的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/548879.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年11月9日 19:05:02
下一篇 2025年11月9日 19:09:39

相关推荐

  • 如何使用 vue-color 创建交互式颜色渐变页面?

    如何创建交互式颜色渐变页面? 实现交互式颜色渐变页面可以通过利用第三方库来简化开发流程。 推荐解决方案: vue-color 立即学习“前端免费学习笔记(深入)”; vue-color是一个vue.js库,提供了一个功能强大的调色板组件。它允许你轻松创建和管理颜色渐变。 特性: 颜色选择器:选择单一…

    2025年12月24日
    200
  • 如何利用 vue-color 库打造交互式色彩渐变页面?

    打造交互性前端:色彩渐变页面的制作方法 在前端开发中,色彩渐变页面和交互式元素深受设计师和开发人员的欢迎。本文将探讨如何利用 vue-color 库轻松实现这样的页面。 使用 vue-color 库构建调色板 vue-color 是一个 vue.js 库,可用于创建可定制的调色板。其基本功能包括: …

    2025年12月24日
    300
  • 如何使用前端技术创建交互式颜色渐变页面?

    如何创建交互式颜色渐变页面? 当您希望在前端界面实现颜色渐变效果并实现交互功能时,可以使用以下方法: 解决方案: 1. 使用 vue-color 库 vue-color 库是一个功能强大的 vue.js 库,可用于创建色板和处理颜色操作。它可以帮助您轻松实现颜色渐变效果,如下所示: 立即学习“前端免…

    好文分享 2025年12月24日
    000
  • Vue 中如何动态添加带有动态样式的伪元素?

    vue 动态添加具有动态样式的伪元素 在某些情况下,需要根据动态条件向 dom 元素添加带有动态样式的伪元素。例如,元素的伪元素“before”可能只有在满足特定条件时才会出现,并且其样式(如长度、高度和其他属性)也是不确定的。 解决方案:css 变量 由于伪元素的样式不能直接在 css 中定义,可…

    2025年12月24日
    000
  • Vue 中如何动态添加伪元素?

    vue中如何动态添加伪元素 在某些情况下,需要动态地为元素添加伪元素,但传统方法受限于伪元素不能写死在 css 中。本文将介绍一种使用 css 变量解决此问题的方法。 使用 css 变量 css 变量允许在样式表中定义可重复使用的变量,然后可以在其他样式中使用这些变量。利用这个特性,我们可以动态地控…

    2025年12月24日
    100
  • 如何使用 CSS 变量动态控制 Vue 应用中 DOM 伪元素的样式?

    灵活操纵 vue 中 dom 伪元素 在 vue 应用中,有时需要在特定条件下动态添加和修改伪元素样式。虽然 css 中的伪元素通常是静态定义的,但有些情况下,需要根据用户的行为或数据动态调整其样式。 动态控制伪元素样式 可以使用 css 变量来解决此问题。css 变量允许您在样式表中存储可变值,然…

    2025年12月24日
    100
  • Vue中如何利用CSS变量动态操纵伪元素样式?

    利用css变量动态操纵伪元素 在vue中,有时需要动态地给dom元素添加伪元素,并且伪元素的样式也是动态变化的。不能在css文件中直接定义伪元素样式,因为伪元素包含动态参数。 这个问题的解决方法之一是使用css变量。css变量允许我们在css中定义变量并动态地将其分配给元素的样式。 代码示例: 立即…

    2025年12月24日
    300
  • HTMLrev 上的免费 HTML 网站模板

    HTMLrev 是唯一的人工策划的库专门专注于免费 HTML 模板,适用于由来自世界各地慷慨的模板创建者制作的网站、登陆页面、投资组合、博客、电子商务和管理仪表板世界。 这个人就是我自己 Devluc,我已经工作了 1 年多来构建、改进和更新这个很棒的免费资源。我自己就是一名模板制作者,所以我知道如…

    2025年12月24日
    300
  • Vue/UniApp 中如何实现选中效果的切换?

    vue/uniapp中复现选中的效果 在vue/uniapp中实现此效果,可以使用view元素和样式类来控制外观。让我们来看看这个问题的示例代码。 日 周 月 年 .tabs { display: flex; justify-content: space-between; flex-directio…

    2025年12月24日
    000
  • 如何简化五子棋代码中的重复部分?

    五子棋代码简化 问题: 如何简化五子棋代码中重复的部分? 问题内容: 提供了vue编写的五子棋代码,但其中有多个重复的部分。希望得到一个更简化的代码版本。 问题答案: 拆分重复方法 将大方法中的重复部分拆分成更小的函数,例如: placepiece():放置棋子checkandplace():检查某…

    2025年12月24日
    000
  • Vue/Uniapp 中如何实现类似图片所示的日周月年切换标签效果?

    vue/uniapp中,如何实现类似图片中效果的日周月年切换标签? 图片中呈现了四个标签,选中”日”后,背景变成蓝色,字体变成白色。而其他未选中的标签,背景为灰色,字体也呈灰色。 一位网友通过纯html实现了一个简易的版本,代码如下: 日 周 月 年 具体效果,可以点开上面的…

    2025年12月24日
    000
  • Vue/UniApp中如何制作圆角选项卡,且选中状态颜色与未选中状态颜色不同?

    vue/uniapp中,如何制作圆角栏目的选项卡效果? 你想要创建一个圆角栏目的选项卡效果,其中一个选中的选项是用白色文本填充蓝色背景,而其他选项是黑色文本填充灰色背景。 以下是使用html和css实现此效果的方法: 日 周 月 年 .tabs { display: flex; justify-co…

    2025年12月24日
    000
  • Vue2表格隐藏列后,固定列出现空白行怎么办?

    vue2表格隐藏列导致固定列空白行 当使用vue2表格库(例如element-table)时,隐藏其中一列可能会导致固定列(通常包含操作按钮)最上方出现空白行。 解决方案 要解决此问题,需要在切换列显示状态后手动调用dolayout()方法。该方法会重新计算表格的布局,消除空白行。 立即学习“前端免…

    2025年12月24日
    000
  • 如何优化 Vue 五子棋程序中的重复代码?

    简化代码 问题: 一个使用 vue 编写的五子棋程序中存在大量重复代码,需要进行简化。 代码重复: 立即学习“前端免费学习笔记(深入)”; 部分的 clickbox 函数中重复的条件检查和棋子放置逻辑。 部分的 aripoint 函数中重复的四种条件检查和棋子放置逻辑。 部分的 determinee…

    2025年12月24日
    100
  • Vue/UniApp 选项卡选中时如何添加边框和背景色?

    vue/uniapp中选中时有边框和背景色的选项卡如何实现 原帖中提供的代码不能实现选中时有边框和背景色的效果。下面是用 html 实现这种效果的代码: Document 日 周 月 年 .tabs { display: flex; justify-content: space-between; f…

    2025年12月24日
    000
  • 如何使用 Vue/Uniapp 实现美观实用的“选框”样式页面元素?

    vue/uniapp页面设计优化 在vue/uniapp中,为实现类似“选框”样式的页面元素,可采用以下优化方案: 创建层叠布局(flex layout): 设置外层容器的显示方式为“flex”,并启用水平排列。 定义“选框”元素: 立即学习“前端免费学习笔记(深入)”; 为每个“选框”创建一个子元…

    2025年12月24日
    000
  • 让我们只用一根安装线就可以使网络响应起来吗?我正在寻找贡献者!

    最近我发布了一个 npm 包,其使命如标题所示:让项目只需一行代码即可响应! 我与您分享响应式应用程序 [beta] 包 我花了几年时间尝试和开发这项技术,目前包括: 动态设置 html 标签字体大小(通过 js 脚本),考虑:(1) 屏幕分辨率和 (2) 浏览器字体大小(用于网络可访问性)将像素定…

    2025年12月24日
    000
  • uniapp/vue 中父元素 pointer-events: none 如何让子元素点击事件生效?

    在 uniapp/vue 中解决父元素 pointer-events: none 下子元素点击事件无效的问题 在使用 uniapp/vue 时,当父元素设置了 pointer-events: none 属性后,子元素的点击事件可能会失效。 问题分析 当父元素设置为 pointer-events: n…

    2025年12月24日
    200
  • 如何将 Element UI 的 CSS 文件优雅地引入本地项目?

    如何优雅地引入 element ui 的 css 文件? element ui 是一个非常流行的前端 ui 框架,它的样式表通常通过 cdn url 引入,但偶尔 cdn 会出现访问不稳定的情况,导致样式无法正常加载。为了解决这个问题,我们可以将样式文件下载到本地。 引入本地样式文件的步骤如下: 下…

    2025年12月24日
    000
  • UniApp/Vue 中如何让父元素 Pointer-Events: None 下的子元素点击生效?

    在 uniapp/vue 中让父元素 pointer-events: none 下的子元素点击生效 当我们设置父元素的 pointer-events 为 none 时,它将阻止鼠标或触摸事件传递给子元素。在这种情况下,底部的点击事件将无法生效。 要解决此问题,可以给需要点击事件的子元素添加 poin…

    2025年12月24日
    200

发表回复

登录后才能评论
关注微信