今天的挑战:打造一个1KB的小前端工具库
一个更复杂的例子:待办事项列表应用
那么,接下来呢?
今天我们来迎接一个激动人心的挑战:创建一个仅 1千字节(约1KB) 大小的前端库。我指的是一个“隐形框架”——不像 Svelte,它只是在编译之后才“隐形”。不需要构建工具,也不需要占用你SSD的庞大 node_modules
文件夹。只需要少量轻量级的JavaScript函数,你可以直接复制、粘贴并立即使用。
系好你的安全带!
略
信号响应性到2025年,前端世界在一件事情上基本上达成了共识:响应式信号。几乎所有主流框架都有自己的响应式信号实现——比如 Svelte 的 $state
符号或是 Vue 的 ref()
。
如果你刚接触信号,不用担心,只需记住两个关键概念就可以了。
- 信号:可以读取和更新的响应式值。
- 效应:依赖信号的函数。当信号变化时,其依赖的效应会自动重新执行。
一个小的 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 点赞
评论
共同学习,写下你的评论
评论加载中...
作者其他优质文章
正在加载中
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