创建runtime-dom包

runtime-dom 针对浏览器运行时,包括DOM API 、属性、事件处理等

runtime-dom/package.json

{
    "name": "@vue/runtime-dom",
    "main": "index.js",
    "module": "dist/runtime-dom.esm-bundler.js",
    "unpkg": "dist/runtime-dom.global.js",
    "buildOptions": {
        "name": "VueRuntimeDOM",
        "formats": [
        "esm-bundler",
        "cjs",
        "global"
        ]
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pnpm install @vue/shared@workspace --filter @vue/runtime-dom
1

实现节点常用操作

runtime-dom/src/nodeOps 这里存放常见DOM操作API,不同运行时提供的具体实现不一样,最终将操作方法传递到runtime-core中,所以runtime-core不需要关心平台相关代码~

export const nodeOps = {
    insert: (child, parent, anchor) => { // 添加节点
        parent.insertBefore(child, anchor || null);
    },
    remove: child => { // 节点删除
        const parent = child.parentNode;
        if (parent) {
            parent.removeChild(child);
        }
    },
    createElement: (tag) => document.createElement(tag),// 创建节点
    createText: text => document.createTextNode(text),// 创建文本
    setText: (node, text) => node.nodeValue = text, //  设置文本节点内容
    setElementText: (el, text) => el.textContent = text, // 设置文本元素中的内容
    parentNode: node => node.parentNode, // 父亲节点
    nextSibling: node => node.nextSibling, // 下一个节点
    querySelector: selector => document.querySelector(selector) // 搜索元素
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

比对属性方法

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, nextValue)
    } else {
        patchAttr(el, key, nextValue)
    }
}
1
2
3
4
5
6
7
8
9
10
11

操作类名

function patchClass(el, value) { // 根据最新值设置类名
    if (value == null) {
        el.removeAttribute('class');
    } else {
        el.className = value;
    }
}
1
2
3
4
5
6
7

操作样式

function patchStyle(el, prev, next) { // 更新style
    const style = el.style;
    for (const key in next) { // 用最新的直接覆盖
        style[key] = next[key]
    }
    if (prev) {
        for (const key in prev) {// 老的有新的没有删除
            if (next[key] == null) {
                style[key] = null
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

操作事件

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
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

在绑定事件的时候,绑定一个伪造的事件处理函数invoker,把真正的事件处理函数设置为invoker.value属性的值

操作属性

function patchAttr(el, key, value) { // 更新属性
    if (value == null) {
        el.removeAttribute(key);
    } else {
        el.setAttribute(key, value);
    }
}
1
2
3
4
5
6
7

创建渲染器

最终我们在 index.js中引入写好的方法,渲染选项就准备好了。 稍后将虚拟DOM转化成真实DOM会调用这些方法

import { nodeOps } from "./nodeOps"
import { patchProp } from "./patchProp"

// 准备好所有渲染时所需要的的属性
const renderOptions = Object.assign({patchProp},nodeOps);
createRenderer(renderOptions).render(
    h('h1','jw'),
    document.getElementById('app')
);
1
2
3
4
5
6
7
8
9

createRenderer接受渲染所需的方法,h方法为创建虚拟节点的方法。这两个方法和平台无关,所以我们将这两个方法在runtime-core中实现。