Vue3 runtime-dom

100 阅读2分钟

禁止转载,侵权必究!

Vue3 渲染器的作用

渲染器的作用是把虚拟 DOM 渲染为特定平台上的真实元素。在浏览器中,渲染器会把虚拟 DOM 渲染成真实 DOM 元素。

1.createRenderer.html script

import {
  reactive,
  effect,
  computed,
  watch,
  createRenderer,
  h,
} from "/node_modules/@vue/runtime-dom/dist/runtime-dom.esm-browser.js";
const renderer = createRenderer({
  createElement(element) {
    return document.createElement(element);
  },
  setElementText(el, text) {
    el.innerHTML = text;
  },
  insert(el, container) {
    container.appendChild(el);
  },
});
renderer.render(h("h1", "hello world"), document.getElementById("app"));

创建 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 不需要关心平台相关代码~

nodeOps.ts

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

比对属性方法

pathProps.ts

export const patchProp = (el, key, prevValue, nextValue) => {
  if (key === "class") {
    // class
    patchClass(el, nextValue);
  } else if (key === "style") {
    // {color:'red',background:red}   {color:'red'}
    // onXxxx  on1
    patchStyle(el, prevValue, nextValue);
  } else if (/^on[^a-z]/.test(key)) {
    // event ,vue 可以绑定多个 onclick1 onclick2
    // 事件操作? addEventListener  removeEventListener
    // @click="fn1"       @click="fn2" ?
    // invoker.fn = fn1   invoker.fn = fn2
    // @click="()=>fn1()"
    // @click="()=>invoker.fn"   @click="()=> invoker.fn" ?

    patchEvent(el, key, nextValue);
  } else {
    //attr
    patchAttr(el, key, nextValue);
  }
};

操作类名

./modules/class.ts

export const patchClass = (el, value) => {
  if (value == null) {
    // 如果没有类名 就是移除
    el.removeAttribute("class");
  } else {
    el.className = value;
  }
};

操作样式

./modules/style.ts

export const patchStyle = (el, prev, next) => {
  const style = el.style; // 稍后更新 el.style属性

  for (let key in next) {
    style[key] = next[key];
  }

  // 老的有新的没有要移除掉
  for (let key in prev) {
    if (next[key] == null) {
      // 老的有新的没有
      style[key] = null; // el[style][key] = 'xxx'
    }
  }
};

操作事件

./modules/event.ts

function createInvoker(initialValue) {
  const invoker = (e) => invoker.value(e);
  invoker.value = initialValue; // 后续更新的时候 只需要更新invoker的value属性
  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 属性的值

操作属性

./modules/attr.ts

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