1. runtime-dom 的作用
runtime-dom 针对浏览器运行时,包括DOM API 、属性、事件处理等
2. 主要学习知识点
- DOM API 的封装与实现
- 如何操作属性
- 如何操作样式
- 如何操作事件
- 如何操作属性
3. DOM API 的封装与实现
Vue3的自定义渲染器
3.1 createRenderer
下面是Vue官网的原话
翻译过来的意思是:
创建自定义渲染器。通过提供特定于平台的节点创建和操作api,您可以利用Vue的核心运行时来针对非dom环境。
有了上面提供的方式,我们可以自己实现自己的渲染器方法了。
3.2 需要实现那些方法
Vue3 官网ts提供部分
function createRenderer<HostNode, HostElement>(
options: RendererOptions<HostNode, HostElement>
): Renderer<HostElement>
interface Renderer<HostElement> {
render: RootRenderFunction<HostElement>
createApp: CreateAppFunction<HostElement>
}
interface RendererOptions<HostNode, HostElement> {
patchProp(
el: HostElement,
key: string,
prevValue: any,
nextValue: any,
// the rest is unused for most custom renderers
isSVG?: boolean,
prevChildren?: VNode<HostNode, HostElement>[],
parentComponent?: ComponentInternalInstance | null,
parentSuspense?: SuspenseBoundary | null,
unmountChildren?: UnmountChildrenFn
): void,
insert(
el: HostNode,
parent: HostElement,
anchor?: HostNode | null
): void,
remove(el: HostNode): void,
createElement(
type: string,
isSVG?: boolean,
isCustomizedBuiltIn?: string,
vnodeProps?: (VNodeProps & { [key: string]: any }) | null
): HostElement,
createText(text: string): HostNode,
createComment(text: string): HostNode,
setText(node: HostNode, text: string): void,
setElementText(node: HostElement, text: string): void,
parentNode(node: HostNode): HostElement | null,
nextSibling(node: HostNode): HostNode | null,
// optional, DOM-specific
querySelector?(selector: string): HostElement | null,
setScopeId?(el: HostElement, id: string): void,
cloneNode?(node: HostNode): HostNode,
insertStaticContent?(
content: string,
parent: HostElement,
anchor: HostNode | null,
isSVG: boolean
): [HostNode, HostNode]
}
RendererOptions 的接口类型就是我们需要实现的方法。总共有十四个API需要我们自己实现。
3.3 实现浏览器的运行时 —— runtime-dom
主要实现代码如下
export const nodeOps = {
// 增加 删除 修改 查询
insert(child, parent, anchor = null) {
parent.insertBefore(child, anchor); // insertBefore 可以等价于appendChild
},
remove(child) {
// 删除节点
const parentNode = child.parentNode;
if (parentNode) {
parentNode.removeChild(child);
}
},
setElementText(el, text) {
el.textContent = text;
},
setText(node, text) {
// document.createTextNode()
node.nodeValue = text;
},
querySelector(selector) {
return document.querySelector(selector);
},
parentNode(node) {
return node.parentNode;
},
nextSibling(node) {
return node.nextSibling;
},
createElement(tagName) {
return document.createElement(tagName);
},
createText(text) {
return document.createTextNode(text);
},
// 文本节点 , 元素中的内容
};
4. 如何操作属性
属性分为 class 类名操作,style 操作, 时间操作, 属性操作;
export const patchProp = (el, key, prevValue, nextValue) => {
if (key === 'class') {
patchClass(el, nextValue)
} else if (key === 'style') {
patchStyle(el, prevValue, nextValue);
} else if (/^on[^a-z]/.test(key)) {
patchEvent(el, key, prevValue , nextValue)
} else {
patchAttr(el, key, nextValue)
}
}
4.1 如何操作 class类型
通过removeAttribute和className属性进行操作
function patchClass(el, value) { // 根据最新值设置类名
if (value == null) {
el.removeAttribute('class');
} else {
el.className = value;
}
}
5. 如何操作样式
分为四种情况需要处理:
- 有老样式,没有新样式
- 有老样式,有新样式
- 没有老样式,有新样式
- 没有老样式,没有新样式
function patchStyle(
el: HTMLElement,
key: string,
prevValue: any,
nextValue: any
) {
const style = el.style;
if (!prevValue) { // 不存在老样式
if (nextValue) { // 有新样式,直接赋值
for (let key in nextValue) {
style[key] = nextValue[key];
}
}
} else {
// 存在老样式
if (!nextValue) {// 不存在新样式
for (let key in prevValue) { // 删除所有老样式
style[key] = null;
}
} else {
// 存在新样式
for (let key in prevValue) { // 删除老样式中,新样式不存在的值
if (!nextValue[key]) {
style[key] = null;
}
}
for (let key in nextValue) { // 复制新样式
el.style[key] = nextValue[key];
}
}
}
}
6. 如何操作事件
6.1 操作事件的注意事项
由于我们得到的值一直是新的函数值,所以无法删除;如下面的情况;
function patchEvent(el, rawName, prevValue, nextValue) {
const name = rawName.slice(2).toLowerCase(); // 转化事件是小写的
el.addEventListener(name,nextValue);
}
我们可以使用高阶函数进行封装下,然后就可以了,处理思路如下:
function createInvoker(initialValue) {
const invoker = (e) => invoker.value(e);
invoker.value = initialValue;
return invoker;
}
function patchEvent(el, rawName, nextValue) { // 更新事件
const invokers = el._vei || (el._vei = {});
const exisitingInvoker = invokers[rawName]; // 是否缓存过
if (nextValue && exisitingInvoker) {
exisitingInvoker.value = nextValue;
} else {
const name = rawName.slice(2).toLowerCase(); // 转化事件是小写的
if (nextValue) {// 缓存函数
const invoker = (invokers[rawName]) = createInvoker(nextValue);
el.addEventListener(name, invoker);
} else if (exisitingInvoker) {
el.removeEventListener(name, exisitingInvoker);
invokers[rawName] = undefined
}
}
}
通过自己createInvoker封装一个独立的函数,并把他存在dom中;这样子我们即可以实现删除原来事件,又支持修改事件。
7. 操作属性
通过removeAttribute和setAttribute属性进行操作
function patchAttr(el, key, value) { // 更新属性
if (value == null) {
el.removeAttribute(key);
} else {
el.setAttribute(key, value);
}
}
致谢
如果感觉我的文章有用,请关注下我的公众号: 前端小黄。 我将不定期更新我的原创文章。
本文章源码地址:github.com/hpstream/vu…;