1KB前端库

2025-06-07

1KB前端库

今天的挑战:构建一个 1KB 的前端库

今天,我们来挑战一个激动人心的挑战:创建一个只有1KB大小的前端库。我说的是一个“消失的框架”——不像 Svelte,它只会在编译后“消失”。无需构建工具,也无需node_modules占用 SSD 的臃肿文件夹。只需几个轻量级的 JavaScript 函数,即可立即复制、粘贴和使用。

系好安全带!


信号反应

到 2025 年,前端世界将在一件事上达成共识:信号驱动响应式设计。几乎每个主流框架都有自己的信号版本——比如 Vue 的 Runeref()和 Svelte 的$stateRune。

如果你对信号不熟悉,不用担心。只需记住两个关键概念:

  1. 信号:可以读取和更新的反应值。
  2. 效果:依赖于信号的函数。当信号发生变化时,其依赖的效果会自动重新运行。

微小信号的实现

我们的紧凑信号实现灵感来自 Andrea Giammarchi 的优秀文章《信号》。如果你对细节感兴趣,我强烈建议你读一读这篇文章。

{

const effects = [Function.prototype];
const disposed = new WeakSet();

function signal(value) {
  const subs = new Set();
  return (newVal) => {
    if (newVal === undefined) {
      subs.add(effects.at(-1));
      return value;
    }
    if (newVal !== value) {
      value = newVal?.call ? newVal(value) : newVal;
      for (let eff of subs) disposed.has(eff) ? subs.delete(eff) : eff();
    }
  };
}

function effect(fn) {
  effects.push(fn);
  try {
    fn();
    return () => disposed.add(fn);
  } finally {
    effects.pop();
  }
}

}

function computed(fn) {
  const s = signal();
  s.dispose = effect(() => s(fn()));
  return s;
}
Enter fullscreen mode Exit fullscreen mode

工作原理:

  • 我们使用块作用域( {}) 将变量保持在全局命名空间之外。当无法使用模块时,这非常方便。
  • signal函数创建一个响应式值。它返回一个同时充当 getter 和 setter 的函数:
    • 如果不带参数调用,它将返回当前值并订阅信号的活动效果。
    • 如果使用新值调用,它会更新信号并触发所有订阅的效果(除非它们被处置)。
  • effect函数注册一个立即运行的回调,并在其依赖的任何信号发生变化时重新运行。
  • computed函数创建一个派生信号——每次依赖项发生变化时都会重新计算的反应值。

示例用法:

const count = signal(0); // Create a signal with initial value 0

effect(() => {
  console.log(`Count is: ${count()}`); // Log the current value of the signal
});

count(1); // Update the signal, which triggers the effect and logs "Count is: 1"
count(2); // Update again, logs "Count is: 2"
Enter fullscreen mode Exit fullscreen mode

响应式 HTML 模板

现在,让我们添加一些模板和渲染魔法。我们将创建一个带标签的模板函数,html它解析 HTML 字符串并将响应式值动态绑定到 DOM。

{

function html(tpl, ...data) {
  const marker = "\ufeff";
  const t = document.createElement("template");
  t.innerHTML = tpl.join(marker);
  if (tpl.length > 1) {
    const iter = document.createNodeIterator(t.content, 1 | 4);
    let n,
      idx = 0;
    while ((n = iter.nextNode())) {
      if (n.attributes) {
        if (n.attributes.length)
          for (let attr of [...n.attributes])
            if (attr.value == marker) render(n, attr.name, data[idx++]);
      } else {
        if (n.nodeValue.includes(marker)) {
          let tmp = document.createElement("template");
          tmp.innerHTML = n.nodeValue.replaceAll(marker, "<!>");
          for (let child of tmp.content.childNodes)
            if (child.nodeType == 8) render(child, null, data[idx++]);
          n.replaceWith(tmp.content);
        }
      }
    }
  }
  return [...t.content.childNodes];
}

const render = (node, attr, value) => {
  const run = value?.call
    ? (fn) => {
        let dispose;
        dispose = effect(() =>
          dispose && !node.isConnected ? dispose() : fn(value())
        );
      }
    : (fn) => fn(value);
  if (attr) {
    node.removeAttribute(attr);
    if (attr.startsWith("on")) node[attr] = value;
    else
      run((val) => {
        if (attr == "value" || attr == "checked") node[attr] = val;
        else
          val === false
            ? node.removeAttribute(attr)
            : node.setAttribute(attr, val);
      });
  } else {
    const key = Symbol();
    run((val) => {
      const upd = Array.isArray(val)
        ? val.flat()
        : val !== undefined
        ? [document.createTextNode(val)]
        : [];
      for (let n of upd) n[key] = true;
      let a = node,
        b;
      while ((a = a.nextSibling) && a[key]) {
        b = upd.shift();
        if (a !== b) {
          if (b) a.replaceWith(b);
          else {
            b = a.previousSibling;
            a.remove();
          }
          a = b;
        }
      }
      if (upd.length) (b || node).after(...upd);
    });
  }
}

}
Enter fullscreen mode Exit fullscreen mode

主要特点:

  • html函数返回一个 DOM 节点数组。
  • 它使用语法支持动态属性、文本内容、子节点和事件监听器on*
  • 如果提供的值是一个函数(或信号本身),它会设置一个重新运行的效果来更新 DOM。

示例用法:


  // Reactive state
  const count = signal(0);

  // Render the app
  const app = html`<div>
      <h1>Counter: ${count}</h1>
      <button onclick=${() => count((val) => val + 1)}>Increment</button>
      <button onclick=${() => count((val) => val - 1)}>Decrement</button>
    </div>`;

  // Mount the app to the DOM
  document.body.append(...app);

Enter fullscreen mode Exit fullscreen mode

更复杂的例子:Todo 应用

看看这个用我们小巧的库构建的交互式 Todo 应用吧。它是一个很好的例子,展示了只需几行代码就能实现的功能。


下一步是什么?

在下一篇文章,我们将添加一个函数来实现高效的列表重新渲染。敬请期待!🚀

文章来源:https://dev.to/fedia/1kb-frontend-library-5ef1
PREV
📚 前 1% 的 React 开发者使用的 8 个 repos 🏆 如何找到前 1% 的开发者使用的 repos?🔦 🪮 jsxstyle/jsxstyle 💨 alangpierce/sucrase 🎨 wooorm/refractor 🐦 transitive-bullshit/react-static-tweets 🖨️ preactjs/preact-render-to-string 🏆 bikeshaving/crank 🎯 evoluhq/evolu 📸 jest-community/snapshot-diff
NEXT
天气应用:使用 Vue 和 Axios