JS生态系统加速eslint解析器使用实例探索

 更新时间:2024年01月21日 11:46:52   作者:大家的林语冰 人猫神话  
这篇文章主要为大家介绍了JS生态系统加速之eslint解析器使用实例探索,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

长话短说:Linting 是在代码中查找模式的行为,这可能导致错误或确保一致的阅读体验。它是一大坨 JS/TS 项目的核心部分。我们发现其选择器引擎和 AST 转换过程存在时间优化的巨大潜力,并且诉诸 JS 编写的完美 linter 能够达到亚秒级的运行时间。

本期《前端翻译计划》共享的是“加速 JS 生态系统系列博客”,包括但不限于:

  • PostCSS,SVGO 等等
  • 模块解析
  • 使用 eslint
  • npm 脚本
  • draft-js emoji 插件
  • polyfill 暴走
  • 桶装文件崩溃
  • Tailwind CSS

本期共享的是第 2 篇博客 —— eslint。

在本系列的前两篇文章中,我们已经讨论了一大坨关于 linting 的问题,所以是时候让 eslint 崭露头角了。总体而言,eslint 非常灵活,您甚至可以将解析器更换为截然不同的解析器。随着 JSX 和 TS 的兴起,这种情况屡见不鲜。凭借健康的插件和预设生态系统的丰富,每个用例可能都有一个规则,如果没有,优秀的文档会指引您创建自己的规则。

但这也给性能分析带来了一个问题,因为由于强大的配置灵活性,两个项目在 linting 性能方面可能会有截然不同的体验。

使用 eslint

eslint 的代码库使用任务运行程序抽象来协调常见的构建任务,但通过抽丝剥茧,我们可以拼凑出用于“lint”任务运行的命令,尤其是 JS 文件的 lint。

node bin/eslint.js --report-unused-disable-directives . --ignore-pattern "docs/**"

如你所见:ESLint 正在使用 ESLint 检查自己的代码库!我们将通过 Node 的内置 --cpu-prof 参数生成 *.cpuprofile,并将其加载到 Speedscope 中分析。

通过将类似的调用堆栈合并在一起,我们可以更清楚地了解时间开销的“重灾区”。这通常被称为“left-heavy”可视化。不要将其与标准火焰图混淆,火焰图的 x 轴表示调用发生的时间。相反,这里的 x 轴表示总时间中消耗的时间,而不是发生的时间。

我们立即可以找出 eslint 代码库中的 linting 设置花费时间的若干关键区域。值得注意的是,总时间的很大一部分花在处理 JSDoc 的规则上(从函数名称推断)。另一个有趣的方面是,在 lint 任务期间有两个不同的解析器在不同时间运行:esquery 和 acorn。但 JSDoc 规则花了这么长时间,激起了我的好奇心。

一个特别的 BackwardTokenCommentCursor 入口似乎很有趣,因为它是该组块中最大的区块。根据附加的文件定位到源码,它似乎是一个保存我们在文件中位置状态的类。作为第一个措施,我添加了一个普通计数器,每当实例化该类并再次运行 lint 任务时,该计数器就会递增。

2000 万次

总而言之,该类已被构造超过 2000 万次。这看起来太多了。粉丝请记住,我们实例化的任何对象或类都会占用内存,并且稍后需要清理该内存。我们可以在数据中看到垃圾收集(清理内存的行为)总共花费 2.43 秒的结果。这并不好。

创建该类的新实例后,它会调用两个函数,这两个函数似乎都会启动搜索。如果不了函数的细节,那么可以排除第一个函数,因为它不包含任何形式的循环。根据经验,循环通常是研究性能的主要嫌疑人。

第二个函数称为 utils.search(),它包含了一个循环。它循环遍历从我们当时检查的文件内容中解析出的 token 流。token 是编程语言的最小构建块,您可以将它们视为语言的“单词”。举个栗子,在 JS 中,“函数”一词通常表示为一个函数 token,逗号或单个分号也是举一反一。在 utils.search() 函数中,我们似乎关心的是找到距离文件中当前位置最近的 token。

