vue组件是被如何渲染的?

765 阅读8分钟

Vue写久了,就会对如下的代码习以为常:

<template>
    <div>
       // 组件
        <a-row>
          <h2></h2>
        </a-row>
    </div>
</template>

上面的代码很简单啊,除了普通的html标签,还有一个组件标签<a-row>

但是细细一想,好像又不那么简单,<a-row>又不是浏览器特有的标签,如div, p, span等,浏览器直接解析这个<a-row>标签是会直接报错的。但是,浏览器并没有报错。

那肯定是Vue帮我们把<a-row>转化为了普通的html标签。Vue是怎么转化的呢?

今天试着来解答下。

什么是组件

Vue 的一个核心思想是组件化

所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSSJavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。

组件的使用就像普通的html标签一样。

因此,在用 Vue 开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。

new Vue 发生了什么

Vue项目的入口文件中都会有一个实例化new Vue的过程,那么这个对象内部是怎么工作的呢?

import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
  el: '#app',
  render: h => h(App)
})

下面的代码来源于Vue的源码,我删除了一些只保留必要的逻辑,保证能看懂就行。

function Vue (options) {
  this._init(options)
}

可以看到 Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法,该方法就是往实例上增加各种属性和方法。

// options就是实例化时,你传入的对象
Vue.prototype._init = function (options) {
  const vm = this
  ...  
  // 合并配置,并在vm上挂载$options
  vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
  )
  ...
  // 进行一系列的初始化,往实例上挂着一些系列的方法和属性
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  // 把data变成响应式
  initState(vm) 
  callHook(vm, 'created')
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
  1. 把传入的配置对象optionsVue构造函数上的属性和方法进行合并,构成实例的vm.$options

  2. 进行了一系列的初始化,目的也是往实例上挂载各种属性和方法。

  3. 执行vm.$mount 方法,目的就是把模板渲染成最终的 DOM

可以看到,created 钩子函数执行时,vue 实例已经完成了data 中的数据已经被响应式化并挂载到实例(this)上。

那么接下来我们来分析 Vue 的挂载过程。

生成render函数

