runtime-dom
是针对浏览器的运行时,包括 DOM 操作、props
(例如class
、事件、样式以及其它attributes
)的更新等内容;本小节我们开启 runtime-dom
的篇章。
在 packages/runtime-dom/
目录下创建目录文件:
│ │ └─ src │ │ ├─ index.ts │ │ ├─ modules │ │ │ ├─ attr.ts // attributes 的更新方法 │ │ │ ├─ class.ts // class 的更新 │ │ │ ├─ event.ts // 事件绑定的更新 │ │ │ └─ style.ts // style属性的更新 │ │ ├─ nodeOps.ts // dom操作方法 │ │ └─ patchProp.ts // 属性更新操作
创建 runtime-dom/package.json
文件:
{ "name": "@vue/runtime-dom", "version": "1.0.0", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", "unpkg": "dist/runtime-dom.global.js", "buildOptions": { "name": "VueRuntimeDOM", "formats": [ "esm-bundler", "cjs", "global" ] } }
先创建一些操作 DOM 的方法,例如元素和文本的增删改查:
// runtime-dom/src/nodeOps.ts export const nodeOps = { // 1. 创建元素 createElement(tagName) { return document.createElement(tagName); }, // 创建文本节点 createText(text) { return document.createTextNode(text); }, // 2. 插入元素 insert(child, parent, anchor) { // 元素移动; // 当第二个参数为null时,插入到末尾; parent.insertBefore(child, anchor || null); }, // 3. 移除元素 remove(child) { const parent = child.parentNode; if (parent) { parent.removeChild(child); } }, // 4. 查询元素 querySelector(selector) { return document.querySelector(selector); }, parentNode(node) { return node.parentNode; }, nextSibling(node) { return node.nextSibling; }, // 5. 设置文本内容 setElementText(el, text) { el.textContent = text; }, setText(node, text) { node.nodeValue = text; }, };
再来实现一些属性的更新方法:
// runtime-dom/src/patchProp.ts import { patchAttr } from "./modules/attr"; import { patchClass } from "./modules/class"; import { patchEvent } from "./modules/event"; import { patchStyle } from "./modules/style"; export const patchProp = (el, key, prevValue, nextValue) => { if (key === "class") { // 1. class 类名 patchClass(el, nextValue); } else if (key === "style") { // 2. 样式 patchStyle(el, prevValue, nextValue); } else if (/^on[^a-z]/.test(key)) { // 3. onXxx 事件 patchEvent(el, key, nextValue); } else { // 4. 其它 attributes 属性 patchAttr(el, key, nextValue); } };
我们将 props
分成四种类型:class
、style
、onXxx
事件、以及其它 attributes
属性;分别用四种方法来处理这四种 prop
。
更新 class
属性:
value
为空时,使用 el.removeAttribute("class")
去掉 class
属性;className
属性。// runtime-dom/src/modules/class.ts export const patchClass = (el, value) => { if (value == null) { el.removeAttribute("class"); } else { el.className = value; } };
更新 style
属性:
style
没有新值时,去掉 style
属性;style
有新值时才进行更新;style
上,如果老的 style
中有重复的,则直接将老的样式覆盖style
中有、新的 style
中没有的样式,需要将其移除// runtime-dom/src/modules/style.ts export const patchStyle = (el, prev, next) => { if (next) { const style = el.style; // 1. 将新的样式添加到style上,如果有重复的直接覆盖 for (let key in next) { style[key] = next[key]; } for (let key in prev) { // 2. 老的有,新的没有,要移除掉 if (next[key] == null) { style[key] = null; } } } else { el.removeAttribute("style"); } };
更新事件(事件是以 onXxx
的形式绑定的):
invoker
来存储事件的回调函数(存储到invoker.value
上),以及执行回调(执行invoker.value()
)el._vei
属性上(缓存的是 invoker
)el._vei
中存在该事件的缓存,则是更新事件;直接更新 invoker.value
即可invoker
,并将invoker
缓存到 el._vei
中,然后通过 el.addEventListener()
绑定事件el.removeEventListener()
将该事件移除。// runtime-dom/src/modules/event.ts function createInvoker(initialValue) { const invoker = (e) => invoker.value(e); // 将事件的回调绑定到 invoker.value 上,后续更新事件回调的时候,只需更新 invoker.value 即可 invoker.value = initialValue; return invoker; } export const patchEvent = (el, key, nextValue) => { const invokers = el._vei || (el._vei = {}); // _vei 是 vue event invoker 的缩写 const name = key.slice(2).toLowerCase(); // 获取事件名 const existingInvoker = invokers[name]; // 取缓存 // 1. 如果是更新事件的回调 if (nextValue && existingInvoker) { // 事件是存储到 invoker.value 上的,更新该属性即可 existingInvoker.value = nextValue; } else { // 2. 如果是绑定新的事件 if (nextValue) { // 2.1 创建 invoker(事件的回调函数),并缓存起来(本质是缓存到el._vei上) const invoker = (invokers[name] = createInvoker(nextValue)); // 2.2 绑定事件 el.addEventListener(name, invoker); } else { // 3. 如果是移除事件 // 3.1 解绑事件 el.removeEventListener(name, existingInvoker); // 3.2 清除缓存 invokers[name] = null; } } };
更新其它 attributes
:
el.removeAttribute()
移除属性;el.setAttribute()
新增和更新属性// runtime-dom/src/modules/attr.ts export const patchAttr = (el, key, value) => { if (value == null) { el.removeAttribute(key); } else { el.setAttribute(key, value); } };
本小节我们大致介绍了 runtime-dom
模块,实现了一些 DOM 操作和 props
更新方法; 下一小节,我们将介绍 runtime-core
模块相关的内容。