exports.search = function search(tokens, location) {
  const index = tokens.findIndex(el => location <= getStartLocation(el))
  return index === -1 ? tokens.length : index
}

为此,搜索是通过 JS 的原生 .findIndex() 方法在 token 数组上完成的。该算法的说明是:

findIndex() 是一种迭代方法。它按升序索引顺序为数组中的每个元素调用一次提供的 callbackFn 函数,直到 callbackFn 返回真值。

鉴于 token 数组随着文件中代码量的增加而增长,这听起来并不理想。我们可以使用更有效的算法来搜索数组中的值,而不是遍历数组中的每个元素。举个栗子,用二分搜索替换那行代码可以将时间减少 50%。

虽然减少 50% 看似不错,但它仍然没有解决此代码被调用 2000 万次的问题。对我而言,这才是问题所在。我们或多或少地试图减少症状的影响,但是治标不治本。我们已经在遍历该文件,因此我们应该确切地知道我们在哪里。不过,改变这一点需要更具侵入性的重构,并且对于本文而言超纲了。看到这不是一个容易解决的问题,我检查了配置文件中还有哪些值得关注的内容。中心的长紫色条很难被忽视,不仅因为它们的颜色不同,而且因为它们占用了大量时间,并且没有深入到数百个较小的函数调用。

选择器引擎

speedscope 中的调用堆栈指向一个名为 esquery 的项目。这是一个较旧的项目,其目标是能够通过小型选择器语言在解析的代码中找到特定对象。如果您仔细观察,您会发现它与 CSS 选择器非常相似。它们的套路基本相同,只是我们没有在 DOM 树中找到特定的 HTML 元素,而是在另一个树结构中找到一个对象。

调试表明 npm 包附带了压缩的源码。混淆的变量名通常是单个字符,强烈暗示压缩的过程正在发生。对我而言幸运的是,该软件包还附带了一个未压缩的变体,因此我修改了 package.json 来指向它。稍后再运行一次,我们会得到以下数据:

好多了!对于未压缩的代码,需要记住的一点是,它的执行速度比压缩的变体慢大约 10-20%。这是一个粗略的近似范围,在比较压缩代码和未压缩代码的性能时,我在整个职业生涯中多次测量过此范围。有了这个经验,getPath 函数似乎需要一些帮助。

function getPath(obj, key) {
  var keys = key.split('.')
  var _iterator = _createForOfIteratorHelper(keys),
    _step
  try {
    for (_iterator.s(); !(_step = _iterator.n()).done; ) {
      var _key = _step.value
      if (obj == null) {
        return obj
      }
      obj = obj[_key]
    }
  } catch (err) {
    _iterator.e(err)
  } finally {
    _iterator.f()
  }
  return obj
}

过时的转译将困扰我们很长一段时间

如果您已经接触 JS 工具领域一段时间,那么这些函数看起来非常熟悉。_createForOfIteratorHelper 99.99% 是由它们的发布管道插入的函数,而不是由该库的作者插入的。当 for-of 循环被添加到 JS 中时,花了一段时间才普遍支持。

向下转译现代 JS 功能的工具往往会因谨慎而犯错,并以非常保守的方式重写代码。在本例中,我们将 string 拆分为字符串数组。使用完整的迭代器来循环它完全是把饭叫饥,并且一个无聊的循环标准足矣。但由于工具没有意识到这一点,因此它们选择了覆盖尽可能多场景的变体。以下是用于比较的原始代码:

function getPath(obj, key) {
  const keys = key.split('.')
  for (const key of keys) {
    if (obj == null) {
      return obj
    }
    obj = obj[key]
  }
  return obj
}