Vue.prototype.$mount = function (el) {
  // 传入一个id,通过id获取的dom对象,记住是真实dom query:document.querySelector(el)
  el = el && query(el)
  
  const options = this.$options
  // 如果options中没有render函数
  if (!options.render) {
    let template = options.template
    if (template) {
      ...
      // 获取模板字符串
      template = template.innerHTML
      ...
    }
    if (template) {
      // 把模板字符串转化为render函数
      const { render, staticRenderFns } = compileToFunctions(template...)
      // 把render函数放在options对象上
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}

首先判断传入的配置对象options有没有render方法,如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。

Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个在线编译的过程。

$mount 方法实际上会去调用 mountComponent 方法:

export function mountComponent (vm, el) {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
  // 这一块后面会重点介绍
  new Watcher(vm, updateComponent)

  return vm
}

mountComponent 核心就是先实例化一个渲染Watcher,在它的实例化中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。

render函数生成虚拟 dom

Virtual DOM 现在都不陌生了,它产生的前提是浏览器中的 DOM 是很昂贵的。为了更直观的感受,可以简单的把一个简单的 div 元素的属性都打印出来,如图所示:

image.png

可以看到真正的 DOM 元素是非常庞大的,因为浏览器的标准就把 DOM 设计的非常复杂,当我们频繁的去做 DOM 更新,会产生一定的性能问题。

Virtual DOM 就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue 中,Virtual DOM 长成这个样子:

export default class VNode {
    this.tag = tag
    this.data = data // 属性
    this.children = children
    this.text = text
    this.elm = elm // 虚拟dom所对应的真实dom节点
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    ...
  }
  get child (): Component | void {
    return this.componentInstance
  }
}

VNode 其实是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等。

由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的

Vue 中,VNode是通过vm._render 方法生成的,接下来分析这部分的实现。

Vue.prototype._render = function () {
  const vm = this
  // 在编译的时候,会把template模板编译成render函数
  const { render, _parentVnode } = vm.$options
  ...
  let vnode
  vnode = render.call(vm._renderProxy, vm.$createElement)
  ...
  return vnode
}

这段代码最关键的是 render 方法的调用,我们在平时的开发工作中手写 render 方法的场景比较少,而写的比较多的是 template 模板,在之前的 mounted 方法的实现中,会把 template 编译成 render 方法。render函数长成这样,其中_c就是vm.$createElement方法。

image.png

下面是我们手写的一个render方法,在render方法中传入了一个参数函数createElement,这个参数就是对应上面的vm.$createElement函数。

new Vue({
  el: '#app',
  data() {
    return {
      message: 'hello vue'
    }
  },
  render(createElement) {
    return createElement(
      'div',
      {
        attrs: {
          id: 'app1'
        }
      },
      this.message
    )
  }
})

下面我们将以一段代码来看看生成vnode长啥样。

new Vue({
  el: '#app',
  data() {
    return {
      message: 'hello vue'
    }
  },
  render(createElement) {
    return createElement(
      'div',
      {
        attrs: {
          id: 'app1'
        }
      },
      [
          createElement('div', 'pengchangjun'), 
          this.message
      ]
    )
  }
})

当执行Vue.prototype._render,首先调用children里面的createElement,先生成子节点的vnode的。最终生成的vnode如下:


{
  tag: 'div',
  data: {
    attrs: {
      id: 'app1'
    }
  },
  children: [
    {
      // 这是一个VNode对象
      tag: 'div',
      data: undefined,
      text: undefined,
      children: [
        {
          // 这是一个vnode对象
          tag: undefined,
          data: undefined,
          children: undefined,
          text: 'pengchangjun',
          ...
        }
      ]
    },
    // 这是一个文本vnode
    {
      tag: undefined,
      data: undefined,
      children: undefined,
      text: 'hello vue',
      ...
    }
  ]
  text: undefined,
  elm: undefined
}

至此,我们了解了 createElement 创建 VNode 的过程,每个 VNode 有 childrenchildren 每个元素也是一个 VNode,这样就形成了一个 VNode Tree,它很好的描述了我们的 DOM Tree

接下来就是要把这个 VNode 渲染成一个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成的。

vnode 生成真实dom

虚拟dom 生成真实dom是通过vm._update 完成的:

Vue.prototype._update = function (vnode) {
  const vm = this
  ...
  if (!prevVnode) {
    # initial render 首次渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
  } else {
    # updates 更新渲染
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  ...
}

patch的过程过程非常复杂,下面用伪代码演示首次渲染的场景。

# 1. 模拟生成 VNode(对应渲染函数的输出)
function createVNode(tag, data = {}, children = [], text = null) {
  return {
    tag,
    data,
    children,
    text,
    elm: null // 后续绑定真实 DOM
  };
}

# 2. 核心:将 VNode 转化为真实 DOM 并返回
function createElm(vnode) {
  let elm;

  // 情况1:文本型 VNode(无 tag)
  if (vnode.text) {
    elm = document.createTextNode(vnode.text);
  } 
  // 情况2:元素型 VNode(有 tag)
  else {
    // 创建真实 DOM 元素
    elm = document.createElement(vnode.tag);
    
    // 处理属性(如 class、style、id 等)
    if (vnode.data) {
      for (const key in vnode.data) {
        if (key === 'class') {
          elm.className = vnode.data[key];
        } else if (key === 'style') {
          Object.assign(elm.style, vnode.data[key]);
        } else {
          elm.setAttribute(key, vnode.data[key]);
        }
      }
    }

    # 递归处理子 VNode:创建子 DOM 并追加到父元素
    if (vnode.children && vnode.children.length) {
      vnode.children.forEach(childVNode => {
        const childElm = createElm(childVNode); // 递归创建子 DOM
        elm.appendChild(childElm);              // 追加到父元素
      });
    }
  }

  # 将真实 DOM 绑定到 VNode 的 elm 属性(关键:建立 VNode 和真实 DOM 的关联)
  vnode.elm = elm;
  return elm;
}

# 3. 挂载真实 DOM 到页面
function mount(vnode, container) {
  const elm = createElm(vnode);
  container.appendChild(elm);
}
  • createElm 是核心:Vue 源码中 src/core/vdom/patch.js 下的 createElm 函数是 VNode 转真实 DOM 的核心,上面的简化代码就是对这个函数的核心逻辑还原;

  • 处理不同节点类型:源码中还会处理组件 VNode、注释节点、插槽等,本质都是先创建对应类型的真实 DOM,再递归处理子节点;

  • 属性处理更完善:Vue 会处理 classstylev-bind、事件绑定(onClick 等)、自定义指令等,最终都映射到真实 DOM 的属性 / 事件上;

  • 无 Diff 过程:首次渲染时 patch 方法的第一个参数是 null,因此直接走 createElm 逻辑,而非更新时的 Diff + Patch。

创建组件vnode

上面的例子是介绍普通的标签是如何渲染成真实DOM的,createElement接受的是一个div标签

var app = new Vue({
  el: '#app',
  render: function (createElement) {
    return createElement('div', 'test')
  }
})

如果createElement接受的是一个组件呢?

var app = new Vue({
  el: '#app',
  render: h => h(App)
})

在分析 createElement 的实现的时候,它最终会调用 _createElement 方法,其中有一段逻辑是对参数 tag 的判断,如果是一个普通的 html 标签,比如是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode

if (typeof tag === 'string') {
  ...
  vnode = new VNode(tag, data, children, undefined, undefined, context)
} else {
  vnode = createComponent(tag, data, context, children)
}

如果是一个组件,那么tag是一个对象,这个对象就是我们在写组件的时候export default导出来的对象。可以看到template模板已经被转化为了render函数

image.png

接下来看一下 createComponent 方法的实现:

export function createComponent (Ctor,data,context,children, tag) {
  const baseCtor = context.$options._base
  
  # 构造子类构造函数
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
  ...
  data = data || {}
  ...
  # 安装组件钩子函数
  installComponentHooks(data)
  
  # 实例化 VNode
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}

针对组件渲染,主要就 3 个关键步骤:构造子类构造函数,安装组件钩子函数和实例化 vnode。

构造子类构造函数

const baseCtor = context.$options._base

if (isObject(Ctor)) {
  # 创建Vue的子类,也就是VueComponent构造函数
  Ctor = baseCtor.extend(Ctor)
}

上面代码的作用就是创建Vue的子类,也就是VueComponent构造函数,在这里 baseCtor 实际上就是 Vue构造函数。Vue.extend 函数如下:

Vue.extend = function (extendOptions) {
  extendOptions = extendOptions || {}
  // this表示Vue构造函数
  const Super = this
  const SuperId = Super.cid
  const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
  // 如果某个组件被多次引用,那么就对构造函数做一个缓存,以免每次都要创建构造函数
  // 当组件第一次被使用的时候,就在extendOptions增加一个属性,当下次再被引用的时候把extendOptions传进来,发现已经被创建过了,那么就直接返回
  if (cachedCtors[SuperId]) {
    return cachedCtors[SuperId]
  }
  // 组件构造函数
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  // 组件构造函数的原型指向Vue的原型,实现继承
  Sub.prototype = Object.create(Super.prototype)
  // 把组件构造函数的constructor重新指向组件构造函数
  Sub.prototype.constructor = Sub
  Sub.cid = cid++
  // 把Vue构造函数的options和组件构造函数的option做一个合并
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super
  ...
  Sub.extend = Super.extend
  Sub.mixin = Super.mixin
  Sub.use = Super.use
  ASSET_TYPES.forEach(function (type) {
    Sub[type] = Super[type]
  })
  ...
  cachedCtors[SuperId] = Sub
  return Sub
}

vue.extend 的作用就是构造一个 Vue 的子类,它使用一种非常经典的原型继承的方式把一个纯对象转换一个继承于 Vue 的构造器 Sub 并返回,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作;最后对于这个 Sub 构造函数做了缓存,避免多次执行 Vue.extend 的时候对同一个子组件重复构造。

这样当我们去实例化 Sub 的时候,就会执行 this._init 逻辑再次走到了 Vue 实例的初始化逻辑。

这里一定要记住,组件的构造函数是VueComponent而不是Vue,只不过VueComponentVue的子类。

安装组件钩子函数

installComponentHooks(data)

Vue 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNodepatch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue也是充分利用这一点,在初始化一个 Component 类型的 VNode 的过程中实现了几个钩子函数:

const componentVNodeHooks = {
  // 初始化钩子
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    ...
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode, activeInstance)
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  },
  prepatch (oldVnode, vnode) {},
  insert (vnode) {},
  destroy (vnode) {}
}

