Skip to content

7. 渲染器的设计

7.1  渲染器与响应系统的结合

渲染器不仅能够渲染真实 DOM 元素,它还是框架跨平台能力的关键

js
// 最简单的渲染器
function renderer(domString, container) {
  container.innerHTML = domString;
}
js
// 结合副作用函数和响应式数据
const count = ref(1);

effect(() => {
  renderer(`<h1>${count.value}</h1>`, document.getElementById("app"));
});

count.value++;

配合 @vue/reactivity 包使用渲染器

html
<script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js"></script>
js
const { effect, ref } = VueReactivity;

function renderer(domString, container) {
  container.innerHTML = domString;
}

const count = ref(1);

effect(() => {
  renderer(`<h1>${count.value}</h1>`, document.getElementById("app"));
});

count.value++;

7.2  渲染器的基本概念

  • 通常使用英文 renderer 来表达“渲染器“,而 render 表示“渲染“,在浏览器平台上,渲染器会把虚拟 DOM 渲染为真实 DOM 元素。
  • 把虚拟 DOM 节点渲染为真实 DOM 节点的过程叫作挂载,通常用英文 mount 来表达
  • 渲染器通常需要接收一个挂载点(容器)作为参数,用来指定具体的挂载位置,通常用英文 container 来表达容器
  • 多次在同一个 container 上调用 renderer.render 函数进行渲染时,渲染器除了要执行挂载动作外,还要执行更新动作
js
function createRenderer() {
  function render(vnode, container) {
    // ...
  }

  // 渲染器与渲染是不同的。渲染器是更加宽泛的概念,它包含渲染。渲染器不仅可以用来渲染,
  //还可以用来激活已有的 DOM 元素。这个过程通常发生在同构渲染的情况下
  function hydrate(vnode, container) {
    // ...
  }

  return {
    render,
    hydrate,
  };
}
const renderer = createRenderer();
// 首次渲染
renderer.render(oldVNode, document.querySelector("#app"));
// 第二次渲染
renderer.render(newVNode, document.querySelector("#app"));
  • 挂载动作本身也可以看作一种特殊的打补丁,它的特殊之处在于旧的 vnode 是不存在的
js
function createRenderer() {
  function render(vnode, container) {
    if (vnode) {
      // 新 vnode 存在,将其与旧 vnode 一起传递给 patch 函数,进行打补丁
      patch(container._vnode, vnode, container);
    } else {
      if (container._vnode) {
        // 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
        // 只需要将 container 内的 DOM 清空即可
        container.innerHTML = "";
      }
    }
    // 把 vnode 存储到 container._vnode 下,即后续渲染中的旧 vnode
    container._vnode = vnode;
  }

  return {
    render,
  };
}

7.3  自定义渲染器

js
function createRenderer() {
  function mountElement(vnode, container) {
    // 创建 DOM 元素
    const el = document.createElement(vnode.type);
    // 处理子节点,如果子节点是字符串,代表元素具有文本节点
    if (typeof vnode.children === "string") {
      // 因此只需要设置元素的 textContent 属性即可
      el.textContent = vnode.children;
    }
    // 将元素添加到容器中
    container.appendChild(el);
  }

  function patch(n1, n2, container) {
    // 在这里编写渲染逻辑
    // 如果 n1 不存在,意味着挂载,则调用 mountElement 函数完成挂载
    if (!n1) {
      mountElement(n2, container);
    } else {
      // n1 存在,意味着打补丁,暂时省略
    }
  }

  function render(vnode, container) {
    if (vnode) {
      patch(container._vnode, vnode, container);
    } else {
      if (container._vnode) {
        container.innerHTML = "";
      }
    }
    container._vnode = vnode;
  }

  return {
    render,
  };
}

const vnode = {
  type: "h1",
  children: "hello",
};
// 创建一个渲染器
const renderer = createRenderer();
// 调用 render 函数渲染该 vnode
renderer.render(vnode, document.querySelector("#app"));

为了实现一个跨平台的通用渲染器,还需要将操作 DOM 的 API 作为配置项

js
function createRenderer(options) {
  // 通过 options 得到操作 DOM 的 API
  const { createElement, insert, setElementText } = options;

  // 在这个作用域内定义的函数都可以访问那些 API
  function mountElement(vnode, container) {
    // ...

    // 调用 createElement 函数创建元素
    const el = createElement(vnode.type);
    if (typeof vnode.children === "string") {
      // 调用 setElementText 设置元素的文本节点
      setElementText(el, vnode.children);
    }
    // 调用 insert 函数将元素插入到容器内
    insert(el, container);
  }

  function patch(n1, n2, container) {
    // ...
  }

  function render(vnode, container) {
    // ...
  }

  return {
    render,
  };
}

// 在创建 renderer 时传入配置项
const renderer = createRenderer({
  // 用于创建元素
  createElement(tag) {
    return document.createElement(tag);
  },

  // 用于设置元素的文本节点
  setElementText(el, text) {
    el.textContent = text;
  },

  // 用于在给定的 parent 下添加指定元素
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor);
  },
});

上次更新于: