Skip to content
On this page

创建 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"]
  }
}
pnpm install @vue/shared@workspace --filter @vue/runtime-dom

实现节点常用操作

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), // 搜索元素
};

比对属性方法

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);
  }
};

操作类名

function patchClass(el, value) {
  // 根据最新值设置类名
  if (value == null) {
    el.removeAttribute("class");
  } else {
    el.className = value;
  }
}

操作样式

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;
      }
    }
  }
}

操作事件

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;
    }
  }
}

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

操作属性

function patchAttr(el, key, value) {
  // 更新属性
  if (value == null) {
    el.removeAttribute(key);
  } else {
    el.setAttribute(key, value);
  }
}

创建渲染器

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

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

// 准备好所有渲染时所需要的的属性
const renderOptions = Object.assign({ patchProp }, nodeOps);
export function render(vnode, container) {
  return createRenderer(renderOptions).render(vnode, container);
}

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

Released under the MIT License.