《Vue.js设计与实现》第七章-渲染器的设计

70 阅读3分钟

7-1.渲染器与响应系统的结合

渲染器不仅能够渲染真实的DOM元素,它还是框架跨平台能力的关键。 我们暂时将渲染器限定在DOM平台。

function renderer(domString, container){
  // 代码自己写
}

我们可以这样使用它:

renderer('<h1>Hello</h1>', documents.getElementById('app'))

我们将使用@vue/reactivity包提供的effectref。定义一个响应式数据count,它是一个ref,在副作用函数内调用renderer函数执行渲染。

// 代码自己写

7-2.渲染器的基本概念

  • render表示什么?
  • reanderer表示什么?
  • vnode表示什么?
  • mount表示什么?
  • container表示什么?

渲染器和渲染是相同的吗?

首先,当调用createRenderer函数创建渲染器时,渲染器不仅包含render函数,还包含hydrate函数。

function createRenderer(){
 // 请完成代码
}

有了渲染器,我们就可以用它来执行渲染任务了。

const renderer = createRenderer()
// 首次渲染
renderer.render(vnode, document.getElementById('app'))

除了首次渲染,还要执行更新动作。

const render = createRenderer()
// 首次渲染
renderer.render(oldVNode, document.getElementById('app'))
// 第二次渲染
renderer.render(newVNode, document.getElementById('app'))

首次渲染时把oldValue渲染到containder内,第二次渲染会将newValue和上次渲染的oldValue进行比较,试图找到并更新变更点。这个过程叫"打补丁"(或更新patch)。

function render(vnode, container){
  // 如果新vnode存在,将其与旧vnode一起传递给patch函数,进行打补丁
  
  // 如过旧vnode存在,且新vnode不存在,说明是卸载操作
  // 只需要将container内的DOM清空
  
  // 最后,把vnode存储到container._vnode下
}

假如我们连续三次调用renderer.render函数来执行渲染:

const render = createRenderer()
 // 首次渲染
 renderer.render(vnode1, document.getElementById('app'))
 // 第二次渲染
 renderer.render(vnode2, document.getElementById('app'))
 // 第三次渲染
 renderer.render(null, documents.getElementById('app'))
  1. 首次渲染: ...
  2. 第二次渲染: ...
  3. 第三次渲染: ...

接着来观察patch方法(此处不详细展开,后续跟进)

patch(n1, n2, container)
  1. 第一个参数n1: 旧vnode。
  2. 第二个参数n2: 新vnode。
  3. 第三个参数container: 容器。

// 暂时不需要写代码

7-3.自定义渲染器

什么是渲染器的跨平台能力?

vnode对象:

const vnode = {
  type: "h1",
  children: "hello",
};

对于这样一个vnode,我们可以使用render函数渲染它:

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

为了完成渲染工作,我们需要补充patch函数:

function patch(n1, n2, container) {
    // 如果n1不存在,意味着挂载,则调用mountElement函数完成挂载
    
    // 如果n1存在,意味着打补丁,暂时省略
}

function mountElement(vnode, container){
  // 创建DOM元素
  
  // 处理子节点,如果子节点是字符串,代表元素具有文本节点
  // 因此只需要设置元素的textContent属性即可
  
  // 将元素添加到容器
}

挂载一个普通标签元素的工作已经完成,接着我们会发现一个大问题。mountElement函数中调用了大量依赖于浏览器的API,例如createElement, appendChild等。我们如何将这些API抽离呢?

我们可以将这些操作DOM的API作为配置项。在创建renderer时传入配置项。

const renderer = createRenderer({
  createElement(tag) {
    return document.createElement(tag);
  },
  setElementText(el, text) {
    el.textContent = text;
  },
  insert(el, parent, anchor = null) {
    parent.inserBefore(el, anchor);
  },
});

重构一下patch方法。

耶真棒!

dcc7f01f009c8724623de043e6d27d9c.jpg