今时今日,for-of 循环普遍支持,因此我再次修补了该包,并将函数实现替换为源码中的原始函数实现。这一简单更改可节省大约 400 毫秒。我总是对我们在浪费的 polyfill 或过时的向下转译上消耗了多少 CPU 时间印象深刻。我还测量了用标准 for 循环替换 for-of 循环。

function getPath(obj, key) {
  const keys = key.split('.')
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    if (obj == null) {
      return obj
    }
    obj = obj[key]
  }
  return obj
}

令人惊讶的是,与 for-of 变体相比,这又提高了 200 毫秒。我想即使在今天,for-of 循环也更难针对引擎进行优化。这让我想起了过去的一项调查,Jovi 和我在发布新版本并切换到 for-of 循环时,对 graphql 包的解析速度突然变慢进行调查。

这是 v8/gecko/webkit 工程师可以正确验证的东东,但我的假设是它仍然必须调用迭代器协议,因为这可能已被全局覆盖,这将改变每个数组的行为。大抵就是如此吧。

虽然我们从这些变化中快速斩获了一些成果,但仍远未达到理想状态。总体而言,该功能仍然是有待优化的首要竞争者,因为它单独负责了总时间的几秒钟。再次应用快速计数器的奇技淫巧,发现它被调用了大约 22k 次。可以肯定的是,这个函数在某种程度上处于“hot”路径中。

特别值得注意的是,一大坨处理字符串的性能密集型代码都围绕 String.prototype.split() 方法。这将有效地迭代所有字符,分配一个新数组,然后迭代该数组,所有这些都可以在一次迭代中完成。

function getPath(obj, key) {
  let last = 0
  // 有效,因为所有的键都是 ASCII,而不是 unicode
  for (let i = 0; i < key.length; i++) {
    if (obj == null) {
      return obj
    }
    if (key[i] === '.') {
      obj = obj[key.slice(last, i)]
      last = i + 1
    } else if (i === key.length - 1) {
      obj = obj[key.slice(last)]
    }
  }
  return obj
}

这次重写对其性能影响巨大。当我们开始时,getPath 总共花费了 2.7 秒,应用了所有优化后,我们设法将其降低到 486 毫秒。

继续使用 matches() 函数,我们看到奇怪的 for-of 向下转译产生了大量开销。为了节省时间,我直接在 github 上复制了源码中的函数。由于 matches() 在调试中更加突兀,因此仅此更改就节省了整整 1 秒。

我们生态系统中的一大坨库都面临此问题。我真的希望有一种银弹可以一键更新它们。也许我们需要一个反向转译器来检测向下转译模式,并将其再次转换回现代代码。

我联系了 jviide,看看是否可以进一步优化 matches()。通过其额外更改,我们能够使整个选择器代码比原始未修改状态快大约 5 倍。它基本上所做的就是消除 matches() 函数中的大量开销,这也使它能够简化一些相关的辅助函数。举个栗子,它注意到模板字符串的转译效果很差。

// 输入
const literal = `${selector.value.value}`

// 输出,向下转译很慢
const literal = ''.concat(selector.value.value)

它甚至更进一步,将每个新选择器解析为动态函数调用链,并缓存生成的包装函数。这个技巧再次大幅加速了选择器引擎。

提早纾困

有时退后一步,从不同的角度解决问题是件好事。到目前为止,我们已经了解了实现细节,但我们实际上正在处理什么样的选择器?是否有可能使其中一些短路?为了测试这个理论,我首先需要更好地了解正在处理的选择器类型。毫不奇怪,大多数选择器都很短。其中有几个确实很有特色。举个栗子,这是一个简单选择器:

VariableDeclaration:not(ExportNamedDeclaration > .declaration) > VariableDeclarator.declarations:matches(
  [init.type="ArrayExpression"],
  :matches(
 [init.type="CallExpression"],
[init.type="NewExpression"]
  )[init.optional!=true][init.callee.type="Identifier"][init.callee.name="Array"],
[init.type="CallExpression"][init.optional!=true][init.callee.type="MemberExpression"][init.callee.computed!=true][init.callee.property.type="Identifier"][init.callee.optional!=true]
 :matches(
   [init.callee.property.name="from"],
   [init.callee.property.name="of"]
)[init.callee.object.type="Identifier"][init.callee.object.name="Array"],
[init.type="CallExpression"][init.optional!=true][init.callee.type="MemberExpression"][init.callee.computed!=true][init.callee.property.type="Identifier"][init.callee.optional!=true]:matches(
   [init.callee.property.name="concat"],
   [init.callee.property.name="copyWithin"],
   [init.callee.property.name="fill"],
   [init.callee.property.name="filter"],
   [init.callee.property.name="flat"],
   [init.callee.property.name="flatMap"],
   [init.callee.property.name="map"],
   [init.callee.property.name="reverse"],
   [init.callee.property.name="slice"],
   [init.callee.property.name="sort"],
   [init.callee.property.name="splice"]
 )
  ) > Identifier.id

这无疑是一个有点偏离轨道的例子。我不想成为那个在不正确匹配时必须进行调试的倒霉蛋。这是我对任何形式的自定义领域特定语言的主要抱怨。它们通常根本不提供工具支持。如果我们留在 JS 领域,我们可以使用适当的调试器随时随地检查该值。虽然前面的字符串选择器示例有点极端,但大多数选择器如下所示:

BinaryExpression

/* 或者 */
VariableDeclaration

仅此而已。大多数选择器只是想知道当前 AST 节点是否属于某种类型。为此,我们实际上并不需要整个选择器引擎。如果我们为此引入一条捷径,并完全绕过选择器引擎会怎么样?

class NodeEventGenerator {
  // ...
  isType = new Set([
    'IfStatement',
    'BinaryExpression'
    // 其他......
  ])
  applySelector(node, selector) {
    // 捷径,只需断言类型
    if (this.isType.has(selector.rawSelector)) {
      if (node.type === selector.rawSelector) {
        this.emitter.emit(selector.rawSelector, node)
      }
      return
    }
    // 回退到完整的选择器引擎匹配
    if (
      esquery.matches(
        node,
        selector.parsedSelector,
        this.currentAncestry,
        this.esqueryOptions
      )
    ) {
      this.emitter.emit(selector.rawSelector, node)
    }
  }
}

由于我们已经短路了选择器引擎,我开始好奇字符串化选择器与以纯 JS 函数编写的选择器相比如何。我的直觉告诉我,将选择器编写为简单的 JS 条件会更容易针对引擎优化。

反思选择器

如果您需要像我们在浏览器中使用 CSS 那样跨越语言障碍传递遍历命令,那么选择器引擎非常有用。但它从来都不是免费的,因为选择器引擎总是需要解析选择器来解构我们应该做什么,然后动态构建一些逻辑来执行解析的东西。

但在 eslint 内部我们没有跨越任何语言障碍。我们还停留在 JS 领域。因此,通过将查询指令转换为选择器,并将它们解析回我们可以再次运行的内容,我们不会获得任何性能方面的好处。相反,我们消耗了大约 25% 的总 linting 时间来解析和执行选择器。我们需要一种新方案。

从概念上讲,选择器只不过是一个“描述”,用于根据它所持有的条件来查找元素。这可能是在树或平面数据结构(比如 array)中的查找。如果您考虑一下,即使标准 Array.prototype.filter() 调用中的回调函数也是一个选择器。我们从元素集合(数组)中选择值,并仅选择我们关心的值。我们对 esquery 所做的事情完全相同。从一堆对象(AST 节点)中,我们挑选出符合特定条件的对象。那就是一个选择器!那么,如果我们避免选择器解析逻辑,并使用纯 JS 函数呢?

