这是我参与「掘金日新计划 · 8 月更文挑战」的第14天,点击查看活动详情
上一节看了编译部分,编译后的 DOM 操作是调用 dom-expressions 下面的方法实现的,这一节来看一下 dom-expressions 仓库中一些逻辑的源码实现。
整个 dom-expressions 仓库分为 client 和 server 两个入口,代表 client side render 和 server side render,我们现在只看 client 部分逻辑。client render 的入口是 render 函数,这部分之前阅读过,不过之前的主要关注点在 createRoot 的调用,这里来看 DOM 相关的操作。createRoot 传入的回调执行时会完成 dom 的创建,我们给 render 传第二个参数作为根节点,在这一步会调用 insert 把我们根组件执行的结果插入到根节点中。
insert 这个方法我们前面见过很多次,这里来看一下源码实现:
export function insert(parent, accessor, marker, initial) {
if (marker !== undefined && !initial) initial = [];
if (typeof accessor !== "function") return insertExpression(parent, accessor, initial, marker);
effect(current => insertExpression(parent, accessor(), current, marker), initial);
}
这里的 accessor 可能是一个普通的 dom 节点,也可能是 solid 组件函数,对于普通的节点可以直接插入,如果是函数组件,这里是在 effect 回调中插入,effect 对应的是 solid 中的 createEffect,这里相当于把插入节点的动作也交给 solid 的 effect 来调度。
我们可以继续阅读实际执行插入的 insertExpression 函数,这个函数的源码实现比较长,不过逻辑不难理解,就是判断待插入数据的类型,最终调用 appendChild 或 replaceChild 来完成节点插入的动作。
前面还见过一个 template 的调用,这个函数式构建 html 布局的关键。现代浏览器都一定程度支持 WebComponents 规范,想创建一块 html 最有效的方式就是借助 template 标签,这里的 template 方法也是这样做的:
export function template(html, check, isSVG) {
const t = document.createElement("template");
t.innerHTML = html;
if ("_DX_DEV_" && check && t.innerHTML.split("<").length - 1 !== check)
throw `The browser resolved template HTML does not match JSX input:\n${t.innerHTML}\n\n${html}. Is your HTML properly formed?`;
let node = t.content.firstChild;
if (isSVG) node = node.firstChild;
return node;
}
之后只需要调用 template 的 cloneNode 就可以创建一组 dom 节点了。
另一个比较重要的部分就是事件处理了,最终添加事件绑定是通过 delegateEvents 函数完成的,它接收事件名数组作为参数,源码实现如下:
export function delegateEvents(eventNames, document = window.document) {
const e = document[$$EVENTS] || (document[$$EVENTS] = new Set());
for (let i = 0, l = eventNames.length; i < l; i++) {
const name = eventNames[i];
if (!e.has(name)) {
e.add(name);
document.addEventListener(name, eventHandler);
}
}
}
这里的 Set 就是为了避免同一事件重复添加,这里面实际的有效代码就是 addEventListener,这里添加的 eventHandler 也是内置的实现,它完成了事件的分发和模拟:
function eventHandler(e) {
const key = `$$${e.type}`;
let node = (e.composedPath && e.composedPath()[0]) || e.target;
// some logic ...
while (node !== null) {
const handler = node[key];
if (handler && !node.disabled) {
const data = node[`${key}Data`];
data !== undefined ? handler.call(node, data, e) : handler.call(node, e);
if (e.cancelBubble) return;
}
node =
node.host && node.host !== node && node.host instanceof Node ? node.host : node.parentNode;
}
}
这里会拿到触发事件的目标节点,之后获取 就是在编译阶段 solid 添加的事件处理函数,之后调用这个事件处理函数就触发了事件监听。
上面这些就是比较常见的一些 solid 编译生成的 DOM 操作逻辑的实现,对这部分感兴趣可以继续深入阅读 dom-expressions 仓库。