前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

文章学习导航
面试总览

30 秒速记

  • 核心算法与宿主节点操作解耦
  • 宿主提供创建、插入、移除和属性更新能力
  • 调度器决定何时执行,渲染器决定怎样应用
  • 异步批处理不等于自动可中断
  • 引用与生命周期要在正确完成边界触发

自定义渲染器把怎样操作平台这件事抽出来,让界面协调逻辑能复用。 浏览器实现用节点接口,其他宿主可以提供自己的创建、插入和属性操作,只要满足渲染器约定。调度是另一层问题,它决定什么时候刷新;把工作延后一次不代表长更新就能中途暂停。设计时还要保证引用、生命周期和清理看到的是一致的已完成状态。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

在本章之前,我们花费了很大的篇幅全面的讲解了一个普通渲染器的实现原理,它可以将 Virtual DOM 渲染为 Web 平台的真实 DOM。本章我们将在上一章的基础上讲解更加高级的渲染器:自定义渲染器(Custom renderer)以及异步渲染。

# 自定义渲染器的原理

渲染器是围绕 Virtual DOM 而存在的,在 Web 平台下它能够把 Virtual DOM 渲染为浏览器中的真实 DOM 对象,通过前面几章的讲解,相信你已经能够认识到渲染器的实现原理,为了能够将 Virtual DOM 渲染为真实 DOM,渲染器内部需要调用浏览器提供的 DOM 编程接口,下面罗列了在出上一章中我们曾经使用到的那些浏览器为我们提供的 DOM 编程接口:

  • document.createElement / createElementNS:创建标签元素。
  • document.createTextNode:创建文本元素。
  • el.nodeValue:修改文本元素的内容。
  • el.removeChild:移除 DOM 元素。
  • el.insertBefore:插入 DOM 元素。
  • el.appendChild:追加 DOM 元素。
  • el.parentNode:获取父元素。
  • el.nextSibling:获取下一个兄弟元素。
  • document.querySelector:挂载 Portal 类型的 VNode 时,用它查找挂载点。

这些 DOM 编程接口完成了 Web 平台(或者说浏览器)下对 DOM 的增加、删除、查找的工作,它是 Web 平台独有的,所以如果渲染器自身强依赖于这些方法(函数),那么这个渲染器也只能够运行在浏览器中,它不具备跨平台的能力。换句话说,如果想要实现一个平台无关的渲染器,那么渲染器自身必须不能强依赖于任何一个平台下特有的接口,而是应该提供一个抽象层,将 “DOM” 的增加、删除、查找等操作使用抽象接口实现,具体到某个平台下时,由开发者决定如何使用该平台下的接口实现这个抽象层,这就是自定义渲染器的本质。

TIP

在下文中,我们将使用 “元素” 一词指代所有平台中的元素对象,例如在 Web 平台下 “元素” 一词指的就是 DOM 元素。

渲染器除了负责对元素的增加、删除、查找之外,它还负责修改某个特定元素自身的属性/特性,例如 Web 平台中元素具有 id、href 等属性/特性。在上一章中,我们使用 patchData 函数来完成元素自身属性/特性的更新,如下代码用于修改一个元素的类名列表(class):

// patchData.js
case 'class':
  el.className = nextValue
  break
@前端进阶之旅: 代码已经复制到剪贴板

这段代码同样也只能运行在浏览器中,为了渲染器能够跨平台,那么修改一个元素自身的属性/特性的工作也应该作为可自定义的一部分才行,因此,一个跨平台的渲染器应该至少包含两个可自定义的部分:可自定义元素的增加、删除、查找等操作、可自定义元素自身属性/特性的修改操作。这样对于任何一个元素来说,它的增删改查都已经变成了可自定义的部分,我们只需要“告知”渲染器在对元素进行增删改查时应该做哪些具体的操作即可。

接下来我们就着手将一个普通渲染器修改为拥有自定义能力的渲染器,在之前的讲解中,我们将渲染器的代码存放在了 render.js 文件中,如下是整个 render.js 文件的核心代码:

// 导出渲染器
export default function render(vnode, container) { /* ... */ }

// ========== 挂载 ==========

function mount(vnode, container, isSVG, refNode) { /* ... */ }

function mountElement(vnode, container, isSVG, refNode) { /* ... */ }

function mountText(vnode, container) { /* ... */ }

function mountFragment(vnode, container, isSVG) { /* ... */ }

function mountPortal(vnode, container) { /* ... */ }

function mountComponent(vnode, container, isSVG) { /* ... */ }

function mountStatefulComponent(vnode, container, isSVG) { /* ... */ }

function mountFunctionalComponent(vnode, container, isSVG) { /* ... */ }

// ========== patch ==========

function patch(prevVNode, nextVNode, container) { /* ... */ }

function replaceVNode(prevVNode, nextVNode, container) { /* ... */ }

function patchElement(prevVNode, nextVNode, container) { /* ... */ }