// String 筑基的 esquery 选择器
const esquerySelector = `[type="CallExpression"][callee.type="MemberExpression"][callee.computed!=true][callee.property.type="Identifier"]:matches([callee.property.name="substr"], [callee.property.name="substring"])`
// 纯 JS 函数的同款选择器
function jsSelector(node) {
  return (
    node.type === 'CallExpression' &&
    node.callee.type === 'MemberExpression' &&
    !node.callee.computed &&
    node.callee.property.type === 'Identifier' &&
    (node.callee.property.name === 'substr' ||
      node.callee.property.name === 'substring')
  )
}

让我们尝试一下吧!我编写了一些基准来衡量这两种方案的时间差异。

whatfoo.substr(1, 2) ops/sec
esquery422,848.208
esquery(优化)3,036,384.255
纯 JS 函数66,961,066.5239

看起来纯 JS 函数变体对基于字符串的函数变体“降维打击”。这简直棒棒哒。即使在花费了所有时间来使 esquery 更快之后,它仍远不及 JS 变体。在选择器不匹配,且引擎可以提前退出的情况下,它仍然比普通函数慢 30 倍。这个小实验证实了我的假设,即我们为选择器引擎付出了相当多的时间。

第三方插件和预设的影响

尽管在 eslint 设置的配置文件中可以看到更多的优化空间,但我开始怀疑我是否花时间优化了正确的事情。到目前为止,我们在 eslint 自己的 linting 设置中看到的相同问题是否也出现在其他 linting 设置中?eslint 的主要优势之一始终是其灵活性和对第三方 linting 规则的支持。回顾过去,我从事的几乎每个项目都安装了一些自定义 linting 规则和大约 2-5 个额外的 eslint 插件或预设。但更重要的是,它们完全关闭了解析器。快速浏览一下 npm 下载统计数据,就可以发现替换 eslint 内置解析器的趋势:

软件包npm 周下载量%
eslint31.718.905100%
@typescript-eslint/parser23.192.76773%
@babel/eslint-parser6.057.11019%

如果这些数字可信,那就意味着,所有 eslint 用户中只有 8% 使用内置解析器。它还显示了 TS 已经变得多么普遍,占 eslint 总用户群的最大份额(73%)。我们没有关于 babel 解析器的用户是否也将其用于 TS 的数据。我的猜测是,它们中的一部分人这样做了,而且 TS 用户的总数实际上甚至更高。

在分析各种开源存储库中的若干不同设置后,我选择了 Vite 中的一个,它也包含其他配置文件中存在的大量模式。它的代码库是用 TS 编写的,并且 eslint 的解析器已相应替换。

和之前一样,我们可以找出各个区域,显示时间花在哪里。有一个区域暗示了,从 TS 格式到 eslint 格式的转换需要相当多的时间。配置加载也发生了一些怪事,因为它永远不会像这里那样占用那么多时间。我们找到了一个老朋友,eslint-import-plugin 和 eslint-plugin-node,它们似乎启动了一堆模块解析逻辑。

不过,这里有趣的一点是,选择器引擎的开销并未显示。有一些 applySelector 函数被调用的实例,但从整体上看它几乎不消耗任何时间。

eslint-plugin-import 和 eslint-plugin-node 似乎总是弹出并需要相当长的时间才能执行的两个第三方插件。每当这些插件之一或两个处于活动状态时,它就会真正显示在分析数据中。两者都会导致大量的文件系统流量,因为它们尝试解析一堆模块,但不缓存结果。

转换所有 AST 节点

我们将从起初发生的 TS 转换开始。我们的工具将提供给它们的代码解析为一种称为 AST(抽象语法树)的数据结构。您可以将其视为我们所有工具所使用的构建块。它告诉的信息如下:“瞧,这里我们声明一个变量,它有这个名称和那个值”,或者“这里有一个带有这个条件的 if 语句,它保护那个代码块”,等等。