const hooksToMerge = Object.keys(componentVNodeHooks)

function installComponentHooks (data) {
  const hooks = data.hook || (data.hook = {})
  for (let i = 0; i < hooksToMerge.length; i++) {
    ...
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}

整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数。

image.png

实例化 VNode

const name = Ctor.options.name || tag
const vnode = new VNode(
  `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
  data, undefined, undefined, undefined, context,
  { Ctor, propsData, listeners, tag, children },
  asyncFactory
)
return vnode

到此就生成了一个组件的vnode,它仅仅是一个占位符vnode,并不是渲染vnode,如下代码就是一个组件的占位符vnode,可以看到占位符vnodetag与普通的html标签是不一样的。

{
    tag"vue-component-1-App",
    data: {
        on: undefined,
        hook: {
            init(){},
            prepatch(){},
            insert(){},
            destory(){}
        }
    },
    children: undefined,
    text: undefined,
    componentInstance: undefined,
    componentOptions: {
        Ctor: function VueComponent(){}, // 组件的构造器
        childrenundefined,
        listenersundefined,
        propsDataundefined,
        tagundefined
    }
    ...
}

组件的patch过程

通过 createComponent 创建了组件 VNode,接下来会走到 vm._update,执行 vm.__patch__ 去把 VNode 转换成真正的 DOM 节点。

这个过程我们在前面已经分析过了,但是那是针对一个普通的 VNode 节点,接下来我们来看看组件的 VNode 会有哪些不一样的地方。

patch 的过程会调用 createElm 创建元素节点:

function createElm (vnode, insertedVnodeQueue, parentElm..){
  ...
  // 判断是否是组件vnode
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
  ...
}
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      // 执行init函数
      i(vnode, false)
    }
    
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

如果 vnode 是一个组件 VNode,那么条件会满足,并且得到 i 就是 init 钩子函数,上节在创建组件 VNode 的时候合并钩子函数中就包含 init 钩子函数。

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    ...
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
},

init 钩子函数执行也很简单,它是通过 createComponentInstanceForVnode 创建一个 VueComponent 的实例,然后调用 $mount 方法挂载子组件, 先来看一下 createComponentInstanceForVnode 的实现:

export function createComponentInstanceForVnode (vnode, parent) {
  const options: InternalComponentOptions = {
    // `_isComponent` 为 `true` 表示它是一个组件
    _isComponent: true,
    // 占位符vnode
    _parentVnode: vnode,
    // 当前激活的实例,即Vue的实例,不是VueComponent的实例
    parent
  }
  ...
  # 这里才是VueComponent的实例
  return new vnode.componentOptions.Ctor(options)
}

createComponentInstanceForVnode 函数生成一个组件的参数options,然后执行 new vnode.componentOptions.Ctor(options)生成一个组件实例。

image.png

所以,子组件的实例化实际上就是在这个时机执行的,它会执行实例的 _init 方法,这个过程和new Vue的过程就差不多了,将组件内部的 VNode 转为真实 DOM,挂载到组件 VNode 的 elm 属性上

所以,经过上面的步骤可以知道,在渲染组件的时候,生成了两种vnode:占位符$vnode和渲染_vnode

我们用一个直观的代码来看看占位符vnode和渲染vnode的差别:

占位符vnode:它的tag是类似于vue-component-1-App,同时,它存在data.hook属性钩子。

{
    tag"vue-component-1-App",
    data: {
        on: undefined,
        hook: {
            init(){},
            ...
        }
    },
    children: undefined,
    text: undefined,
    componentInstance: undefined,
    componentOptions: {
        Ctor: function VueComponent(){}, // 组件的构造器
        ...
        tagundefined
    }
    ...
}

渲染vnode:它的tag是一个真实html标签,它没有data.hook属性钩子,这个才是最后patch用的vnode

{
  tag: 'div',
  data: {
    attrs: {id: 'app'}
  },
  // 组件的父vnode,即占位符vnode
  parent: {
    tag: "vue-component-1-App",
    componentInstance: 'VueComponent实例',
    componentOptions: {},
    children: undefined
  },
  children: [
    {
      tag: "div",
      data: undefined,
      children: [
        {
          tag: undefined,
          data: undefined,
          text: " pengchangjun "
        }
      ]
    }
  ]
}
  • $vnode:称为组件占位符 VNode,是父组件中表示当前组件的那个 VNode;
  • _vnode:称为组件内部渲染 VNode,是当前组件自身渲染生成的根 VNode(比如组件模板编译后生成的 div/ul 等真实元素 VNode)。

简单来说:$vnode 是父组件视角下的当前组件,_vnode 是当前组件自身渲染出的内容。

怎么理解呢?

当一个<App>组件里面包含了一个<Child>,当第一次把模板转化为vnode时,遇到<Child>会把它转为为$vnode,这是父组件视角下的当前子组件的vnode。

**当进入到<Child>组件内部渲染时,才开始生成组件内部的vnode,也就是渲染vnode。 **

组件更新

Vue2 采用 虚拟 DOM(VNode) + Diff 算法(补丁算法) 实现高效更新,核心是「只更新新旧 VNode 有差异的部分到真实 DOM」,而非重新生成整个 DOM。整个过程可以分为 4 步:

  1. 组件数据更新触发重新渲染

当组件的 data/props/computed 等响应式数据变化时,Vue 会触发组件的重新渲染,生成新的 VNode 树(对应更新后的组件结构)。

  1. 新旧 VNode 进行 Diff 比对(核心)

Vue2 的 Diff 算法是同层比对(不会跨层级比对,降低复杂度),只对比新旧 VNode 树的同一层级节点,找出最小差异

  • 先比对节点的 key 和标签名,如果不一致,直接销毁旧节点、创建新节点;
  • 如果节点类型一致,再比对节点的属性(如 class/style/attrs)、子节点等,记录差异(「补丁」)。
  1. 执行补丁(Patch):只更新差异到真实 DOM

Diff 比对完成后,Vue 不会重新生成整个真实 DOM,而是根据记录的补丁,只对真实 DOM 中对应差异的部分进行增 / 删 / 改操作

  • 比如只是文本内容变了,就直接修改真实 DOM 的 textContent
  • 比如只是某个属性变了,就只更新该 DOM 元素的对应属性;
  • 比如子节点多了一个,就只新增这个子 DOM 节点,而非重建整个父节点。
  1. 递归处理子 VNode

如果当前节点的子 VNode 有差异,会递归对其子节点执行 Diff + Patch 操作,直到整个 VNode 树比对完成。

下面用伪代码模拟 Vue2 的更新流程,理解只更新差异的核心:

// 1. 定义旧VNode(更新前的结构)
const oldVNode = {
  tag: 'div',
  props: { class: 'old-class' },
  children: [
    { tag: 'p', text: '旧文本' },
    { tag: 'span', text: '不变的文本' }
  ],
  elm: document.querySelector('.container') // 关联的真实DOM
};

// 2. 数据更新,生成新VNode(更新后的结构)
const newVNode = {
  tag: 'div',
  props: { class: 'new-class' },
  children: [
    { tag: 'p', text: '新文本' },
    { tag: 'span', text: '不变的文本' }
  ]
};

// 3. Diff算法:比对新旧VNode,找出差异
function diff(oldVNode, newVNode) {
  const patches = [];
  // 比对标签(这里标签相同,跳过)
  if (oldVNode.tag === newVNode.tag) {
    // 比对属性:class变了,记录属性补丁
    if (oldVNode.props.class !== newVNode.props.class) {
      patches.push({
        type: 'ATTR',
        key: 'class',
        value: newVNode.props.class
      });
    }
    // 比对子节点
    oldVNode.children.forEach((oldChild, index) => {
      const newChild = newVNode.children[index];
      // 文本不同,记录文本补丁
      if (oldChild.text !== newChild.text) {
        patches.push({
          type: 'TEXT',
          index, // 子节点索引
          value: newChild.text
        });
      }
    });
  }
  return patches;
}

// 4. Patch算法:根据补丁更新真实DOM(只更差异)
function patch(elm, patches) {
  patches.forEach(patch => {
    switch (patch.type) {
      case 'ATTR':
        // 只更新class属性,不碰其他
        elm.setAttribute(patch.key, patch.value);
        break;
      case 'TEXT':
        // 只更新对应索引的子节点文本,不重建子节点
        elm.children[patch.index].textContent = patch.value;
        break;
    }
  });
}

// 执行更新流程
const patches = diff(oldVNode, newVNode);
patch(oldVNode.elm, patches);

新旧 VNode 比对后,不会替换旧 VNode 的差异部分再重新生成整个真实 DOM,而是直接根据差异去修改已有的真实 DOM 节点,这是 Vue 高效更新的核心。