前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
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 秒速记

  • 可复用先对身份与类型,不兼容就替换
  • 复用宿主节点后更新属性与孩子
  • 旧属性消失必须移除
  • 文本、空孩子、节点列表之间要覆盖转换
  • 事件更新避免旧监听残留与重复注册

patch 的任务是让已有界面与新描述一致,同时复用仍然合适的节点。 身份或类型不兼容就替换,能复用时保留宿主节点,再比较属性和孩子。更新不只是写新值,旧样式、属性和事件消失后也要撤销。孩子从文本变列表、从列表变空这些边界尤其容易遗漏,要逐种定义如何卸载和挂载,才能避免页面残留。

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

在上一章中我们讲解并实现了渲染器的挂载逻辑,本质上就是将各种类型的 VNode 渲染成真实DOM的过程。渲染器除了将全新的 VNode 挂载成真实DOM之外,它的另外一个职责是负责对新旧 VNode 进行比对,并以合适的方式更新DOM,也就是我们常说的 patch。本章内容除了让你了解基本的比对逻辑之外,还讲述了在新旧 VNode 比对的过程中应该遵守怎样的原则,让我们开始吧!

# 基本原则

通常重渲染(re-render)是由组件的更新开始的,因为在框架的使用层面开发者通过变更数据状态从而引起框架内部对UI的自动更新,但是组件的更新本质上还是对真实DOM的更新,或者说是对标签元素的更新,所以我们就优先来看一下如何更新一个标签元素。

我们首先回顾一下渲染器的代码,如下:

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

如上高亮的两句代码所示,当使用 render 渲染器渲染一个全新的 VNode 时,会调用 mount 函数挂载该 VNode,同时让容器元素存储对该 VNode 对象的引用,这样当再次调用渲染器渲染新的 VNode 对象到相同的容器元素时,由于旧的 VNode 已经存在,所以会调用 patch 函数以合适的方式进行更新,如下代码所示:

// 旧的 VNode
const prevVNode = h('div')

// 新的 VNode
const nextVNode = h('span')

// 第一次渲染 VNode 到 #app,此时会调用 mount 函数
render(prevVNode, document.getElementById('app'))

// 第二次渲染新的 VNode 到相同的 #app 元素,此时会调用 patch 函数
render(nextVNode, document.getElementById('app'))
@前端进阶之旅: 代码已经复制到剪贴板

patch 函数会对新旧 VNode 进行比对,也就是我们所说的 diff,那么不同的两个 VNode 之间应该遵守怎样的比对规则呢?其实这个问题很容易回答,我们知道 VNode 有类型之分,不同类型的 VNode 之间存在一定的差异,所以不同的 VNode 之间第一个比对原则就是:只有相同类型的 VNode 才有比对的意义,例如我们有两个 VNode,其中一个 VNode 的类型是标签元素,而另一个 VNode 的类型是组件,当这两个 VNode 进行比对时,最优的做法是使用新的 VNode 完全替换旧的 VNode,换句话说我们根本就没有做任何比对的操作,因为这完全没有意义,所以根据这个思想我们实现的 patch 函数如下:

function patch(prevVNode, nextVNode, container) {
  // 分别拿到新旧 VNode 的类型,即 flags
  const nextFlags = nextVNode.flags
  const prevFlags = prevVNode.flags

  // 检查新旧 VNode 的类型是否相同,如果类型不同,则直接调用 replaceVNode 函数替换 VNode
  // 如果新旧 VNode 的类型相同,则根据不同的类型调用不同的比对函数
  if (prevFlags !== nextFlags) {
    replaceVNode(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.ELEMENT) {
    patchElement(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.COMPONENT) {
    patchComponent(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.TEXT) {
    patchText(prevVNode, nextVNode)
  } else if (nextFlags & VNodeFlags.FRAGMENT) {
    patchFragment(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.PORTAL) {
    patchPortal(prevVNode, nextVNode)
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,既然 patch 函数的作用是用来比对新旧 VNode,那么 patch 函数必然需要接收新旧 VNode 作为参数,我们使用 prevVNode 形参代表旧的 VNode,使用 nextVNode 形参代表新的 VNode,如上是很清晰的一段比对逻辑,首先我们需要拿到新旧 VNode 的类型(flags),接着是一连串的 if...else if 语句,其核心原则是:如果类型不同,则直接调用 replaceVNode 函数使用新的 VNode 替换旧的 VNode,否则根据不同的类型调用与之相符的比对函数,如下图所示:

# 替换 VNode

我们首先来研究一下如何替换 VNode,即 replaceVNode 函数应该做什么,我们先来复现需要替换 VNode 的场景,如下代码所示:

// 旧的 VNode 是一个 div 标签
const prevVNode = h('div', null, '旧的 VNode')

class MyComponent {
  render () {
    return h('h1', null, '新的 VNode')
  }
}
// 新的 VNode 是一个组件
const nextVNode = h(MyComponent)

// 先后渲染新旧 VNode 到 #app
render(prevVNode, document.getElementById('app'))
render(nextVNode, document.getElementById('app'))
@前端进阶之旅: 代码已经复制到剪贴板

在如上代码中,我们先后渲染了新旧 VNode 到 #app 元素,由于新旧 VNode 具有不同的类型,所以此时会触发 VNode 的替换操作,替换操作并不复杂,本质就是把旧的 VNode 所渲染的DOM移除,再挂载新的 VNode,如下是 replaceVNode 函数的实现:

function replaceVNode(prevVNode, nextVNode, container) {
  // 将旧的 VNode 所渲染的 DOM 从容器中移除
  container.removeChild(prevVNode.el)
  // 再把新的 VNode 挂载到容器中
  mount(nextVNode, container)
}
@前端进阶之旅: 代码已经复制到剪贴板

TIP

完整代码&在线体验地址: https://codesandbox.io/s/jlxjk18vm5

看上去很简单,但实际上仅有这两行代码的话,是存在缺陷的。至于有何缺陷我们会在本章的后面讲解,因为目前我们的背景铺垫还不够。

# 更新标签元素

# 更新标签元素的基本原则

当新旧 VNode 的类型不同时,会调用

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

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

  • Webpack

  • Node

  • Javascript

  • 综合

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

Vue渲染器之patch|原理篇

文章学习导航
面试总览

30 秒速记

  • 可复用先对身份与类型,不兼容就替换
  • 复用宿主节点后更新属性与孩子
  • 旧属性消失必须移除
  • 文本、空孩子、节点列表之间要覆盖转换
  • 事件更新避免旧监听残留与重复注册

patch 的任务是让已有界面与新描述一致,同时复用仍然合适的节点。 身份或类型不兼容就替换,能复用时保留宿主节点,再比较属性和孩子。更新不只是写新值,旧样式、属性和事件消失后也要撤销。孩子从文本变列表、从列表变空这些边界尤其容易遗漏,要逐种定义如何卸载和挂载,才能避免页面残留。

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

在上一章中我们讲解并实现了渲染器的挂载逻辑,本质上就是将各种类型的 VNode 渲染成真实DOM的过程。渲染器除了将全新的 VNode 挂载成真实DOM之外,它的另外一个职责是负责对新旧 VNode 进行比对,并以合适的方式更新DOM,也就是我们常说的 patch。本章内容除了让你了解基本的比对逻辑之外,还讲述了在新旧 VNode 比对的过程中应该遵守怎样的原则,让我们开始吧!

# 基本原则

通常重渲染(re-render)是由组件的更新开始的,因为在框架的使用层面开发者通过变更数据状态从而引起框架内部对UI的自动更新,但是组件的更新本质上还是对真实DOM的更新,或者说是对标签元素的更新,所以我们就优先来看一下如何更新一个标签元素。

我们首先回顾一下渲染器的代码,如下:

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

如上高亮的两句代码所示,当使用 render 渲染器渲染一个全新的 VNode 时,会调用 mount 函数挂载该 VNode,同时让容器元素存储对该 VNode 对象的引用,这样当再次调用渲染器渲染新的 VNode 对象到相同的容器元素时,由于旧的 VNode 已经存在,所以会调用 patch 函数以合适的方式进行更新,如下代码所示:

// 旧的 VNode
const prevVNode = h('div')

// 新的 VNode
const nextVNode = h('span')

// 第一次渲染 VNode 到 #app,此时会调用 mount 函数
render(prevVNode, document.getElementById('app'))

// 第二次渲染新的 VNode 到相同的 #app 元素,此时会调用 patch 函数
render(nextVNode, document.getElementById('app'))
@前端进阶之旅: 代码已经复制到剪贴板

patch 函数会对新旧 VNode 进行比对,也就是我们所说的 diff,那么不同的两个 VNode 之间应该遵守怎样的比对规则呢?其实这个问题很容易回答,我们知道 VNode 有类型之分,不同类型的 VNode 之间存在一定的差异,所以不同的 VNode 之间第一个比对原则就是:只有相同类型的 VNode 才有比对的意义,例如我们有两个 VNode,其中一个 VNode 的类型是标签元素,而另一个 VNode 的类型是组件,当这两个 VNode 进行比对时,最优的做法是使用新的 VNode 完全替换旧的 VNode,换句话说我们根本就没有做任何比对的操作,因为这完全没有意义,所以根据这个思想我们实现的 patch 函数如下:

function patch(prevVNode, nextVNode, container) {
  // 分别拿到新旧 VNode 的类型,即 flags
  const nextFlags = nextVNode.flags
  const prevFlags = prevVNode.flags

  // 检查新旧 VNode 的类型是否相同,如果类型不同,则直接调用 replaceVNode 函数替换 VNode
  // 如果新旧 VNode 的类型相同,则根据不同的类型调用不同的比对函数
  if (prevFlags !== nextFlags) {
    replaceVNode(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.ELEMENT) {
    patchElement(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.COMPONENT) {
    patchComponent(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.TEXT) {
    patchText(prevVNode, nextVNode)
  } else if (nextFlags & VNodeFlags.FRAGMENT) {
    patchFragment(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.PORTAL) {
    patchPortal(prevVNode, nextVNode)
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

如上代码所示,既然 patch 函数的作用是用来比对新旧 VNode,那么 patch 函数必然需要接收新旧 VNode 作为参数,我们使用 prevVNode 形参代表旧的 VNode,使用 nextVNode 形参代表新的 VNode,如上是很清晰的一段比对逻辑,首先我们需要拿到新旧 VNode 的类型(flags),接着是一连串的 if...else if 语句,其核心原则是:如果类型不同,则直接调用 replaceVNode 函数使用新的 VNode 替换旧的 VNode,否则根据不同的类型调用与之相符的比对函数,如下图所示:

# 替换 VNode

我们首先来研究一下如何替换 VNode,即 replaceVNode 函数应该做什么,我们先来复现需要替换 VNode 的场景,如下代码所示:

// 旧的 VNode 是一个 div 标签
const prevVNode = h('div', null, '旧的 VNode')

class MyComponent {
  render () {
    return h('h1', null, '新的 VNode')
  }
}
// 新的 VNode 是一个组件
const nextVNode = h(MyComponent)

// 先后渲染新旧 VNode 到 #app
render(prevVNode, document.getElementById('app'))
render(nextVNode, document.getElementById('app'))
@前端进阶之旅: 代码已经复制到剪贴板

在如上代码中,我们先后渲染了新旧 VNode 到 #app 元素,由于新旧 VNode 具有不同的类型,所以此时会触发 VNode 的替换操作,替换操作并不复杂,本质就是把旧的 VNode 所渲染的DOM移除,再挂载新的 VNode,如下是 replaceVNode 函数的实现:

function replaceVNode(prevVNode, nextVNode, container) {
  // 将旧的 VNode 所渲染的 DOM 从容器中移除
  container.removeChild(prevVNode.el)
  // 再把新的 VNode 挂载到容器中
  mount(nextVNode, container)
}
@前端进阶之旅: 代码已经复制到剪贴板

TIP

完整代码&在线体验地址: https://codesandbox.io/s/jlxjk18vm5

看上去很简单,但实际上仅有这两行代码的话,是存在缺陷的。至于有何缺陷我们会在本章的后面讲解,因为目前我们的背景铺垫还不够。

# 更新标签元素

# 更新标签元素的基本原则

当新旧 VNode 的类型不同时,会调用 replaceVNode 函数直接使用新的 VNode 替换旧的 VNode。但如果新旧 VNode 的类型相同,则会根据不同的类型调用不同的比对函数,这一小节我们就来看看如何更新一个标签元素。

首先即使两个 VNode 的类型同为标签元素,但它们也可能是不同的标签,也就是说它们的 tag 属性值不尽相同。这就又引申出了一条更新原则:我们认为不同的标签渲染的内容不同,例如 ul 标签下只能渲染 li 标签,所以拿 ul 标签和一个 div 标签进行比对是没有任何意义的,这种情况下我们不会对旧的标签元素打补丁,而是使用新的标签元素替换旧的标签元素,这就需要用到我们前面讲过的 replaceVNode 函数,如下 patchElement 函数所示:

function patchElement(prevVNode, nextVNode, container) {
  // 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数,使用新的 VNode 替换旧的 VNode
  if (prevVNode.tag !== nextVNode.tag) {
    replaceVNode(prevVNode, nextVNode, container)
    return
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

那么如果新旧 VNode 描述的是相同的标签呢?如果标签相同,那两个 VNode 之间的差异就只会出现在 VNodeData 和 children 上了,所以对于描述相同标签的两个 VNode 之间的比对,本质上就是对 VNodeData 和 children 的比对,我们先来看一下如何更新 VNodeData,如下面两个 VNode 所示:

// 旧的 VNode
const prevVNode = h('div', {
  style: {
    width: '100px',
    height: '100px',
    backgroundColor: 'red'
  }
})

// 新的 VNode
const nextVNode = h('div', {
  style: {
    width: '100px',
    height: '100px',
    border: '1px solid green'
  }
})
@前端进阶之旅: 代码已经复制到剪贴板

← 渲染器的核心 Diff 算法图解 Vue 响应式原理 →

fe