// `const foo = 42` 的 AST 形式如下所示:
{
  type: "VariableDeclaration",
  kind: "const",
  declarations: [
    {
      kind: "VariableDeclarator",
      name: {
        type: "Identifier",
        name: "foo",
      },
      init: {
        type: "NumericLiteral",
        value: 42
      }
  ]
}

您可以在优秀的 AST Explorer 网站上亲眼看到我们的工具如何解析代码。它可以让您更好地了解我们工具的 AST 格式的异同点。

虽然但是,在 eslint 的情况下存在问题。无论我们选择什么解析器,我们都希望规则能够正常工作。当我们激活 no-console 规则时,我们希望它适用于所有规则,而不是强制为每个解析器重写每个规则。本质上,我们需要的是一个我们都同意的共享 AST 格式。这正是 eslint 所做的。它期望每个 AST 节点都匹配 estree 规范,该规范规定了每个 AST 节点的外观。这个规范存在已久,一大坨 JS 工具都始于该规范。甚至 babel 也构建于其上,但此后有若干记录在案的偏差。

但当您使用 TS 时,问题的症结就在这里。TS 的 AST 格式非常不同,因为它还需要考虑代表类型本身的节点。一些构造在内部也有不同的表示,因为它使 TS 本身变得更容易。这意味着,每个 TS AST 节点都必须转换为 eslint 格式。这种转换需要时间。在此配置文件中,约占总时间的 22%。花费这么长时间的原因不仅仅是遍历本身,而且每次转换我们都会分配新的对象。我们在内存中基本上有两个不同 AST 格式的副本。

也许 babel 的解析器更快?如果我们用 @babel/eslint-parser 替换 @typescript-eslint/parser,那又如何?

what解析时间
@typescript-eslint/parser2.1s
@babel/eslint-parser + @babel/preset-typescript0.6s

事实证明,这样做可以节省相当多的时间。有趣的是,这一更改还大大缩短了配置加载时间。配置加载时间的改进可能是由于 babel 的解析器分布在更少的文件中。

粉丝请注意,虽然 babel 解析器明显更快,但它不支持类型感知 linting。这是 @typescript-eslint/parser 独有的功能。这为诸如 no-for-in-array 规则之类的规则提供了可能性,它可以检测您在 for-in 循环中迭代的变量实际上是否是 object 的 array。因此您可能想继续使用 @typescript-eslint/parser。如果您确信您不使用它们的任何规则,并且您只是需要 eslint 来理解 TS 的语法,再加上更快一点的 lint,那么切换到 babel 的解析器是一个不错的选择。

理想的 linter 是什么样子?

我偶然发现了关于 eslint 未来的讨论,其中性能是首要任务之一。其中提出了一些很棒的想法,特别是引入会话的概念,这允许完整的程序检查,而不是像今天那样在每个文件的基础上进行检查。鉴于至少 73% 的 eslint 用户使用它来检查 TS 代码,因此需要更少 AST 转换的更紧密集成也会对性能产生巨大影响。

还有一些关于 Rust 移植的讨论,这激起了我对当前基于 Rust 的 JS linter 的速度有多快的好奇。rslint 是唯一一个似乎已经做好生产准备,并能够解析大部分 TS 语法的工具。

除了 rslint,我还开始想知道纯 JS 中的简单 linter 会是什么样子。一种没有选择器引擎,不需要持续的 AST 转换,只需要解析代码并检查其上的各种规则。所以我用一个非常简单的 API 包装了 babel 的解析器,并添加了自定义遍历逻辑来遍历 AST 树。我没有选择 babel 自己的遍历函数,因为它们在每次迭代时都会导致大量分配,并且是基于生成器构建的,这比不使用生成器要慢一些。还尝试了一些我自己多年来编写的自定义 JS/TS 解析器,这些解析器源于几年前将 esbuild 的解析器移植到 JS。

话虽如此,以下是在 Vite 存储库上运行它们时的数字(144 个文件)。

what时间
eslint(JS)5.85s
自定义 linter(JS)0.52s
rslint(Rust 筑基)0.45s

基于这些数字,我相当有信心,基于这个小实验,我们只需使用 JS 就可以非常接近 Rust 的性能。

完美谢幕

总的来说,eslint 项目有着非常光明的前景。它是最成功的 OSS 项目之一,并且找到了获得大量资金的秘诀。我们研究了一些能让 eslint 更快的东西,还有一大坨这里没有涉及的领域需要研究。

“eslint 的未来”讨论包含了一大坨伟大的想法,这些想法将使 eslint 变得更好且可能更快。我认为头大的一点是,避免尝试立即解决所有问题,因为根据我的经验,这通常注定会失败。彻底重写也是如此。相反,我认为当前的代码库是一个完美的起点,可以被塑造成更棒的东西。

从局外人的角度来看,需要做出一些关键决定。举个栗子,此时继续支持基于字符串的选择器是否有意义?如果是,eslint 团队是否有能力承担 esquery 的维护工作,并给予它一些急需的关爱?鉴于 npm 下载计数表明 73% 的 eslint 用户是 TS 用户,那么原生 TS 支持又如何呢?

免责声明

本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考,英文原味版请传送 Speeding up the JavaScript ecosystem - eslint

以上就是JS生态系统加速eslint解析器使用实例探索的详细内容,更多关于JS eslint解析器的资料请关注脚本之家其它相关文章!

相关文章

  • 微信小程序商品详情页的底部弹出框效果

    微信小程序商品详情页的底部弹出框效果

    这篇文章主要为大家详细介绍了微信小程序商品详情页的底部弹出框效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-04-04
  • 用js通过url传参把数据从一个页面传到另一个页面

    用js通过url传参把数据从一个页面传到另一个页面

    如果是传到新页面的话,你网站基于什么语言开发直接用get或者post获取,然后输出到这个层
    2014-09-09
  • js实现炫酷光感效果

    js实现炫酷光感效果

    这篇文章主要为大家详细介绍了js实现炫酷光感效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-09-09
  • 微信小程序实现滑动/点击切换Tab及scroll-left的使用

    微信小程序实现滑动/点击切换Tab及scroll-left的使用

    这篇文章主要介绍了微信小程序实现滑动/点击切换Tab,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-04-04
  • js方法数据验证的简单实例

    js方法数据验证的简单实例

    下面小编就为大家带来一篇js方法数据验证的简单实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-09-09
  • JS+CSS实现自动切换的网页滑动门菜单效果代码

    JS+CSS实现自动切换的网页滑动门菜单效果代码

    这篇文章主要介绍了JS+CSS实现自动切换的网页滑动门菜单效果代码,涉及JavaScript基于时间函数动态变换页面tab样式的技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-09-09
  • javascript设计模式之模块模式学习笔记

    javascript设计模式之模块模式学习笔记

    这篇文章主要为大家详细介绍了javascript设计模式之模块模式学习笔记,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-02-02
  • javascript实现简单小钢琴有声弹奏效果

    javascript实现简单小钢琴有声弹奏效果

    用HTML5+javascript实现的小钢琴,按下钢琴键上的相应字母用或用鼠标点击钢琴键发声,javascript代码包含了对鼠标按下、移动和松开,以及键盘按下的事件监听
    2024-02-02
  • 文件上传插件SWFUpload的使用指南

    文件上传插件SWFUpload的使用指南

    本文主要介绍了文件上传插件SWFUpload使用指南,SWFUpload是一个flash和js相结合而成的文件上传插件,其功能非常强大。需要的朋友可以参考下
    2016-11-11
  • Ionic3实现图片瀑布流布局

    Ionic3实现图片瀑布流布局

    本篇文章主要介绍了Ionic3实现图片瀑布流布局,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08

最新评论