前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 官方说明 制定迁移与安全策略。

TIP

本章主要讲解渲染器将各种类型的 VNode 挂载为真实 DOM 的原理,阅读本章内容你将对 Fragment 和 Portal 有更加深入的理解,同时渲染器对有状态组件和函数式组件的挂载实际上也透露了有状态组件和函数式组件的实现原理,这都会包含在本章的内容之中。另外本章的代码将使用上一章所编写的 h 函数,所以请确保你已经阅读了上一章的内容。

# 责任重大的渲染器

所谓渲染器,简单的说就是将 Virtual DOM 渲染成特定平台下真实 DOM 的工具(就是一个函数,通常叫 render),渲染器的工作流程分为两个阶段:mount 和 patch,如果旧的 VNode 存在,则会使用新的 VNode 与旧的 VNode 进行对比,试图以最小的资源开销完成 DOM 的更新,这个过程就叫 patch,或“打补丁”。如果旧的 VNode 不存在,则直接将新的 VNode 挂载成全新的 DOM,这个过程叫做 mount。

通常渲染器接收两个参数,第一个参数是将要被渲染的 VNode 对象,第二个参数是一个用来承载内容的容器(container),通常也叫挂载点,如下代码所示:

function render(vnode, container) {
  const prevVNode = container.vnode
  if (prevVNode == null) {
    if (vnode) {
      // 没有旧的 VNode,只有新的 VNode。使用 `mount` 函数挂载全新的 VNode
      mount(vnode, container)
      // 将新的 VNode 添加到 container.vnode 属性下,这样下一次渲染时旧的 VNode 就存在了
      container.vnode = vnode
    }
  } else {
    if (vnode) {
      // 有旧的 VNode,也有新的 VNode。则调用 `patch` 函数打补丁
      patch(prevVNode, vnode, container)
      // 更新 container.vnode
      container.vnode = vnode
    } else {
      // 有旧的 VNode 但是没有新的 VNode,这说明应该移除 DOM,在浏览器中可以使用 removeChild 函数。
      container.removeChild(prevVNode.el)
      container.vnode = null
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

整体思路非常简单,如果旧的 VNode 不存在且新的 VNode 存在,那就直接挂载(mount)新的 VNode ;如果旧的 VNode 存在且新的 VNode 不存在,那就直接将 DOM 移除;如果新旧 VNode 都存在,那就打补丁(patch):

旧 VNode 新 VNode 操作
❌ ✅ 调用 mount 函数
✅ ❌ 移除 DOM
✅ ✅ 调用 patch 函数

之所以说渲染器的责任非常之大,是因为它不仅仅是一个把 VNode 渲染成真实 DOM 的工具,它还负责以下工作:

  • 控制部分组件生命周期钩子的调用

在整个渲染周期中包含了大量的 DOM 操作、组件的挂载、卸载,控制着组件的生命周期钩子调用的时机。

  • 多端渲染的桥梁

渲染器也是多端渲染的桥梁,自定义渲染器的本质就是把特定平台操作“DOM”的方法从核心算法中抽离,并提供可配置的方案。

  • 与异步渲染有直接关系

Vue3 的异步渲染是基于调度器的实现,若要实现异步渲染,组件的挂载就不能同步进行,DOM的变更就要在合适的时机,一些需要在真实DOM存在之后才能执行的操作(如 ref)也应该在合适的时机进行。对于时机的控制是由调度器来完成的,但类似于组件的挂载与卸载以及操作 DOM 等行为的入队还是由渲染器来完成的,这也是为什么 Vue2 无法轻易实现异步渲染的原因。

  • 包含最核心的 Diff 算法

Diff 算法是渲染器的核心特性之一,可以说正是 Diff 算法的存在才使得 Virtual DOM 如此成功。

# 挂载普通标签元素

# 基本原理

渲染器的责任重大,所以它做的事情也非常多,一口吃成胖子是不太现实的,我们需要一点点地消化。

在初次调用渲染器渲染某个 VNode 时:

const vnode = {/*...*/}
render(vnode, container)
@前端进阶之旅: 代码已经复制到剪贴板

由于没有旧的 VNode 存在,所以会调用 mount 函数挂载全新的 VNode ,这个小节我们就探讨一下渲染器的 mount 函数是如何把 VNode 渲染成真实 DOM 的,以及其中一些核心的关键点。

mount 函数的作用是把一个 VNode 渲染成真实 DOM,根据不同类型的 VNode 需要采用不同的挂载方式,如下:

function mount(vnode, container) {
  const { flags } = vnode
  if (flags & VNodeFlags.ELEMENT) {
    // 挂载普通标签
    mountElement(vnode, container)
  } else if (flags & VNodeFlags.COMPONENT) {
    // 挂载组件
    mountComponent(vnode, container)
  } else if (flags & VNodeFlags.TEXT) {
    // 挂载纯文本
    mountText(vnode, container)
  } else if (flags & VNodeFlags.FRAGMENT) {
    // 挂载 Fragment
    mountFragment(vnode, container)
  } else if (flags & VNodeFlags.PORTAL) {
    // 挂载 Portal
    mountPortal(vnode, container)
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

我们根据 VNode 的 flags 属性值能够区分一个 VNode 对象的类型,不同类型的 VNode 采用不同的挂载函数:

我们首先来讨论一下 mountElement 函数,它用于挂载普通标签元素。我们在"组件的本质"一章中曾经编写过如下这段代码:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  container.appendChild(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

这是一个极简的用于挂载普通标签元素的 mountElement 函数,它会调用浏览器提供的 document.createElement 函数创建元素,接着调用 appendChild 函数将元素添加到 container 中,但它具有以下缺陷:

  • 1、VNode 被渲染为真实DOM之后,没有引用真实DOM元素
  • 2、没有将 VNodeData 应用到真实DOM元素上
  • 3、没有继续挂载子节点,即 children
  • 4、不能严谨地处理 SVG 标签

针对这四个问题,我们逐个去解决。先来看第一个问题:VNode 被渲染为真实DOM之后,没有引用真实DOM元素,这个问题很好解决,只需要添加一行代码即可:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  vnode.el = el
  container.appendChild(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

再来看第二个问题:没有将 VNodeData 应用到元素上,我们知道 VNodeData 作为 VNode 的描述,对于标签元素来说它包含了元素的样式、事件等诸多信息,我们需要将这些信息应用到新创建的真实DOM元素上,假设我们有如下 VNode:

TIP

再次强调,本章使用上一章节中所编写的 h 函数。

const elementVnode = h(
  'div',
  {
    style: {
      height: '100px',
      width: '100px',
      background: 'red'
    }
  }
)
@前端进阶之旅: 代码已经复制到剪贴板

我们使用 h 函数创建了一个描述 div 标签的 VNode 对象,观察 VNodeData 可以发现,它拥有一些内联样式,所以在 mountElement 函数内,我们需要将这些内联样式应用到元素上,我们给 mountElement 增加如下代码:

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

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
      • 责任重大的渲染器
      • 挂载普通标签元素
        • 基本原理
        • class的处理
        • Attributes 和 DOM Properties
        • 事件的处理
      • 挂载纯文本、Fragment 和 Portal
      • 有状态组件的挂载和原理
      • 函数式组件的挂载和原理
    • 渲染器的核心 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 官方说明 制定迁移与安全策略。

TIP

本章主要讲解渲染器将各种类型的 VNode 挂载为真实 DOM 的原理,阅读本章内容你将对 Fragment 和 Portal 有更加深入的理解,同时渲染器对有状态组件和函数式组件的挂载实际上也透露了有状态组件和函数式组件的实现原理,这都会包含在本章的内容之中。另外本章的代码将使用上一章所编写的 h 函数,所以请确保你已经阅读了上一章的内容。

# 责任重大的渲染器

所谓渲染器,简单的说就是将 Virtual DOM 渲染成特定平台下真实 DOM 的工具(就是一个函数,通常叫 render),渲染器的工作流程分为两个阶段:mount 和 patch,如果旧的 VNode 存在,则会使用新的 VNode 与旧的 VNode 进行对比,试图以最小的资源开销完成 DOM 的更新,这个过程就叫 patch,或“打补丁”。如果旧的 VNode 不存在,则直接将新的 VNode 挂载成全新的 DOM,这个过程叫做 mount。

通常渲染器接收两个参数,第一个参数是将要被渲染的 VNode 对象,第二个参数是一个用来承载内容的容器(container),通常也叫挂载点,如下代码所示:

function render(vnode, container) {
  const prevVNode = container.vnode
  if (prevVNode == null) {
    if (vnode) {
      // 没有旧的 VNode,只有新的 VNode。使用 `mount` 函数挂载全新的 VNode
      mount(vnode, container)
      // 将新的 VNode 添加到 container.vnode 属性下,这样下一次渲染时旧的 VNode 就存在了
      container.vnode = vnode
    }
  } else {
    if (vnode) {
      // 有旧的 VNode,也有新的 VNode。则调用 `patch` 函数打补丁
      patch(prevVNode, vnode, container)
      // 更新 container.vnode
      container.vnode = vnode
    } else {
      // 有旧的 VNode 但是没有新的 VNode,这说明应该移除 DOM,在浏览器中可以使用 removeChild 函数。
      container.removeChild(prevVNode.el)
      container.vnode = null
    }
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

整体思路非常简单,如果旧的 VNode 不存在且新的 VNode 存在,那就直接挂载(mount)新的 VNode ;如果旧的 VNode 存在且新的 VNode 不存在,那就直接将 DOM 移除;如果新旧 VNode 都存在,那就打补丁(patch):

旧 VNode 新 VNode 操作
❌ ✅ 调用 mount 函数
✅ ❌ 移除 DOM
✅ ✅ 调用 patch 函数

之所以说渲染器的责任非常之大,是因为它不仅仅是一个把 VNode 渲染成真实 DOM 的工具,它还负责以下工作:

  • 控制部分组件生命周期钩子的调用

在整个渲染周期中包含了大量的 DOM 操作、组件的挂载、卸载,控制着组件的生命周期钩子调用的时机。

  • 多端渲染的桥梁

渲染器也是多端渲染的桥梁,自定义渲染器的本质就是把特定平台操作“DOM”的方法从核心算法中抽离,并提供可配置的方案。

  • 与异步渲染有直接关系

Vue3 的异步渲染是基于调度器的实现,若要实现异步渲染,组件的挂载就不能同步进行,DOM的变更就要在合适的时机,一些需要在真实DOM存在之后才能执行的操作(如 ref)也应该在合适的时机进行。对于时机的控制是由调度器来完成的,但类似于组件的挂载与卸载以及操作 DOM 等行为的入队还是由渲染器来完成的,这也是为什么 Vue2 无法轻易实现异步渲染的原因。

  • 包含最核心的 Diff 算法

Diff 算法是渲染器的核心特性之一,可以说正是 Diff 算法的存在才使得 Virtual DOM 如此成功。

# 挂载普通标签元素

# 基本原理

渲染器的责任重大,所以它做的事情也非常多,一口吃成胖子是不太现实的,我们需要一点点地消化。

在初次调用渲染器渲染某个 VNode 时:

const vnode = {/*...*/}
render(vnode, container)
@前端进阶之旅: 代码已经复制到剪贴板

由于没有旧的 VNode 存在,所以会调用 mount 函数挂载全新的 VNode ,这个小节我们就探讨一下渲染器的 mount 函数是如何把 VNode 渲染成真实 DOM 的,以及其中一些核心的关键点。

mount 函数的作用是把一个 VNode 渲染成真实 DOM,根据不同类型的 VNode 需要采用不同的挂载方式,如下:

function mount(vnode, container) {
  const { flags } = vnode
  if (flags & VNodeFlags.ELEMENT) {
    // 挂载普通标签
    mountElement(vnode, container)
  } else if (flags & VNodeFlags.COMPONENT) {
    // 挂载组件
    mountComponent(vnode, container)
  } else if (flags & VNodeFlags.TEXT) {
    // 挂载纯文本
    mountText(vnode, container)
  } else if (flags & VNodeFlags.FRAGMENT) {
    // 挂载 Fragment
    mountFragment(vnode, container)
  } else if (flags & VNodeFlags.PORTAL) {
    // 挂载 Portal
    mountPortal(vnode, container)
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

我们根据 VNode 的 flags 属性值能够区分一个 VNode 对象的类型,不同类型的 VNode 采用不同的挂载函数:

我们首先来讨论一下 mountElement 函数,它用于挂载普通标签元素。我们在"组件的本质"一章中曾经编写过如下这段代码:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  container.appendChild(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

这是一个极简的用于挂载普通标签元素的 mountElement 函数,它会调用浏览器提供的 document.createElement 函数创建元素,接着调用 appendChild 函数将元素添加到 container 中,但它具有以下缺陷:

  • 1、VNode 被渲染为真实DOM之后,没有引用真实DOM元素
  • 2、没有将 VNodeData 应用到真实DOM元素上
  • 3、没有继续挂载子节点,即 children
  • 4、不能严谨地处理 SVG 标签

针对这四个问题,我们逐个去解决。先来看第一个问题:VNode 被渲染为真实DOM之后,没有引用真实DOM元素,这个问题很好解决,只需要添加一行代码即可:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)
  vnode.el = el
  container.appendChild(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

再来看第二个问题:没有将 VNodeData 应用到元素上,我们知道 VNodeData 作为 VNode 的描述,对于标签元素来说它包含了元素的样式、事件等诸多信息,我们需要将这些信息应用到新创建的真实DOM元素上,假设我们有如下 VNode:

TIP

再次强调,本章使用上一章节中所编写的 h 函数。

const elementVnode = h(
  'div',
  {
    style: {
      height: '100px',
      width: '100px',
      background: 'red'
    }
  }
)
@前端进阶之旅: 代码已经复制到剪贴板

我们使用 h 函数创建了一个描述 div 标签的 VNode 对象,观察 VNodeData 可以发现,它拥有一些内联样式,所以在 mountElement 函数内,我们需要将这些内联样式应用到元素上,我们给 mountElement 增加如下代码:

function mountElement(vnode, container) {
  const el = document.createElement(vnode.tag)

  // 拿到 VNodeData
  const data = vnode.data
  if (data) {
    // 如果 VNodeData 存在,则遍历之
    for(let key in data) {
      // key 可能是 class、style、on 等等
      switch(key) {
        case 'style':
          // 如果 key 的值是 style,说明是内联样式,逐个将样式规则应用到 el
          for(let k in data.style) {
            el.style[k] = data.style[k]
          }
        break
      }
    }
  }

  container.appendChild(el)
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,在创建真实DOM之后,我们需要检查 VNodeData 是否存在,如果 VNodeData 存在则遍历之。由于 VNodeData 中不仅仅包含内联样式的描述(即 style),还可能包含其他描述如 class、事件等等,所以我们使用 switch...case 语句对不同的 key 值做区分处理,以 style 为例,我们只需要将 data.style 中的样式规则应用到真实DOM即可。使用渲染器渲染 elementVNode 的效果如下:

对于 class 或事件或其他DOM属性都是类似的处理方式,为了不偏题我们放到后面统一讲解,接下来我们来看第三个问题:没有继续挂载子节点,即 children,我们知道 VNode 是有可能存在子节点的,现在的 mountElement 函数仅仅将该 VNode 本身所描述的DOM元素添加到了页面中,却没有理会其子节点,为了递归地挂载子节点,我们需要为 mountElement 函数增加如下代码:

← 自定义渲染器和异步渲染渲染器的核心 Diff 算法 →

fe