Vue 3源码极客之:`Vue`的`patch`函数:它如何处理`VNode`的`props`、`events`和`directives`更新。

大家好!今天咱们来聊聊 Vue 3 源码里那个神秘又重要的家伙——patch 函数。别怕,虽然它深藏在源码深处,但其实也没那么可怕。咱们的目标是把它扒个精光,看看它到底是怎么处理 VNode 的 propseventsdirectives 更新的。

首先,打个招呼: 各位老铁,准备好了吗?咱们要开车了!目的地:patch 函数的内部世界!

Patch 函数是个啥?

在 Vue 的世界里,patch 函数是虚拟 DOM (VNode) 的核心算法。简单来说,它的任务就是比较新旧 VNode,然后把差异应用到真实的 DOM 上,从而实现高效的更新。

patch函数有很多分支,针对不同类型的 VNode 有不同的处理逻辑。今天我们主要关注的是:当新旧 VNode 都是元素节点,并且需要更新 propseventsdirectives 时,patch 函数是怎么工作的。

Props 的更新:新老 Props 大作战

Props,也就是 HTML 属性,比如 classstyleid 等等。patch 函数处理 props 的更新的核心思路是:

  1. 找出需要新增/修改的 props: 遍历新的 VNode 的 props,如果旧的 VNode 没有这个 prop,或者值不一样,那就需要更新。
  2. 找出需要删除的 props: 遍历旧的 VNode 的 props,如果新的 VNode 没有这个 prop,那就需要删除。

下面咱们用伪代码来模拟一下这个过程:

function patchProps(el, oldProps, newProps) {
  if (oldProps === newProps) {
    return; // 如果 props 没变,直接结束
  }

  oldProps = oldProps || {}; // 避免 undefined 错误
  newProps = newProps || {};

  // 1. 处理新增/修改的 props
  for (const key in newProps) {
    const oldValue = oldProps[key];
    const newValue = newProps[key];

    if (newValue !== oldValue) {
      patchProp(el, key, oldValue, newValue); // 真正更新 prop 的函数
    }
  }

  // 2. 处理需要删除的 props
  for (const key in oldProps) {
    if (!(key in newProps)) {
      patchProp(el, key, oldProps[key], null); // 传入 null 表示删除
    }
  }
}

// 实际执行props 更新的函数
function patchProp(el, key, oldValue, newValue) {
    if (newValue === null) {
        // 删除 prop
        el.removeAttribute(key)
    } else {
        // 更新 prop
        el.setAttribute(key,newValue)
    }
}

上面的 patchProps 函数就像一个战场指挥官,负责调度,而 patchProp 才是真正冲锋陷阵的士兵,负责执行具体的 DOM 操作。

patchProp 函数的细节:

patchProp 函数会根据 prop 的类型做不同的处理。比如:

  • class: 直接设置 el.className
  • style: 比较新旧 style 对象,然后更新 el.style 的各个属性。
  • 其他属性: 直接设置 el.setAttribute(key, value)

Vue 3 源码里对 patchProp 的实现要复杂得多,因为它要处理各种特殊情况,比如 boolean 属性、属性名大小写、xlink 命名空间等等。但核心思路是不变的:比较新旧值,然后更新 DOM。

举个栗子:

假设我们有这样一个 VNode:

const oldVNode = {
  type: 'div',
  props: {
    class: 'old-class',
    id: 'old-id',
    style: {
      color: 'red',
      fontSize: '16px'
    }
  }
};

const newVNode = {
  type: 'div',
  props: {
    class: 'new-class',
    title: 'new-title',
    style: {
      color: 'blue',
      fontWeight: 'bold'
    }
  }
};

那么,patchProps 函数会做以下事情:

  1. class: 新值是 new-class,旧值是 old-class,不一样,更新 el.className = 'new-class'
  2. id: 旧值是 old-id,新值没有,删除 el.removeAttribute('id')
  3. title: 新值是 new-title,旧值没有,新增 el.setAttribute('title', 'new-title')
  4. style:
    • color: 新值是 blue,旧值是 red,更新 el.style.color = 'blue'
    • fontSize: 旧值是 16px,新值没有,删除 el.style.fontSize (或者设置为 '')。
    • fontWeight: 新值是 bold,旧值没有,新增 el.style.fontWeight = 'bold'

Events 的更新:addEventListener 和 removeEventListener 的艺术

Events,也就是事件监听器,比如 clickmouseoverinput 等等。Vue 3 对 events 的处理方式和 props 有些不同,因为它需要管理事件监听器的绑定和解绑。

