4. 实现vue3源码——runtime-dom

219 阅读3分钟

1. runtime-dom 的作用

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

2. 主要学习知识点

  1. DOM API 的封装与实现
  2. 如何操作属性
  3. 如何操作样式
  4. 如何操作事件
  5. 如何操作属性

3. DOM API 的封装与实现

Vue3的自定义渲染器

3.1 createRenderer

下面是Vue官网的原话 image.png 翻译过来的意思是: 创建自定义渲染器。通过提供特定于平台的节点创建和操作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. 如何操作样式

分为四种情况需要处理:

  1. 有老样式,没有新样式
  2. 有老样式,有新样式
  3. 没有老样式,有新样式
  4. 没有老样式,没有新样式
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…