为了账号安全,请及时绑定邮箱和手机立即绑定

1KB前端小库:实现响应式编程与模板渲染

今天的挑战:打造一个1KB的小前端工具

今天我们来迎接一个激动人心的挑战:创建一个仅 1千字节(约1KB) 大小的前端库。我指的是一个“隐形框架”——不像 Svelte,它只是在编译之后才“隐形”。不需要构建工具,也不需要占用你SSD的庞大 node_modules 文件夹。只需要少量轻量级的JavaScript函数,你可以直接复制、粘贴并立即使用。

系好你的安全带!

信号响应性

到2025年,前端世界在一件事情上基本上达成了共识:响应式信号。几乎所有主流框架都有自己的响应式信号实现——比如 Svelte 的 $state 符号或是 Vue 的 ref()

如果你刚接触信号,不用担心,只需记住两个关键概念就可以了。

  1. 信号:可以读取和更新的响应式值。
  2. 效应:依赖信号的函数。当信号变化时,其依赖的效应会自动重新执行。

一个小的 Tiny Signals 实现

我们的信号实现受到了安德烈·吉亚马尔奇的精彩文章《关于信号的详细解析》的启发。如果你对细节感兴趣,我非常推荐你读一读。

    {

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

    function 信号(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 效应(fn) {
      effects.push(fn);
      try {
        fn();
        return () => disposed.add(fn);
      } finally {
        effects.pop();
      }
    }

    }

    function 计算(fn) {
      const s = 信号();
      s.dispose = 效应(() => s(fn()));
      return s;
    }

点击全屏进入;点击退出全屏

它是怎么工作的:

  • 我们使用一个 块级作用域 ({}) 来将变量保持在全局命名空间之外。这在无法使用模块时非常有用。
  • signal 函数创建一个响应式值。它返回一个函数,该函数既可以用作获取器也可以用作设置器:

  • 如果不带参数调用,则返回当前值,并订阅当前激活的效果到信号。

  • 如果用新值调用,则更新信号并触发所有已订阅的效果(除非这些效果已经被释放)。
  • effect 函数注册一个回调,该回调会在注册时立即执行,并会在其依赖信号变化时重新执行。
  • computed 函数创建一个 衍生信号 — 一个响应式值,每当其依赖项发生变化时都会重新计算值。

示例

    const count = signal(0); // 创建一个初始值为0的信号实例

    effect(() => {
      console.log(`Count is: ${count()}`); // 记录当前的信号值
    });

    count(1); // 更新信号,这会触发effect并记录 "Count是: 1"
    count(2); // 再次更新信号,这会记录 "Count是: 2"

全屏 退出

此处为空

响应式 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);
    });
  }
}

进入全屏 退出全屏

主要特点:

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

例如.

      // 响应式状态
      const count = signal(0);

      // 渲染应用
      const app = html`<div>
          <h1>计数器: ${count}</h1>
          <button onclick=${() => count((val) => val + 1)}>加一</button>
          <button onclick=${() => count((val) => val - 1)}>减一</button>
        </div>`;

      // 将应用挂载到 DOM 上
      document.body.append(...app);

全屏 退出全屏


一个更复杂的例子:待办事项列表应用

试试这个用我们小巧的库构建的互动待办事项清单应用。这只是一个用几行代码就能完成的绝佳示例。


那么,接下来呢?

在接下来的一期中,我们仅用一个函数,实现列表的高效重新渲染。敬请关注!🚀

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号

举报

0/150
提交
取消