function patchChildren(
  prevChildFlags,
  nextChildFlags,
  prevChildren,
  nextChildren,
  container
) { /* ... */ }

function patchText(prevVNode, nextVNode) { /* ... */ }

function patchFragment(prevVNode, nextVNode, container) { /* ... */ }

function patchPortal(prevVNode, nextVNode) { /* ... */ }

function patchComponent(prevVNode, nextVNode, container) { /* ... */ }

// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function lis(arr) { /* ... */ }
@前端进阶之旅: 代码已经复制到剪贴板

观察如上代码结构,可以发现一个渲染器由两部分组成:mount 和 patch。在 mount 和 patch 中都会调用浏览器提供的 DOM 编程接口来完成真正的渲染工作。为了将浏览器提供的 DOM 编程接口与渲染器的代码分离,我们可以将如上代码封装到一个叫做 createRenderer 的函数中,如下代码所示:

export default function createRenderer(options) {
  function render(vnode, container) { /* ... */ }

  // ========== 挂载 ==========

  function mount(vnode, container, isSVG, refNode) { /* ... */ }

  function mountElement(vnode, container, isSVG, refNode) { /* ... */ }

  function mountText(vnode, container) { /* ... */ }

  function mountFragment(vnode, container, isSVG) { /* ... */ }

  function mountPortal(vnode, container) { /* ... */ }

  function mountComponent(vnode, container, isSVG) { /* ... */ }

  function mountStatefulComponent(vnode, container, isSVG) { /* ... */ }

  function mountFunctionalComponent(vnode, container, isSVG) { /* ... */ }

  // ========== patch ==========

  function patch(prevVNode, nextVNode, container) { 
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
      • 自定义渲染器的原理
      • 自定义渲染器的应用
      • References
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

Vue自定义渲染器和异步渲染|原理篇

文章学习导航
面试总览

30 秒速记

  • 核心算法与宿主节点操作解耦
  • 宿主提供创建、插入、移除和属性更新能力
  • 调度器决定何时执行,渲染器决定怎样应用
  • 异步批处理不等于自动可中断
  • 引用与生命周期要在正确完成边界触发

自定义渲染器把怎样操作平台这件事抽出来,让界面协调逻辑能复用。 浏览器实现用节点接口,其他宿主可以提供自己的创建、插入和属性操作,只要满足渲染器约定。调度是另一层问题,它决定什么时候刷新;把工作延后一次不代表长更新就能中途暂停。设计时还要保证引用、生命周期和清理看到的是一致的已完成状态。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

在本章之前,我们花费了很大的篇幅全面的讲解了一个普通渲染器的实现原理,它可以将 Virtual DOM 渲染为 Web 平台的真实 DOM。本章我们将在上一章的基础上讲解更加高级的渲染器:自定义渲染器(Custom renderer)以及异步渲染。

# 自定义渲染器的原理

渲染器是围绕 Virtual DOM 而存在的,在 Web 平台下它能够把 Virtual DOM 渲染为浏览器中的真实 DOM 对象,通过前面几章的讲解,相信你已经能够认识到渲染器的实现原理,为了能够将 Virtual DOM 渲染为真实 DOM,渲染器内部需要调用浏览器提供的 DOM 编程接口,下面罗列了在出上一章中我们曾经使用到的那些浏览器为我们提供的 DOM 编程接口:

  • document.createElement / createElementNS:创建标签元素。
  • document.createTextNode:创建文本元素。
  • el.nodeValue:修改文本元素的内容。
  • el.removeChild:移除 DOM 元素。
  • el.insertBefore:插入 DOM 元素。
  • el.appendChild:追加 DOM 元素。
  • el.parentNode:获取父元素。
  • el.nextSibling:获取下一个兄弟元素。
  • document.querySelector:挂载 Portal 类型的 VNode 时,用它查找挂载点。

这些 DOM 编程接口完成了 Web 平台(或者说浏览器)下对 DOM 的增加、删除、查找的工作,它是 Web 平台独有的,所以如果渲染器自身强依赖于这些方法(函数),那么这个渲染器也只能够运行在浏览器中,它不具备跨平台的能力。换句话说,如果想要实现一个平台无关的渲染器,那么渲染器自身必须不能强依赖于任何一个平台下特有的接口,而是应该提供一个抽象层,将 “DOM” 的增加、删除、查找等操作使用抽象接口实现,具体到某个平台下时,由开发者决定如何使用该平台下的接口实现这个抽象层,这就是自定义渲染器的本质。

TIP

在下文中,我们将使用 “元素” 一词指代所有平台中的元素对象,例如在 Web 平台下 “元素” 一词指的就是 DOM 元素。

渲染器除了负责对元素的增加、删除、查找之外,它还负责修改某个特定元素自身的属性/特性,例如 Web 平台中元素具有 id、href 等属性/特性。在上一章中,我们使用 patchData 函数来完成元素自身属性/特性的更新,如下代码用于修改一个元素的类名列表(class):

// patchData.js
case 'class':
  el.className = nextValue
  break
@前端进阶之旅: 代码已经复制到剪贴板

这段代码同样也只能运行在浏览器中,为了渲染器能够跨平台,那么修改一个元素自身的属性/特性的工作也应该作为可自定义的一部分才行,因此,一个跨平台的渲染器应该至少包含两个可自定义的部分:可自定义元素的增加、删除、查找等操作、可自定义元素自身属性/特性的修改操作。这样对于任何一个元素来说,它的增删改查都已经变成了可自定义的部分,我们只需要“告知”渲染器在对元素进行增删改查时应该做哪些具体的操作即可。

接下来我们就着手将一个普通渲染器修改为拥有自定义能力的渲染器,在之前的讲解中,我们将渲染器的代码存放在了 render.js 文件中,如下是整个 render.js 文件的核心代码:

// 导出渲染器
export default function render(vnode, container) { /* ... */ }

// ========== 挂载 ==========

function mount(vnode, container, isSVG, refNode) { /* ... */ }

function mountElement(vnode, container, isSVG, refNode) { /* ... */ }

function mountText(vnode, container) { /* ... */ }

function mountFragment(vnode, container, isSVG) { /* ... */ }

function mountPortal(vnode, container) { /* ... */ }

function mountComponent(vnode, container, isSVG) { /* ... */ }

function mountStatefulComponent(vnode, container, isSVG) { /* ... */ }

function mountFunctionalComponent(vnode, container, isSVG) { /* ... */ }

// ========== patch ==========

function patch(prevVNode, nextVNode, container) { /* ... */ }

function replaceVNode(prevVNode, nextVNode, container) { /* ... */ }

function patchElement(prevVNode, nextVNode, container) { /* ... */ }

function patchChildren(
  prevChildFlags,
  nextChildFlags,
  prevChildren,
  nextChildren,
  container
) { /* ... */ }

function patchText(prevVNode, nextVNode) { /* ... */ }

function patchFragment(prevVNode, nextVNode, container) { /* ... */ }

function patchPortal(prevVNode, nextVNode) { /* ... */ }

function patchComponent(prevVNode, nextVNode, container) { /* ... */ }

// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function lis(arr) { /* ... */ }
@前端进阶之旅: 代码已经复制到剪贴板

观察如上代码结构,可以发现一个渲染器由两部分组成:mount 和 patch。在 mount 和 patch 中都会调用浏览器提供的 DOM 编程接口来完成真正的渲染工作。为了将浏览器提供的 DOM 编程接口与渲染器的代码分离,我们可以将如上代码封装到一个叫做 createRenderer 的函数中,如下代码所示:

export default function createRenderer(options) {
  function render(vnode, container) { /* ... */ }

  // ========== 挂载 ==========

  function mount(vnode, container, isSVG, refNode) { /* ... */ }

  function mountElement(vnode, container, isSVG, refNode) { /* ... */ }

  function mountText(vnode, container) { /* ... */ }

  function mountFragment(vnode, container, isSVG) { /* ... */ }

  function mountPortal(vnode, container) { /* ... */ }

  function mountComponent(vnode, container, isSVG) { /* ... */ }

  function mountStatefulComponent(vnode, container, isSVG) { /* ... */ }

  function mountFunctionalComponent(vnode, container, isSVG) { /* ... */ }

  // ========== patch ==========

  function patch(prevVNode, nextVNode, container) { /* ... */ }

  function replaceVNode(prevVNode, nextVNode, container) { /* ... */ }

  function patchElement(prevVNode, nextVNode, container) { /* ... */ }

  function patchChildren(
    prevChildFlags,
    nextChildFlags,
    prevChildren,
    nextChildren,
    container
  ) { /* ... */ }

  function patchText(prevVNode, nextVNode) { /* ... */ }

  function patchFragment(prevVNode, nextVNode, container) { /* ... */ }

  function patchPortal(prevVNode, nextVNode) { /* ... */ }

  function patchComponent(prevVNode, nextVNode, container) { /* ... */ }

  return { render }
}

// https://en.wikipedia.org/wiki/Longest_increasing_subsequence
function lis(arr) { /* ... */ }
@前端进阶之旅: 代码已经复制到剪贴板

createRenderer 函数的返回值就是之前的 render 函数,也就是说调用 createRenderer 函数可以创建一个渲染器。createRenderer 函数接收一个参数 options,该参数的作用是为了允许外界有能力将操作元素的具体实现以选项的方式传递进来。

那么 options 参数中应该包含哪些选项呢?其实前面我们已经分析过了,只要是需要自定义的部分就应该作为选项传递进来,所以参数 options 中至少要包含两部分:一部分是元素的增加、删除、查找;另外一部分是元素的修改,即 patchData 函数。如下代码所示:

← 辅助创建 VNode 的 h 函数渲染器之挂载 →

fe