JS如何实现自定义渲染器?渲染的抽象

javascript中实现自定义渲染器的核心价值在于将ui描述与渲染逻辑解耦,从而实现跨平台、性能优化、架构清晰和创新扩展;其关键组件包括虚拟节点(vnode)、宿主环境操作接口、协调与打补丁算法、组件抽象、响应式系统和调度器,这些共同构建了一个灵活高效的渲染体系,使同一套ui代码可适配不同目标环境,并通过精细化控制提升性能与可维护性。

JS如何实现自定义渲染器?渲染的抽象

JavaScript 中实现自定义渲染器,核心在于将“渲染什么”与“如何渲染”彻底解耦。它提供了一套抽象机制,允许我们用统一的描述方式(通常是虚拟 DOM)来定义 UI 结构,然后根据不同的目标环境(比如浏览器 DOM、Canvas、WebGL 甚至服务器端字符串)来具体执行渲染操作。这就像你给一个剧本,但可以有不同的导演和舞台班底来呈现它。

解决方案

要构建一个自定义渲染器,我们通常会围绕几个关键概念展开:一个虚拟节点(VNode)的抽象、一套宿主环境操作(Host Operations)的接口,以及一个负责协调(Reconciliation)打补丁(Patching)的算法。

首先,你需要定义你的 VNode 结构,它本质上就是描述 UI 元素的一个纯 JavaScript 对象。比如:

// 一个简单的VNode结构class VNode {  constructor(type, props, children) {    this.type = type; // 元素类型,如 'div', 'p', 或者一个组件    this.props = props || {}; // 元素的属性或组件的props    this.children = children || []; // 子节点    // 实际的渲染器还会包含key、el(对应的真实元素)等  }}

接着,就是最关键的宿主环境操作。这些是一组函数,它们定义了如何与目标渲染环境进行交互。如果你想渲染到浏览器 DOM,这些操作就是

document.createElement

appendChild

setAttribute

等的封装。如果你想渲染到 Canvas,它们可能就是

ctx.fillRect

ctx.fillText

等。

// 宿主环境操作的抽象接口 (以DOM为例)const domHostOperations = {  createElement(type) {    return document.createElement(type);  },  createText(text) {    return document.createTextNode(text);  },  appendChild(parent, child) {    parent.appendChild(child);  },  insertBefore(parent, child, anchor) {    parent.insertBefore(child, anchor);  },  removeChild(parent, child) {    parent.removeChild(child);  },  patchProp(el, key, prevValue, nextValue) {    // 处理属性更新,包括事件、样式等    if (key.startsWith('on')) {      const eventName = key.slice(2).toLowerCase();      if (prevValue) el.removeEventListener(eventName, prevValue);      if (nextValue) el.addEventListener(eventName, nextValue);    } else if (key === 'style') {      for (const styleKey in nextValue) {        el.style[styleKey] = nextValue[styleKey];      }      for (const styleKey in prevValue) {        if (!(styleKey in nextValue)) {          el.style[styleKey] = '';        }      }    } else if (key in el) {      el[key] = nextValue;    } else {      if (nextValue == null || nextValue === false) {        el.removeAttribute(key);      } else {        el.setAttribute(key, nextValue);      }    }  },  setElementText(el, text) {    el.textContent = text;  }  // 还有很多其他操作,比如设置SVG命名空间、处理Fragment等};

最后,你需要一个渲染器工厂函数。这个函数接收宿主环境操作作为参数,然后返回一个

render

函数和

patch

函数。

render

负责首次挂载,

patch

负责后续更新。它们内部会执行 VNode 的遍历、比较(diffing)和实际的宿主操作。

function createRenderer(hostOperations) {  const {    createElement,    createText,    appendChild,    insertBefore,    removeChild,    patchProp,    setElementText  } = hostOperations;  function mountElement(vnode, container, anchor = null) {    const el = vnode.el = createElement(vnode.type); // 关联真实元素    for (const key in vnode.props) {      patchProp(el, key, null, vnode.props[key]);    }    if (Array.isArray(vnode.children)) {      vnode.children.forEach(child => mount(child, el));    } else if (typeof vnode.children === 'string') {      setElementText(el, vnode.children);    }    insertBefore(container, el, anchor);  }  function mountText(vnode, container, anchor = null) {    const el = vnode.el = createText(vnode.children);    insertBefore(container, el, anchor);  }  function patch(oldVnode, newVnode, container, anchor = null) {    if (oldVnode === newVnode) return;    if (oldVnode && !isSameVNodeType(oldVnode, newVnode)) {      // 类型不同,直接替换      unmount(oldVnode);      mount(newVnode, container, anchor);      return;    }    const el = newVnode.el = oldVnode.el; // 复用真实元素    // 更新属性    patchProps(el, newVnode.props, oldVnode.props);    // 更新子节点    patchChildren(oldVnode, newVnode, el);  }  function patchProps(el, newProps, oldProps) {    for (const key in newProps) {      if (newProps[key] !== oldProps[key]) {        patchProp(el, key, oldProps[key], newProps[key]);      }    }    for (const key in oldProps) {      if (!(key in newProps)) {        patchProp(el, key, oldProps[key], null); // 移除旧属性      }    }  }  function patchChildren(oldVnode, newVnode, container) {    const oldChildren = oldVnode.children;    const newChildren = newVnode.children;    if (typeof newVnode.children === 'string') {      if (oldChildren !== newVnode.children) {        setElementText(container, newVnode.children);      }    } else if (Array.isArray(newChildren)) {      if (Array.isArray(oldChildren)) {        // 核心的diff算法,这里简化处理,实际生产级会复杂很多        const commonLength = Math.min(oldChildren.length, newChildren.length);        for (let i = 0; i  oldChildren.length) {          newChildren.slice(oldChildren.length).forEach(child => mount(child, container));        } else if (oldChildren.length > newChildren.length) {          oldChildren.slice(newChildren.length).forEach(child => unmount(child));        }      } else {        setElementText(container, ''); // 清空旧文本子节点        newChildren.forEach(child => mount(child, container));      }    } else { // newChildren 为 null 或 undefined      if (Array.isArray(oldChildren)) {        oldChildren.forEach(child => unmount(child));      } else if (typeof oldChildren === 'string') {        setElementText(container, '');      }    }  }  function unmount(vnode) {    if (vnode.el && vnode.el.parentNode) {      vnode.el.parentNode.removeChild(vnode.el);    }    // 递归卸载子节点等  }  function isSameVNodeType(n1, n2) {    return n1.type === n2.type; // 简化判断,实际会考虑key、组件类型等  }  function mount(vnode, container, anchor = null) {    const { type } = vnode;    if (typeof type === 'string') { // 普通元素      mountElement(vnode, container, anchor);    } else if (type === Text) { // 文本节点      mountText(vnode, container, anchor);    }    // 实际还会处理组件、Fragment、Teleport等  }  return {    render(vnode, container) {      if (vnode) {        // 首次渲染或更新        if (container._vnode) {          patch(container._vnode, vnode, container);        } else {          mount(vnode, container);        }      } else if (container._vnode) {        // 卸载        unmount(container._vnode);      }      container._vnode = vnode; // 存储当前渲染的vnode    }  };}// 使用示例const renderer = createRenderer(domHostOperations);const vnode1 = new VNode('div', { id: 'app' }, [  new VNode('h1', null, 'Hello Custom Renderer!'),  new VNode('p', { style: 'color: blue;' }, 'This is a paragraph.')]);const vnode2 = new VNode('div', { id: 'app' }, [  new VNode('h1', null, 'Hello World!'),  new VNode('span', { style: 'font-weight: bold;' }, 'Updated content.')]);// 首次渲染renderer.render(vnode1, document.getElementById('root'));// 模拟更新setTimeout(() => {  renderer.render(vnode2, document.getElementById('root'));}, 2000);// 卸载setTimeout(() => {  renderer.render(null, document.getElementById('root'));}, 4000);

为什么我们需要自定义渲染器?它的核心价值在哪里?

在我看来,自定义渲染器这事儿,最核心的价值就是解放了前端的想象力。你想啊,我们过去写 JavaScript,基本上就是为了操作浏览器 DOM。但有了自定义渲染器,UI 的描述和它的呈现方式就彻底分开了。

这带来了几个非常实际的好处:

跨平台能力: 这是最显而易见的。React Native、Weex、Uni-app 都是这套思想的产物。你写一套类似 React 的组件代码,通过不同的渲染器,就能跑在 iOS、Android、Web 甚至小程序上。这简直是“一次编写,到处运行”的终极体现,极大地提升了开发效率和代码复用率。性能优化与特定环境适配: 浏览器 DOM 操作其实挺重的,而且有各种性能陷阱。自定义渲染器允许你针对特定环境做极致优化。比如,如果你在 Canvas 上做游戏,你可以直接操作 Canvas API,避免 DOM 的开销。或者,在服务端渲染(SSR)时,渲染器直接把 VNode 转化成 HTML 字符串,完全不涉及 DOM。这种精细的控制,能让你在性能上做到很多意想不到的事情。创新与实验性: 当你把渲染逻辑抽象出来后,就可以尝试各种新奇的 UI 表现形式。比如,渲染到 WebGL 实现 3D 界面,渲染到命令行输出文本界面,甚至渲染到硬件设备上。这给了开发者一个巨大的沙盒,去探索 UI 交互的边界。解耦与架构清晰: 它强制你把 UI 的“是什么”和“怎么显示”分开。这让你的代码结构更清晰,逻辑更纯粹。组件只关心状态和 VNode 的生成,而渲染器只关心如何把 VNode 映射到实际的视图。这种分层架构,对于大型复杂项目来说,简直是福音。

说白了,它把前端从“DOM 奴隶”的角色中解脱出来,让我们能更专注于 UI 自身的逻辑和体验,而不是被特定平台的实现细节所束缚。

构建自定义渲染器的关键抽象和组件有哪些?

要搭起一个自定义渲染器,光有 VNode 和宿主操作还不够,这中间还有一些至关重要的“胶水”和“大脑”:

虚拟节点(VNode)层:

统一的 UI 描述: 这是所有渲染的基础。无论是

div

p

这样的原生元素,还是你写的

MyComponent

组件,甚至是文本节点、注释节点,都得有一个统一的 VNode 结构来表示。它通常包含

type

(类型)、

props

(属性)、

children

(子节点)和

key

(用于优化列表渲染)。类型多样性: 你的 VNode 系统得能区分不同类型的节点,比如元素 VNode、文本 VNode、组件 VNode、函数式组件 VNode、Fragment(片段)VNode、Teleport(传送门)VNode 等等。每种类型在渲染时都有不同的处理逻辑。

宿主环境操作(Host Operations)抽象层:

环境无关性接口: 这就是我们前面提到的

createElement

,

appendChild

,

patchProp

等等。这套接口必须是与具体渲染环境无关的,也就是说,无论是 DOM 还是 Canvas,只要它能提供这些操作的实现,你的渲染器就能工作。这是实现跨平台的基石。细粒度控制: 这些操作越细粒度,你的渲染器就越灵活,能够实现更精确的更新。比如,不仅有

setAttribute

,可能还有专门处理

className

style

、事件监听的

patchProp

协调(Reconciliation)算法:

Diffing: 这是渲染器的“大脑”。当状态更新,生成新的 VNode 树时,它会与旧的 VNode 树进行比较,找出两者之间的最小差异。这个过程就是 Diffing。它通常采用深度优先遍历,并结合

key

属性来优化列表项的移动和复用。Patching: 找到差异后,就需要调用宿主环境操作来“打补丁”,将这些差异应用到实际的 UI 界面上。这包括创建新元素、删除旧元素、更新属性、移动元素、更新文本内容等。Diffing 和 Patching 是一个紧密结合的过程。

组件抽象层(如果支持组件):

组件实例: 当 VNode 的

type

是一个组件时,渲染器需要能够创建组件实例,管理其生命周期(挂载、更新、卸载),并调用其

render

方法来获取子 VNode。生命周期钩子: 你的渲染器需要提供机制,让组件能够在渲染的不同阶段(如挂载前、挂载后、更新前、更新后)执行自定义逻辑。状态管理与响应式: 虽然这不完全是渲染器本身的职责,但一个完整的 UI 框架会集成状态管理和响应式系统,当数据变化时,能自动触发 VNode 的重新生成和渲染器的更新。

调度器(Scheduler)/批处理(Batching):

优化更新频率: 频繁的 UI 更新会导致性能问题。调度器负责将多个小的更新操作合并成一个批次,然后在合适的时机(比如浏览器下一帧

requestAnimationFrame

或微任务队列)统一执行,减少不必要的宿主操作。这能显著提升渲染性能。

这些组件协同工作,构建了一个健壮且可扩展的自定义渲染器。它就像一个精密的工厂,VNode 是蓝图,宿主操作是各种工具,而协调算法则是工厂里的智能机器人,确保生产线高效运转。

在实际开发中,实现一个简易自定义渲染器会遇到哪些挑战和考量?

自己动手写一个简易的自定义渲染器,这事儿挺有意思的,但也会碰到一些不小的挑战,这可不是搭个积木那么简单:

Diffing 算法的复杂性:

列表更新的效率: 这是最头疼的。当子节点是列表时,如何高效地比较新旧列表,找出最小的插入、删除、移动、更新操作,同时还要考虑

key

的作用,这需要一个精巧的算法。比如 Vue 和 React 的 Diff 算法,都经过了大量的优化和迭代,涵盖了各种边界情况。自己写一个既正确又高效的,是很大的挑战。不同类型节点的处理: 如果新旧 VNode 类型不同,是直接替换还是尝试复用?这需要清晰的策略。

属性和事件的精细化处理:

属性类型: 普通 HTML 属性、DOM 属性、布尔属性、SVG 属性、样式(

style

)、类名(

class

)等等,每种属性的更新方式都可能不同。你得考虑周全。事件委托与合成事件: 直接在每个元素上绑定事件效率不高。像 React 那样实现事件委托(把事件监听器挂载到根元素上,然后通过事件冒泡来处理)和合成事件系统,能提供更好的性能和跨浏览器一致性,但这实现起来可不简单。

生命周期和副作用管理:

组件生命周期: 如果你的渲染器支持组件,那么组件的挂载、更新、卸载等生命周期钩子如何与渲染流程结合?什么时候触发

mounted

?什么时候触发

updated

副作用清理: 在组件卸载时,如何清理掉它创建的 DOM 元素、事件监听器、定时器、网络请求等副作用,避免内存泄漏?这需要一套可靠的机制。

文本节点和注释节点的处理:

它们虽然看起来简单,但文本节点的变化直接影响

textContent

,而注释节点可能用于调试或占位,它们在 Diff 过程中也需要被正确处理。

特殊 VNode 类型的支持:

Fragment: 如何处理没有根元素的 VNode 列表(比如

...
)?你需要一个特殊的 VNode 类型来表示它,并且在渲染时只渲染其子节点。Teleport: 如何将一个 VNode 渲染到 DOM 树的另一个位置(比如弹窗、Modal)?这需要渲染器提供特定的机制来“传送”节点。Suspense/Error Boundary: 现代框架还支持异步组件加载和错误边界,这些也需要渲染器层面的支持。

性能考量和调度:

批量更新: 如何避免频繁的 DOM 操作?将多次 VNode 更新合并成一次实际的渲染,通常会用到
requestAnimationFrame

或微任务队列来调度。测量与调试: 如何知道你的渲染器哪里慢了?你需要工具和方法来测量渲染性能,找出瓶颈。

内存管理:

VNode 树和真实 DOM 元素之间的引用关系需要仔细维护,避免循环引用导致内存泄漏。尤其是在频繁创建和销毁节点时。

这些挑战使得一个“简易”的自定义渲染器,在真正走向实用时,会迅速变得非常复杂。这也就是为什么 Vue 和 React 这样的框架,其内部的渲染器代码量巨大,且经过了无数次的优化和重构。但即便如此,亲手尝试去实现一部分,对于理解前端框架的运作机制,绝对是一次宝贵的经历。

以上就是JS如何实现自定义渲染器?渲染的抽象的详细内容,更多请关注创想鸟其它相关文章!

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

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月20日 10:24:55
下一篇 2025年12月20日 10:25:03

相关推荐

  • CSS mask属性无法获取图片:为什么我的图片不见了?

    CSS mask属性无法获取图片 在使用CSS mask属性时,可能会遇到无法获取指定照片的情况。这个问题通常表现为: 网络面板中没有请求图片:尽管CSS代码中指定了图片地址,但网络面板中却找不到图片的请求记录。 问题原因: 此问题的可能原因是浏览器的兼容性问题。某些较旧版本的浏览器可能不支持CSS…

    2025年12月24日
    900
  • Uniapp 中如何不拉伸不裁剪地展示图片?

    灵活展示图片:如何不拉伸不裁剪 在界面设计中,常常需要以原尺寸展示用户上传的图片。本文将介绍一种在 uniapp 框架中实现该功能的简单方法。 对于不同尺寸的图片,可以采用以下处理方式: 极端宽高比:撑满屏幕宽度或高度,再等比缩放居中。非极端宽高比:居中显示,若能撑满则撑满。 然而,如果需要不拉伸不…

    2025年12月24日
    400
  • 如何让小说网站控制台显示乱码,同时网页内容正常显示?

    如何在不影响用户界面的情况下实现控制台乱码? 当在小说网站上下载小说时,大家可能会遇到一个问题:网站上的文本在网页内正常显示,但是在控制台中却是乱码。如何实现此类操作,从而在不影响用户界面(UI)的情况下保持控制台乱码呢? 答案在于使用自定义字体。网站可以通过在服务器端配置自定义字体,并通过在客户端…

    2025年12月24日
    800
  • 如何在地图上轻松创建气泡信息框?

    地图上气泡信息框的巧妙生成 地图上气泡信息框是一种常用的交互功能,它简便易用,能够为用户提供额外信息。本文将探讨如何借助地图库的功能轻松创建这一功能。 利用地图库的原生功能 大多数地图库,如高德地图,都提供了现成的信息窗体和右键菜单功能。这些功能可以通过以下途径实现: 高德地图 JS API 参考文…

    2025年12月24日
    400
  • 如何使用 scroll-behavior 属性实现元素scrollLeft变化时的平滑动画?

    如何实现元素scrollleft变化时的平滑动画效果? 在许多网页应用中,滚动容器的水平滚动条(scrollleft)需要频繁使用。为了让滚动动作更加自然,你希望给scrollleft的变化添加动画效果。 解决方案:scroll-behavior 属性 要实现scrollleft变化时的平滑动画效果…

    2025年12月24日
    000
  • 如何为滚动元素添加平滑过渡,使滚动条滑动时更自然流畅?

    给滚动元素平滑过渡 如何在滚动条属性(scrollleft)发生改变时为元素添加平滑的过渡效果? 解决方案:scroll-behavior 属性 为滚动容器设置 scroll-behavior 属性可以实现平滑滚动。 html 代码: click the button to slide right!…

    2025年12月24日
    500
  • 为什么设置 `overflow: hidden` 会导致 `inline-block` 元素错位?

    overflow 导致 inline-block 元素错位解析 当多个 inline-block 元素并列排列时,可能会出现错位显示的问题。这通常是由于其中一个元素设置了 overflow 属性引起的。 问题现象 在不设置 overflow 属性时,元素按预期显示在同一水平线上: 不设置 overf…

    2025年12月24日 好文分享
    400
  • 网页使用本地字体:为什么 CSS 代码中明明指定了“荆南麦圆体”,页面却仍然显示“微软雅黑”?

    网页中使用本地字体 本文将解答如何将本地安装字体应用到网页中,避免使用 src 属性直接引入字体文件。 问题: 想要在网页上使用已安装的“荆南麦圆体”字体,但 css 代码中将其置于第一位的“font-family”属性,页面仍显示“微软雅黑”字体。 立即学习“前端免费学习笔记(深入)”; 答案: …

    2025年12月24日
    000
  • 如何选择元素个数不固定的指定类名子元素?

    灵活选择元素个数不固定的指定类名子元素 在网页布局中,有时需要选择特定类名的子元素,但这些元素的数量并不固定。例如,下面这段 html 代码中,activebar 和 item 元素的数量均不固定: *n *n 如果需要选择第一个 item元素,可以使用 css 选择器 :nth-child()。该…

    2025年12月24日
    200
  • 使用 SVG 如何实现自定义宽度、间距和半径的虚线边框?

    使用 svg 实现自定义虚线边框 如何实现一个具有自定义宽度、间距和半径的虚线边框是一个常见的前端开发问题。传统的解决方案通常涉及使用 border-image 引入切片图片,但是这种方法存在引入外部资源、性能低下的缺点。 为了避免上述问题,可以使用 svg(可缩放矢量图形)来创建纯代码实现。一种方…

    2025年12月24日
    100
  • 如何让“元素跟随文本高度,而不是撑高父容器?

    如何让 元素跟随文本高度,而不是撑高父容器 在页面布局中,经常遇到父容器高度被子元素撑开的问题。在图例所示的案例中,父容器被较高的图片撑开,而文本的高度没有被考虑。本问答将提供纯css解决方案,让图片跟随文本高度,确保父容器的高度不会被图片影响。 解决方法 为了解决这个问题,需要将图片从文档流中脱离…

    2025年12月24日
    000
  • 为什么我的特定 DIV 在 Edge 浏览器中无法显示?

    特定 DIV 无法显示:用户代理样式表的困扰 当你在 Edge 浏览器中打开项目中的某个 div 时,却发现它无法正常显示,仔细检查样式后,发现是由用户代理样式表中的 display none 引起的。但你疑问的是,为什么会出现这样的样式表,而且只针对特定的 div? 背后的原因 用户代理样式表是由…

    2025年12月24日
    200
  • inline-block元素错位了,是为什么?

    inline-block元素错位背后的原因 inline-block元素是一种特殊类型的块级元素,它可以与其他元素行内排列。但是,在某些情况下,inline-block元素可能会出现错位显示的问题。 错位的原因 当inline-block元素设置了overflow:hidden属性时,它会影响元素的…

    2025年12月24日
    000
  • 为什么 CSS mask 属性未请求指定图片?

    解决 css mask 属性未请求图片的问题 在使用 css mask 属性时,指定了图片地址,但网络面板显示未请求获取该图片,这可能是由于浏览器兼容性问题造成的。 问题 如下代码所示: 立即学习“前端免费学习笔记(深入)”; icon [data-icon=”cloud”] { –icon-cl…

    2025年12月24日
    200
  • 为什么使用 inline-block 元素时会错位?

    inline-block 元素错位成因剖析 在使用 inline-block 元素时,可能会遇到它们错位显示的问题。如代码 demo 所示,当设置了 overflow 属性时,a 标签就会错位下沉,而未设置时却不会。 问题根源: overflow:hidden 属性影响了 inline-block …

    2025年12月24日
    000
  • 如何使用 vue-color 创建交互式颜色渐变页面?

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

    2025年12月24日
    200
  • 如何利用 CSS 选中激活标签并影响相邻元素的样式?

    如何利用 css 选中激活标签并影响相邻元素? 为了实现激活标签影响相邻元素的样式需求,可以通过 :has 选择器来实现。以下是如何具体操作: 对于激活标签相邻后的元素,可以在 css 中使用以下代码进行设置: li:has(+li.active) { border-radius: 0 0 10px…

    2025年12月24日
    100
  • 为什么我的 CSS 元素放大效果无法正常生效?

    css 设置元素放大效果的疑问解答 原提问者在尝试给元素添加 10em 字体大小和过渡效果后,未能在进入页面时看到放大效果。探究发现,原提问者将 CSS 代码直接写在页面中,导致放大效果无法触发。 解决办法如下: 将 CSS 样式写在一个单独的文件中,并使用 标签引入该样式文件。这个操作与原提问者观…

    2025年12月24日
    000
  • 如何模拟Windows 10 设置界面中的鼠标悬浮放大效果?

    win10设置界面的鼠标移动显示周边的样式(探照灯效果)的实现方式 在windows设置界面的鼠标悬浮效果中,光标周围会显示一个放大区域。在前端开发中,可以通过多种方式实现类似的效果。 使用css 使用css的transform和box-shadow属性。通过将transform: scale(1.…

    2025年12月24日
    200
  • 为什么我的 em 和 transition 设置后元素没有放大?

    元素设置 em 和 transition 后不放大 一个 youtube 视频中展示了设置 em 和 transition 的元素在页面加载后会放大,但同样的代码在提问者电脑上没有达到预期效果。 可能原因: 问题在于 css 代码的位置。在视频中,css 被放置在单独的文件中并通过 link 标签引…

    2025年12月24日
    100

发表回复

登录后才能评论
关注微信