核心思路是:

  1. normalize event name: Vue 会对事件名进行格式化,例如将 @click 转换为 onClick
  2. 找出需要新增的事件监听器: 遍历新的 VNode 的 props,找到以 on 开头的属性,如果旧的 VNode 没有这个事件监听器,或者回调函数不一样,那就需要绑定新的事件监听器。
  3. 找出需要删除的事件监听器: 遍历旧的 VNode 的 props,找到以 on 开头的属性,如果新的 VNode 没有这个事件监听器,那就需要解绑旧的事件监听器。

同样的,咱们用伪代码来模拟一下:

function patchEvents(el, oldProps, newProps) {
  if (oldProps === newProps) {
    return; // 如果 events 没变,直接结束
  }

  oldProps = oldProps || {};
  newProps = newProps || {};

  // 1. 处理新增/修改的事件监听器
  for (const key in newProps) {
    if (key.startsWith('on')) {
      const eventName = key.slice(2).toLowerCase(); // 获取事件名,比如 "click"
      const newHandler = newProps[key];
      const oldHandler = oldProps[key];

      if (newHandler !== oldHandler) {
        if (oldHandler) {
          el.removeEventListener(eventName, oldHandler); // 先解绑旧的
        }
        el.addEventListener(eventName, newHandler); // 再绑定新的
      }
    }
  }

  // 2. 处理需要删除的事件监听器
  for (const key in oldProps) {
    if (key.startsWith('on') && !(key in newProps)) {
      const eventName = key.slice(2).toLowerCase();
      const oldHandler = oldProps[key];
      el.removeEventListener(eventName, oldHandler); // 解绑旧的
    }
  }
}

这个 patchEvents 函数也像一个事件调度员,负责管理事件监听器的绑定和解绑。

关于事件处理函数的存储:

Vue 3 并没有直接把事件处理函数绑定到 DOM 元素上,而是使用了一种叫做 "invoker" 的机制。简单来说,invoker 是一个包装函数,它会存储事件处理函数,并且在事件触发时执行它。

这样做的好处是:

  • 方便统一管理事件处理函数。
  • 方便在事件触发前后执行一些额外的逻辑,比如阻止默认行为、停止事件传播等等。

举个栗子:

假设我们有这样一个 VNode:

const oldVNode = {
  type: 'div',
  props: {
    onClick: () => console.log('old click'),
    onMouseover: () => console.log('old mouseover')
  }
};

const newVNode = {
  type: 'div',
  props: {
    onClick: () => console.log('new click'),
    onFocus: () => console.log('new focus')
  }
};

那么,patchEvents 函数会做以下事情:

  1. onClick: 新值和旧值不一样,先解绑旧的 onClick,再绑定新的 onClick
  2. onMouseover: 旧值有 onMouseover,新值没有,解绑 onMouseover
  3. onFocus: 新值有 onFocus,旧值没有,绑定 onFocus

Directives 的更新:自定义指令的舞台

Directives,也就是自定义指令,是 Vue 提供的一种扩展 HTML 功能的机制。比如 v-modelv-ifv-for 等等,都是指令。

patch 函数处理 directives 的更新比 props 和 events 要复杂一些,因为它需要调用指令的各种钩子函数,比如 beforeMountmountedbeforeUpdateupdatedbeforeUnmountunmounted 等等。

核心思路是:

  1. normalize directives: 确保指令格式正确。
  2. 找出需要新增的指令: 遍历新的 VNode 的 directives,如果旧的 VNode 没有这个指令,那就需要调用 beforeMountmounted 钩子函数。
  3. 找出需要更新的指令: 遍历新的 VNode 的 directives,如果旧的 VNode 也有这个指令,那就需要调用 beforeUpdateupdated 钩子函数。
  4. 找出需要删除的指令: 遍历旧的 VNode 的 directives,如果新的 VNode 没有这个指令,那就需要调用 beforeUnmountunmounted 钩子函数。

咱们用伪代码来模拟一下:

function patchDirectives(el, oldVNode, newVNode) {
  const oldDirectives = oldVNode.directives || [];
  const newDirectives = newVNode.directives || [];

  // 1. 处理新增的指令
  for (const newDirective of newDirectives) {
    const oldDirective = oldDirectives.find(d => d.name === newDirective.name);
    if (!oldDirective) {
      // 调用 beforeMount 钩子
      if (newDirective.directive.beforeMount) {
        newDirective.directive.beforeMount(el, newDirective.binding, oldVNode, newVNode);
      }

      // 调用 mounted 钩子
      if (newDirective.directive.mounted) {
        newDirective.directive.mounted(el, newDirective.binding, oldVNode, newVNode);
      }
    }
  }

  // 2. 处理更新的指令
  for (const newDirective of newDirectives) {
    const oldDirective = oldDirectives.find(d => d.name === newDirective.name);
    if (oldDirective) {
      // 调用 beforeUpdate 钩子
      if (newDirective.directive.beforeUpdate) {
        newDirective.directive.beforeUpdate(el, newDirective.binding, oldDirective.binding, oldVNode, newVNode);
      }

      // 调用 updated 钩子
      if (newDirective.directive.updated) {
        newDirective.directive.updated(el, newDirective.binding, oldDirective.binding, oldVNode, newVNode);
      }
    }
  }

  // 3. 处理删除的指令
  for (const oldDirective of oldDirectives) {
    const newDirective = newDirectives.find(d => d.name === oldDirective.name);
    if (!newDirective) {
      // 调用 beforeUnmount 钩子
      if (oldDirective.directive.beforeUnmount) {
        oldDirective.directive.beforeUnmount(el, oldDirective.binding, oldVNode, newVNode);
      }

      // 调用 unmounted 钩子
      if (oldDirective.directive.unmounted) {
        oldDirective.directive.unmounted(el, oldDirective.binding, oldVNode, newVNode);
      }
    }
  }
}

这个 patchDirectives 函数就像一个指令调度员,负责管理指令的生命周期。

关于指令的 binding 对象:

每个指令都有一个 binding 对象,它包含了指令的各种信息,比如:

  • value: 指令的值,比如 v-model="message" 中的 message
  • oldValue: 指令的旧值。
  • arg: 指令的参数,比如 v-bind:href="url" 中的 href
  • modifiers: 指令的修饰符,比如 v-on:click.prevent="handleClick" 中的 prevent

指令的钩子函数可以通过 binding 对象来获取这些信息,从而实现各种自定义的逻辑。

举个栗子:

假设我们有这样一个 VNode:

const oldVNode = {
  type: 'div',
  directives: [
    {
      name: 'focus',
      directive: {
        mounted: (el) => el.focus()
      },
      binding: {}
    }
  ]
};

const newVNode = {
  type: 'div',
  directives: [
    {
      name: 'focus',
      directive: {
        updated: (el) => console.log('focus updated')
      },
      binding: {}
    },
    {
      name: 'highlight',
      directive: {
        beforeMount: (el) => el.style.backgroundColor = 'yellow'
      },
      binding: {}
    }
  ]
};

那么,patchDirectives 函数会做以下事情:

  1. focus:
    • 旧 VNode 有 focus 指令,新 VNode 也有,调用 updated 钩子函数。
  2. highlight:
    • 新 VNode 有 highlight 指令,旧 VNode 没有,调用 beforeMount 钩子函数。

总结:

patch 函数处理 propseventsdirectives 的更新的核心思路都是:比较新旧值,然后更新 DOM。

  • Props: 比较新旧属性,新增、修改或删除属性。
  • Events: 比较新旧事件监听器,绑定或解绑事件监听器。
  • Directives: 调用指令的各种钩子函数,管理指令的生命周期。

一张表格总结:

特性 处理方式 核心 API
Props 1. 遍历 newProps,如果 key 不存在于 oldProps 或 值不同,则更新/新增属性。 2. 遍历 oldProps, 如果 key 不存在于 newProps, 则删除属性。 el.setAttribute, el.removeAttribute, el.className, el.style
Events 1. 遍历 newProps, 如果 key 以 "on" 开头且事件处理函数不同,则先移除旧的事件监听器,再添加新的事件监听器。 2. 遍历 oldProps,如果 key 以 "on" 开头且 newProps 中不存在该 key,则移除事件监听器。 el.addEventListener, el.removeEventListener
Directives 1. 遍历 newDirectives, 如果 oldDirectives 中不存在该指令,调用 beforeMountmounted 钩子。 2. 遍历 newDirectives, 如果 oldDirectives 中存在该指令,调用 beforeUpdateupdated 钩子。 3. 遍历 oldDirectives,如果 newDirectives 中不存在该指令,调用 beforeUnmountunmounted 钩子。 指令的各种钩子函数 ( beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted ),指令的 binding 对象

最后,说点掏心窝子的话:

理解 patch 函数是深入理解 Vue 核心原理的关键一步。虽然源码看起来很复杂,但只要抓住核心思路,一步一步地分析,你就能揭开它的神秘面纱。

希望今天的分享对你有所帮助!下次有机会,咱们再聊聊 patch 函数的其他部分,比如如何处理文本节点、注释节点、组件节点等等。

感谢各位老铁的观看!咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注