阅读视图

发现新文章,点击刷新页面。

代码生成:从AST到render函数

在前几篇文章中,我们学习了代码编译--转成--生成的过程。今天,我们将聚焦于指令系统——这个 Vue 中强大的声明式功能。从内置指令(v-if、v-for、v-model)到自定义指令,我们将深入它们的编译原理和运行时实现。

前言:指令的本质

指令是 Vue 模板中带有 v- 前缀的特殊属性。它本质上是一种声明式的语法糖,让我们能够在模板中直接操作 DOM 元素。

<!-- 使用指令 -->
<input v-model="message" />
<div v-if="visible">条件渲染</div>
<div v-custom:arg.modifier="value">自定义指令</div>

指令的注册方式

全局注册

const app = createApp(App);

app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});

app.directive('color', {
  mounted(el, binding) {
    el.style.color = binding.value;
  },
  updated(el, binding) {
    el.style.color = binding.value;
  }
});

局部注册

export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus();
      }
    },
    color: {
      mounted(el, binding) {
        el.style.color = binding.value;
      },
      updated(el, binding) {
        el.style.color = binding.value;
      }
    }
  }
}

import { directive } from 'vue';

export default {
  setup() {
    const vFocus = {
      mounted(el) {
        el.focus();
      }
    };
    
    return { vFocus };
  }
}

组件注册原理

// 指令注册的内部实现
function createDirective(name, definition) {
  // 规范化指令定义
  if (typeof definition === 'function') {
    // 函数简写形式
    definition = {
      mounted: definition,
      updated: definition
    };
  }
  
  return {
    name,
    ...definition
  };
}

// 全局注册表
const globalDirectives = new Map();

app.directive = function(name, definition) {
  if (definition === undefined) {
    // 获取指令
    return globalDirectives.get(name);
  } else {
    // 注册指令
    globalDirectives.set(name, createDirective(name, definition));
    return this;
  }
};

指令生命周期钩子

完整的钩子函数

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode) {
    console.log('created', binding);
  },
  
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {
    console.log('beforeMount', binding);
  },
  
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {
    console.log('mounted', binding);
    el.focus();
  },
  
  // 在包含组件的 VNode 更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {
    console.log('beforeUpdate', binding);
  },
  
  // 在包含组件的 VNode 及其子组件的 VNode 更新后调用
  updated(el, binding, vnode, prevVnode) {
    console.log('updated', binding);
  },
  
  // 在绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {
    console.log('beforeUnmount', binding);
  },
  
  // 在绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {
    console.log('unmounted', binding);
  }
};

binding 对象的属性

const binding = {
  value: 'directive value',        // 指令绑定的值
  oldValue: 'old value',            // 更新前的值
  arg: 'argName',                   // 指令参数
  modifiers: {                       // 修饰符对象
    prevent: true,
    stop: true
  },
  instance: componentInstance,       // 组件实例
  dir: directiveDefinition,          // 指令定义对象
  // 在 Vue 3.4+ 中新增
  modifiersKeys: ['prevent', 'stop'] // 修饰符数组
};

组件钩子函数的调用时机

组件钩子函数的调用时机

编译阶段的指令处理

指令的 AST 表示

我们来看一个比较复杂的自定义指令的例子:

<div v-custom:arg.mod1.mod2="value"></div>

这个例子对应的 AST 节点如下:

const elementNode = {
  type: 'Element',
  tag: 'div',
  props: [
    // 普通属性
    { name: 'class', value: 'container' },
    // 指令
    {
      type: 'Directive',
      name: 'custom',
      arg: 'arg',
      modifiers: ['mod1', 'mod2'],
      value: 'value',
      exp: {
        type: 'Expression',
        content: 'value'
      }
    }
  ]
};

指令的编译转换

/**
 * 指令转换插件
 */
const transformDirective = (node, context) => {
  if (node.type !== 'Element') return;
  
  if (!node.props) node.props = [];
  
  // 收集指令
  const directives = [];
  
  for (let i = node.props.length - 1; i >= 0; i--) {
    const prop = node.props[i];
    
    if (prop.type === 'Directive') {
      directives.push(prop);
      node.props.splice(i, 1); // 从props中移除
    }
  }
  
  if (directives.length === 0) return;
  
  // 为节点添加指令信息
  node.directives = directives.map(dir => ({
    name: dir.name,
    arg: dir.arg,
    modifiers: dir.modifiers,
    value: dir.value,
    exp: dir.exp
  }));
};

/**
 * 内置指令的转换
 */
const transformBuiltInDirectives = (node, context) => {
  if (!node.directives) return;
  
  for (const dir of node.directives) {
    switch (dir.name) {
      case 'if':
        transformVIf(node, dir, context);
        break;
      case 'for':
        transformVFor(node, dir, context);
        break;
      case 'model':
        transformVModel(node, dir, context);
        break;
      case 'show':
        transformVShow(node, dir, context);
        break;
      case 'on':
        transformVOn(node, dir, context);
        break;
      case 'bind':
        transformVBind(node, dir, context);
        break;
      // 自定义指令会保留,运行时处理
    }
  }
};

指令的代码生成

/**
 * 生成指令的运行时代码
 */
const genDirective = (dir, context) => {
  const { name, arg, modifiers, value } = dir;
  
  // 处理参数
  const argStr = arg ? `'${arg}'` : 'null';
  
  // 处理修饰符
  const modifiersObj = {};
  if (modifiers) {
    for (const mod of modifiers) {
      modifiersObj[mod] = true;
    }
  }
  
  // 生成指令对象
  return {
    name: `'${name}'`,
    value: `() => ${value}`,
    arg: argStr,
    modifiers: JSON.stringify(modifiersObj)
  };
};

/**
 * 生成节点上的所有指令
 */
const genDirectives = (node, context) => {
  if (!node.directives || node.directives.length === 0) return '';
  
  const dirs = node.directives.map(dir => genDirective(dir, context));
  
  return `directives: [${dirs.map(d => `{${Object.entries(d).map(([k, v]) => `${k}: ${v}`).join(', ')}}`).join(', ')}]`;
};

运行时的指令调用

指令调度器

/**
 * 运行时指令管理器
 */
class DirectiveManager {
  constructor() {
    this.directives = new Map(); // 全局指令
    this.instances = new WeakMap(); // 元素上的指令实例
  }
  
  /**
   * 注册指令
   */
  register(name, definition) {
    this.directives.set(name, definition);
  }
  
  /**
   * 获取指令定义
   */
  get(name) {
    return this.directives.get(name);
  }
  
  /**
   * 在元素上应用指令
   */
  applyDirectives(el, vnode) {
    const { directives } = vnode;
    if (!directives) return;
    
    const instances = [];
    
    for (const dir of directives) {
      const definition = this.get(dir.name);
      if (!definition) {
        console.warn(`指令 ${dir.name} 未注册`);
        continue;
      }
      
      // 创建指令实例
      const instance = {
        dir: definition,
        binding: this.createBinding(dir, vnode),
        vnode
      };
      
      instances.push(instance);
      
      // 调用 created 钩子
      if (definition.created) {
        definition.created(el, instance.binding, vnode);
      }
    }
    
    this.instances.set(el, instances);
  }
  
  /**
   * 创建 binding 对象
   */
  createBinding(dir, vnode) {
    return {
      value: dir.value ? dir.value() : undefined,
      oldValue: undefined,
      arg: dir.arg,
      modifiers: dir.modifiers || {},
      instance: vnode.component,
      dir: this.get(dir.name)
    };
  }
  
  /**
   * 更新指令
   */
  updateDirectives(oldVNode, newVNode) {
    const el = newVNode.el;
    const oldInstances = this.instances.get(el) || [];
    const newDirectives = newVNode.directives || [];
    
    // 创建新实例的映射
    const newInstances = [];
    const newDirMap = new Map();
    
    for (const dir of newDirectives) {
      newDirMap.set(dir.name, dir);
    }
    
    // 更新现有指令
    for (const oldInstance of oldInstances) {
      const newDir = newDirMap.get(oldInstance.dir.name);
      
      if (newDir) {
        // 指令仍然存在,更新 binding
        const oldBinding = oldInstance.binding;
        const newBinding = this.createBinding(newDir, newVNode);
        newBinding.oldValue = oldBinding.value;
        
        // 调用 beforeUpdate
        if (oldInstance.dir.beforeUpdate) {
          oldInstance.dir.beforeUpdate(el, newBinding, newVNode, oldInstance.vnode);
        }
        
        // 更新实例
        oldInstance.binding = newBinding;
        oldInstance.vnode = newVNode;
        newInstances.push(oldInstance);
        
        newDirMap.delete(oldInstance.dir.name);
      } else {
        // 指令被移除,调用 beforeUnmount
        if (oldInstance.dir.beforeUnmount) {
          oldInstance.dir.beforeUnmount(el, oldInstance.binding, oldInstance.vnode);
        }
      }
    }
    
    // 添加新指令
    for (const [name, dir] of newDirMap) {
      const definition = this.get(name);
      if (!definition) continue;
      
      const instance = {
        dir: definition,
        binding: this.createBinding(dir, newVNode),
        vnode: newVNode
      };
      
      // 调用 created
      if (definition.created) {
        definition.created(el, instance.binding, newVNode);
      }
      
      newInstances.push(instance);
    }
    
    this.instances.set(el, newInstances);
  }
  
  /**
   * 触发指令钩子
   */
  invokeHook(el, hookName, ...args) {
    const instances = this.instances.get(el);
    if (!instances) return;
    
    for (const instance of instances) {
      const hook = instance.dir[hookName];
      if (hook) {
        hook(el, instance.binding, ...args);
      }
    }
  }
}

// 创建全局指令管理器
const directiveManager = new DirectiveManager();

与渲染器的集成

/**
 * 在渲染器中集成指令
 */
class Renderer {
  patch(oldVNode, newVNode, container) {
    // ... 其他patch逻辑
    
    if (oldVNode && newVNode && oldVNode.el === newVNode.el) {
      // 更新指令
      directiveManager.updateDirectives(oldVNode, newVNode);
    }
  }
  
  mountElement(vnode, container, anchor) {
    const el = document.createElement(vnode.type);
    vnode.el = el;
    
    // 在挂载前调用指令钩子
    directiveManager.applyDirectives(el, vnode);
    
    // ... 其他挂载逻辑
    
    // 挂载后调用 mounted
    directiveManager.invokeHook(el, 'mounted');
  }
  
  unmount(vnode) {
    const el = vnode.el;
    
    // 调用 beforeUnmount
    directiveManager.invokeHook(el, 'beforeUnmount', vnode);
    
    // ... 卸载逻辑
    
    // 调用 unmounted
    directiveManager.invokeHook(el, 'unmounted');
  }
}

内置指令的编译实现

常见内置指令

内置指令 编译处理 运行时 示例
v-if 转为条件表达式 条件渲染 <div v-if="show">
v-for 转为renderList 循环渲染 <li v-for="item in list">
v-model 拆分为value+事件 双向绑定 <input v-model="text">
v-show 转为style控制 切换display <div v-show="visible">
v-on 转为事件绑定 事件监听 <button @click="fn">
v-bind 转为属性绑定 属性更新 <div :class="cls">
自定义指令 保留指令信息 调用钩子 <div v-custom>

v-if 的编译

function transformVIf(node, dir, context) {
  // 将元素转换为条件节点
  node.type = 'Conditional';
  node.condition = dir.value;
  node.consequent = node;
  
  // 查找相邻的 v-else-if 和 v-else
  let current = node;
  while (current.next) {
    const nextNode = current.next;
    const elseDir = nextNode.directives?.find(d => d.name === 'else-if' || d.name === 'else');
    
    if (elseDir) {
      if (elseDir.name === 'else-if') {
        // 转换为条件分支
        current.alternate = {
          type: 'Conditional',
          condition: elseDir.value,
          consequent: nextNode
        };
        current = current.alternate;
      } else {
        // v-else
        current.alternate = nextNode;
      }
      
      // 移除指令标记
      nextNode.directives = nextNode.directives?.filter(d => d.name !== 'else-if' && d.name !== 'else');
    } else {
      break;
    }
  }
}

/**
 * 生成 v-if 代码
 */
function genVIf(node) {
  if (node.type !== 'Conditional') return;
  
  let code = `ctx.${node.condition} ? `;
  code += genNode(node.consequent);
  code += ' : ';
  
  if (node.alternate) {
    if (node.alternate.type === 'Conditional') {
      code += genVIf(node.alternate);
    } else {
      code += genNode(node.alternate);
    }
  } else {
    code += 'null';
  }
  
  return code;
}

v-show 的编译

function transformVShow(node, dir, context) {
  // v-show 只是添加 style 控制
  if (!node.props) node.props = [];
  
  const styleProp = node.props.find(p => p.name === 'style');
  
  if (styleProp) {
    // 合并现有 style
    styleProp.value = `[${styleProp.value}, ctx.${dir.value} ? null : { display: 'none' }]`;
  } else {
    // 添加 style 属性
    node.props.push({
      name: 'style',
      value: `ctx.${dir.value} ? null : { display: 'none' }`
    });
  }
  
  // 移除 v-show 指令
  node.directives = node.directives?.filter(d => d.name !== 'show');
}

/**
 * 生成 v-show 代码(在 props 中体现)
 */
function genVShow(node) {
  // v-show 已经在 props 中处理,这里不需要额外生成
  return genNode(node);
}

v-model 的编译

function transformVModel(node, dir, context) {
  const value = dir.value;
  const modifiers = dir.modifiers || [];
  
  // 根据元素类型生成不同的事件和属性
  let propName = 'modelValue';
  let eventName = 'onUpdate:modelValue';
  
  if (node.tag === 'input') {
    if (modifiers.includes('number')) {
      // v-model.number
      return genNumberModel(value);
    } else if (modifiers.includes('trim')) {
      // v-model.trim
      return genTrimModel(value);
    }
  } else if (node.tag === 'select') {
    propName = 'modelValue';
    eventName = 'onUpdate:modelValue';
  } else if (node.tag === 'textarea') {
    propName = 'modelValue';
    eventName = 'onUpdate:modelValue';
  }
  
  // 添加 props
  if (!node.props) node.props = [];
  
  // 添加 value 绑定
  node.props.push({
    name: propName,
    value: `ctx.${value}`
  });
  
  // 添加事件绑定
  node.props.push({
    name: eventName,
    value: genUpdateHandler(value, modifiers)
  });
}

/**
 * 生成更新处理器
 */
function genUpdateHandler(value, modifiers) {
  let handler = `$event => ctx.${value} = $event`;
  
  if (modifiers.includes('number')) {
    handler = `$event => ctx.${value} = parseFloat($event)`;
  } else if (modifiers.includes('trim')) {
    handler = `$event => ctx.${value} = $event.trim()`;
  }
  
  if (modifiers.includes('lazy')) {
    handler = handler.replace('$event', '$event.target.value');
  }
  
  return handler;
}

/**
 * 生成数字输入模型
 */
function genNumberModel(value) {
  return {
    type: 'Directive',
    name: 'bind',
    arg: 'value',
    value: `ctx.${value}`
  }, {
    type: 'Directive',
    name: 'on',
    arg: 'input',
    value: `$event => ctx.${value} = $event.target.value ? parseFloat($event.target.value) : ''`
  };
}

/**
 * 生成修剪模型
 */
function genTrimModel(value) {
  return {
    type: 'Directive',
    name: 'bind',
    arg: 'value',
    value: `ctx.${value}`
  }, {
    type: 'Directive',
    name: 'on',
    arg: 'blur',
    value: `$event => ctx.${value} = $event.target.value.trim()`
  };
}

v-for 的编译

function transformVFor(node, dir, context) {
  // 解析 v-for 表达式 "item in list"
  const match = dir.value.match(/(.*?) in (.*)/);
  if (!match) return;
  
  const [, alias, source] = match;
  
  // 转换为 For 节点
  node.type = 'For';
  node.source = source.trim();
  node.alias = alias.trim();
  node.children = node.children || [];
  
  // 添加 key 处理
  const keyProp = node.props?.find(p => p.name === 'key' || p.name === ':key');
  if (!keyProp) {
    // 自动添加 key 建议
    console.warn('v-for 应该提供 key 属性');
  }
  
  // 移除 v-for 指令
  node.directives = node.directives?.filter(d => d.name !== 'for');
}

/**
 * 生成 v-for 代码
 */
function genVFor(node) {
  if (node.type !== 'For') return;
  
  const { source, alias, children } = node;
  
  return `renderList(ctx.${source}, (${alias}, index) => {
    return ${genNode(children[0])}
  })`;
}

自定义指令的编译处理

自定义指令的保留

/**
 * 处理自定义指令
 */
function transformCustomDirective(node, context) {
  if (!node.directives) return;
  
  // 保留自定义指令,运行时处理
  node.customDirectives = node.directives.filter(dir => {
    return !['if', 'for', 'model', 'show', 'on', 'bind'].includes(dir.name);
  });
  
  // 移除已处理的指令
  node.directives = node.directives.filter(dir => {
    return ['if', 'for', 'model', 'show', 'on', 'bind'].includes(dir.name);
  });
}

/**
 * 生成自定义指令代码
 */
function genCustomDirectives(node, context) {
  if (!node.customDirectives?.length) return '';
  
  const dirs = node.customDirectives.map(dir => {
    const { name, arg, modifiers, value } = dir;
    
    return {
      name: `'${name}'`,
      value: `() => ${value}`,
      arg: arg ? `'${arg}'` : 'null',
      modifiers: JSON.stringify(modifiers || {})
    };
  });
  
  return `directives: [${dirs.map(d => 
    `{${Object.entries(d).map(([k, v]) => `${k}: ${v}`).join(', ')}}`
  ).join(', ')}]`;
}

指令的参数和修饰符

/**
 * 解析指令参数和修饰符
 */
function parseDirective(name) {
  // 例如:v-on:click.prevent.stop
  const parts = name.split(':');
  const dirName = parts[0];
  
  let arg = parts[1] || '';
  let modifiers = [];
  
  // 解析修饰符
  if (arg.includes('.')) {
    const argParts = arg.split('.');
    arg = argParts[0];
    modifiers = argParts.slice(1);
  }
  
  return {
    name: dirName,
    arg,
    modifiers
  };
}

/**
 * 生成修饰符处理代码
 */
function genModifiers(modifiers) {
  const obj = {};
  for (const mod of modifiers) {
    obj[mod] = true;
  }
  return JSON.stringify(obj);
}

事件修饰符的实现

常用事件修饰符

通用事件修饰符

修饰符 作用 典型使用场景
.stop 阻止事件冒泡。 防止点击一个内部的按钮意外触发了外层容器的点击事件。
.prevent 阻止事件的默认行为。 自定义表单提交逻辑,或自定义链接行为。
.capture 使用事件捕获模式。 当你希望父元素能比子元素更早地捕获到事件时使用。
.self 只有当 event.target 是当前元素自身时,才触发事件处理函数。 严格区分是点击了元素本身还是其内部子元素的场景。
.once 事件将只会触发一次。 一次性操作,如首次点击的引导、支付按钮等,防止重复提交。
.passive 告诉浏览器你不想阻止事件的默认行为,从而提升性能。尤其适用于移动端的滚动事件(touchmove),能让滚动更流畅。 提升滚动性能,通常用于改善移动端设备的滚屏体验。

注:修饰符可以串联使用,比如 @click.stop.prevent 会同时阻止冒泡和默认行为。但需要注意顺序,因为相关代码会按顺序生成。

按键修饰符

按键修饰符专门用于监听键盘事件,方便监听按下了哪个键。Vue 为最常用的按键提供了别名,我们可以直接使用:

  • .enter (回车键)
  • .tab (制表键)
  • .delete (捕获“删除”和“退格”键)
  • .esc (退出键)
  • .space (空格键)
  • .up / .down / .left / .right (方向键)

鼠标按键修饰符

指定由特定鼠标按键触发的事件:

  • .left (鼠标左键)
  • .right (鼠标右键)
  • .middle (鼠标滚轮键)

运行时的事件处理

/**
 * 运行时事件绑定处理
 */
class EventManager {
  constructor() {
    this.eventHandlers = new WeakMap();
  }
  
  /**
   * 绑定事件
   */
  addEventListener(el, eventName, handler, options) {
    // 解析事件选项
    let useCapture = false;
    let isPassive = false;
    
    if (eventName.includes('!')) {
      useCapture = true;
      eventName = eventName.replace('!', '');
    }
    
    if (eventName.includes('~')) {
      isPassive = true;
      eventName = eventName.replace('~', '');
    }
    
    const eventOptions = {
      capture: useCapture,
      passive: isPassive
    };
    
    // 存储事件处理器
    if (!this.eventHandlers.has(el)) {
      this.eventHandlers.set(el, new Map());
    }
    
    const handlers = this.eventHandlers.get(el);
    handlers.set(eventName, { handler, options: eventOptions });
    
    // 绑定事件
    el.addEventListener(eventName, handler, eventOptions);
  }
  
  /**
   * 更新事件
   */
  updateEventListener(el, eventName, newHandler) {
    const handlers = this.eventHandlers.get(el);
    if (!handlers) return;
    
    const old = handlers.get(eventName);
    if (old) {
      el.removeEventListener(eventName, old.handler, old.options);
    }
    
    if (newHandler) {
      this.addEventListener(el, eventName, newHandler.handler, newHandler.options);
    }
  }
}

手写实现:完整指令系统

/**
 * 完整指令编译器
 */
class DirectiveCompiler {
  constructor() {
    this.builtInDirectives = new Set(['if', 'for', 'model', 'show', 'on', 'bind']);
  }
  
  /**
   * 编译模板中的指令
   */
  compile(template) {
    // 1. 解析AST
    const ast = this.parse(template);
    
    // 2. 转换AST
    this.transform(ast);
    
    // 3. 生成代码
    const code = this.generate(ast);
    
    return code;
  }
  
  /**
   * 解析模板
   */
  parse(template) {
    // 简化的解析逻辑
    const ast = {
      type: 'Root',
      children: []
    };
    
    // 解析元素和指令
    const elementRegex = /<(\w+)([^>]*)>/g;
    const directiveRegex = /v-(\w+)(?::(\w+))?(?:\.(\w+))?="([^"]*)"/g;
    
    // ... 解析逻辑
    
    return ast;
  }
  
  /**
   * 转换AST
   */
  transform(node) {
    if (node.type === 'Element') {
      // 提取指令
      const directives = [];
      
      if (node.attributes) {
        for (const attr of node.attributes) {
          const match = attr.name.match(/^v-(\w+)(?::(\w+))?(?:\.([\w.]+))?$/);
          if (match) {
            const [_, name, arg, modifiersStr] = match;
            const modifiers = modifiersStr ? modifiersStr.split('.') : [];
            
            directives.push({
              name,
              arg,
              modifiers,
              value: attr.value,
              exp: {
                type: 'Expression',
                content: attr.value
              }
            });
            
            // 移除原始属性
            node.attributes = node.attributes.filter(a => a !== attr);
          }
        }
      }
      
      if (directives.length > 0) {
        node.directives = directives;
        
        // 处理内置指令
        for (const dir of directives) {
          if (this.builtInDirectives.has(dir.name)) {
            this.processBuiltInDirective(node, dir);
          }
        }
        
        // 保留自定义指令
        node.customDirectives = directives.filter(
          dir => !this.builtInDirectives.has(dir.name)
        );
      }
      
      // 递归处理子节点
      if (node.children) {
        for (const child of node.children) {
          this.transform(child);
        }
      }
    }
  }
  
  /**
   * 处理内置指令
   */
  processBuiltInDirective(node, dir) {
    switch (dir.name) {
      case 'if':
        this.processVIf(node, dir);
        break;
      case 'for':
        this.processVFor(node, dir);
        break;
      case 'model':
        this.processVModel(node, dir);
        break;
      case 'show':
        this.processVShow(node, dir);
        break;
      case 'on':
        this.processVOn(node, dir);
        break;
      case 'bind':
        this.processVBind(node, dir);
        break;
    }
  }
  
  /**
   * 处理 v-if
   */
  processVIf(node, dir) {
    node.type = 'Conditional';
    node.condition = dir.value;
    node.consequent = { ...node };
    delete node.consequent.directives;
    delete node.consequent.customDirectives;
  }
  
  /**
   * 处理 v-for
   */
  processVFor(node, dir) {
    const match = dir.value.match(/(.*?) in (.*)/);
    if (match) {
      node.type = 'For';
      node.alias = match[1].trim();
      node.source = match[2].trim();
      node.iterator = node;
      delete node.iterator.directives;
      delete node.iterator.customDirectives;
    }
  }
  
  /**
   * 处理 v-model
   */
  processVModel(node, dir) {
    if (!node.props) node.props = [];
    
    node.props.push({
      name: 'modelValue',
      value: `ctx.${dir.value}`
    });
    
    node.props.push({
      name: 'onUpdate:modelValue',
      value: this.genUpdateHandler(dir)
    });
  }
  
  /**
   * 处理 v-show
   */
  processVShow(node, dir) {
    if (!node.props) node.props = [];
    
    const styleProp = node.props.find(p => p.name === 'style');
    if (styleProp) {
      styleProp.value = `[${styleProp.value}, ctx.${dir.value} ? null : { display: 'none' }]`;
    } else {
      node.props.push({
        name: 'style',
        value: `ctx.${dir.value} ? null : { display: 'none' }`
      });
    }
  }
  
  /**
   * 处理 v-on
   */
  processVOn(node, dir) {
    if (!node.props) node.props = [];
    
    const eventName = dir.arg;
    let handler = `ctx.${dir.value}`;
    
    // 应用修饰符
    if (dir.modifiers) {
      handler = this.applyModifiers(handler, dir.modifiers);
    }
    
    node.props.push({
      name: `on${eventName.charAt(0).toUpperCase() + eventName.slice(1)}`,
      value: handler
    });
  }
  
  /**
   * 处理 v-bind
   */
  processVBind(node, dir) {
    if (!node.props) node.props = [];
    
    node.props.push({
      name: dir.arg,
      value: `ctx.${dir.value}`
    });
  }
  
  /**
   * 应用修饰符
   */
  applyModifiers(handler, modifiers) {
    for (const mod of modifiers) {
      switch (mod) {
        case 'stop':
          handler = `$event => { $event.stopPropagation(); ${handler}($event) }`;
          break;
        case 'prevent':
          handler = `$event => { $event.preventDefault(); ${handler}($event) }`;
          break;
        case 'once':
          handler = `once(${handler})`;
          break;
      }
    }
    return handler;
  }
  
  /**
   * 生成更新处理器
   */
  genUpdateHandler(dir) {
    let handler = `$event => ctx.${dir.value} = $event`;
    
    if (dir.modifiers) {
      if (dir.modifiers.includes('number')) {
        handler = `$event => ctx.${dir.value} = parseFloat($event)`;
      }
      if (dir.modifiers.includes('trim')) {
        handler = `$event => ctx.${dir.value} = $event.trim()`;
      }
      if (dir.modifiers.includes('lazy')) {
        handler = handler.replace('$event', '$event.target.value');
      }
    }
    
    return handler;
  }
  
  /**
   * 生成代码
   */
  generate(node) {
    if (!node) return 'null';
    
    switch (node.type) {
      case 'Root':
        return this.generateRoot(node);
      case 'Element':
        return this.generateElement(node);
      case 'Conditional':
        return this.generateConditional(node);
      case 'For':
        return this.generateFor(node);
      default:
        return 'null';
    }
  }
  
  /**
   * 生成元素代码
   */
  generateElement(node) {
    const parts = ['createVNode'];
    
    // 标签
    parts.push(`'${node.tag}'`);
    
    // 属性
    if (node.props) {
      const propsObj = {};
      for (const prop of node.props) {
        propsObj[prop.name] = prop.value;
      }
      parts.push(JSON.stringify(propsObj));
    } else {
      parts.push('null');
    }
    
    // 子节点
    if (node.children) {
      const children = node.children.map(child => this.generate(child));
      if (children.length === 1) {
        parts.push(children[0]);
      } else {
        parts.push(`[${children.join(', ')}]`);
      }
    } else {
      parts.push('null');
    }
    
    // 自定义指令
    if (node.customDirectives?.length) {
      const dirs = node.customDirectives.map(dir => ({
        name: `'${dir.name}'`,
        value: `() => ${dir.value}`,
        arg: dir.arg ? `'${dir.arg}'` : 'null',
        modifiers: JSON.stringify(dir.modifiers || {})
      }));
      
      parts.push(JSON.stringify({
        directives: dirs
      }));
    }
    
    return `createVNode(${parts.join(', ')})`;
  }
  
  /**
   * 生成条件节点
   */
  generateConditional(node) {
    return `${node.condition} ? ${this.generate(node.consequent)} : null`;
  }
  
  /**
   * 生成循环节点
   */
  generateFor(node) {
    return `renderList(ctx.${node.source}, (${node.alias}, index) => ${this.generate(node.iterator)})`;
  }
}

结语

理解指令系统,不仅帮助我们更好地使用内置指令,也能创建强大的自定义指令,提升开发效率。指令系统是 Vue 声明式编程的重要体现,它将 DOM 操作封装成声明式的语法,让开发者可以专注于业务逻辑。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

AST转换:静态提升与补丁标志

在上一篇文章中,我们学习了模板编译的三个阶段。今天,我们将深入AST转换阶段的核心优:静态提升补丁标志。这两个优化是 Vue3 性能大幅提升的关键,它们让 Vue 在运行时能够跳过大量不必要的比较,实现精准更新。

前言:从一次渲染说起

想象一下,我们正在读一本电子书:这其中 99% 的内容是固定的,只有 1% 的页码会变化,这时候我们会怎么做:

  • 普通方式:每次变化时,重读整本书(Vue2的方式)
  • 优化方式:只重新读变化的页码(Vue3的方式)

这就是静态提升和补丁标志的核心思想:标记不变的内容,跳过重复工作。

静态节点标记(PatchFlags)

什么是补丁标志?

补丁标志是一个位掩码,用来标记节点的动态内容类型。它告诉渲染器:这个节点哪些部分是需要关注的变化点。

Vue3 中定义了丰富的补丁标志:

const PatchFlags = {
  TEXT: 1,                    // 动态文本内容
  CLASS: 1 << 1,              // 动态 class
  STYLE: 1 << 2,              // 动态 style
  PROPS: 1 << 3,              // 动态属性
  FULL_PROPS: 1 << 4,         // 全量比较
  HYDRATE_EVENTS: 1 << 5,     // 事件监听
  STABLE_FRAGMENT: 1 << 6,    // 稳定 Fragment
  KEYED_FRAGMENT: 1 << 7,     // 带 key 的 Fragment
  UNKEYED_FRAGMENT: 1 << 8,   // 无 key 的 Fragment
  NEED_PATCH: 1 << 9,         // 需要非 props 比较
  DYNAMIC_SLOTS: 1 << 10,     // 动态插槽
  
  HOISTED: -1,                // 静态提升节点
  BAIL: -2                    // 退出优化
};

位掩码的作用

位掩码可以用一个数字表示多个标记,以上述补丁标志为例,如果一个节点既有动态 class,又有动态 style,该怎么处理:

// 组合标记:class和style都是动态的
const combined = CLASS | STYLE;  // 110 = 6

动态内容的识别

编译器是如何识别哪些内容是动态的?其实编译器也是根据补丁标志来进行判断处理的,例如以下模板示例:

<div 
  class="static" 
  :class="dynamicClass"
  :style="dynamicStyle"
  id="static-id"
>
  <h1>静态标题</h1>
  <p>{{ dynamicText }}</p>
  <button @click="handler">点击</button>
</div>

通过编译后的标记:

// 编译后的标记
function render(ctx) {
  return createVNode('div', {
    class: ['static', ctx.dynamicClass],  // class部分是动态的
    style: ctx.dynamicStyle,               // style是动态的
    id: 'static-id'                        // id是静态的
  }, [
    createVNode('h1', null, '静态标题'),    // 完全静态
    createVNode('p', null, ctx.dynamicText, PatchFlags.TEXT),  // 只有文本动态
    createVNode('button', { 
      onClick: ctx.handler 
    }, '点击', PatchFlags.EVENTS)           // 只有事件动态
  ], PatchFlags.CLASS | PatchFlags.STYLE);  // div的class和style动态
}

如果没有标记,说明是静态节点,什么都不用做。

静态提升(HoistStatic)

静态提升的原理

静态提升是将完全静态的节点提取到渲染函数之外,避免每次渲染都重新创建,还是以上一节的代码为例:

const _hoisted_1 = createVNode('h1', null, '静态标题', PatchFlags.HOISTED);

function render(ctx) {
  return createVNode('div', null, [
    _hoisted_1,  // 直接复用
    createVNode('p', null, ctx.dynamicText, PatchFlags.TEXT)
  ]);
}

静态节点的判定规则

当一个节点同时满足以下条件时,这时我们就判定它为静态节点

  1. 没有动态绑定:不存在双向绑定 v-model(简写:)、v-bindv-on
  2. 没有指令:不存在 v-ifv-forv-slot 等指令
  3. 没有插值:不存在 {{ }} 等插值语句
  4. 所有子节点也都是静态的

静态提升的深度

Vue3 不仅提升顶层静态节点,还会提升深层静态节点:

<template>
  <div>
    <div>  <!-- 这个div不是静态的,因为它有动态子节点 -->
      <span>完全静态</span>  <!-- 但这个span是静态的,会被提升 -->
      <span>{{ text }}</span>
    </div>
    <div class="static">  <!-- 这个div是静态的,会被提升 -->
      <span>静态1</span>
      <span>静态2</span>
    </div>
  </div>
</template>

动态节点收集

Block的概念

Block 是Vue3中一个重要的优化概念,它会收集当前模板中的所有动态节点。通常情况下,我们会约定组件模版的根节点作为 Block 角色,从根节点开始,所有动态子代节点都会被收集到根节点的 dynamicChildren 数组中,以此来形成一颗 Block Tree

到了这里,也许会有人问:如果我的 Vue 组件模板中,都是静态节点,不存在动态节点呢? 这种情况也是存在的,这种情况下,就只存在根节点一个 Block,无法形成树,因此也不用额外处理。

Block Tree

Block 会收集所有后代动态节点,形成动态节点树 Block Tree。我们来看下面一个模板代码示例:

<div>  <!-- 这是Block -->
  <span>静态</span>
  <p :class="dynamic">动态1</p>
  <div>
    <span>静态</span>
    <span>{{ text }}</span>  <!-- 动态2 -->
  </div>
</div>

这段代码完整转成树形结构应该是这样的: 完整树形结构 只收集动态节点,形成的动态节点树: 动态节点树结构

更新时的优化

有了动态节点树,更新时只需要遍历 dynamicChildren

function patchChildren(oldNode, newNode, container) {
  if (newNode.dynamicChildren) {
    // 只更新动态节点
    for (let i = 0; i < newNode.dynamicChildren.length; i++) {
      patch(
        oldNode.dynamicChildren[i],
        newNode.dynamicChildren[i],
        container
      );
    }
  } else {
    // 没有动态节点,说明是完全静态,什么都不用做
  }
}

节点转换器的设计

转换器的整体架构

/**
 * AST转换器
 */
class ASTTransformer {
  constructor(ast, options = {}) {
    this.ast = ast;
    this.options = options;
    this.context = {
      currentNode: null,
      parent: null,
      staticNodes: new Set(),
      dynamicNodes: new Set(),
      patchFlags: new Map(),
      hoisted: [],        // 提升的静态节点
      replaceNode: (node) => {
        // 替换当前节点
      },
      removeNode: () => {
        // 删除当前节点
      }
    };
  }
  
  /**
   * 执行转换
   */
  transform() {
    // 1. 遍历AST,标记静态节点
    this.traverse(this.ast);
    
    // 2. 计算补丁标志
    this.computePatchFlags();
    
    // 3. 提取静态节点
    this.hoistStatic();
    
    return this.ast;
  }
  
  /**
   * 遍历AST
   */
  traverse(node, parent = null) {
    if (!node) return;
    
    this.context.currentNode = node;
    this.context.parent = parent;
    
    // 应用所有转换插件
    for (const plugin of this.plugins) {
      plugin(node, this.context);
    }
    
    // 递归处理子节点
    if (node.children) {
      for (const child of node.children) {
        this.traverse(child, node);
      }
    }
  }
}

静态节点检测插件

/**
 * 静态节点检测插件
 */
const detectStaticPlugin = (node, context) => {
  if (node.type === 'Element') {
    // 检查是否有动态绑定
    const hasDynamic = checkDynamic(node);
    
    if (!hasDynamic) {
      // 检查所有子节点
      const childrenStatic = node.children?.every(child => 
        context.staticNodes.has(child) || child.type === 'Text'
      ) ?? true;
      
      if (childrenStatic) {
        context.staticNodes.add(node);
        node.isStatic = true;
      }
    }
  } else if (node.type === 'Text') {
    // 文本节点默认是静态的
    node.isStatic = true;
  }
};

/**
 * 检查节点是否包含动态内容
 */
function checkDynamic(node) {
  if (!node.props) return false;
  
  for (const prop of node.props) {
    // 检查指令
    if (prop.name.startsWith('v-') || prop.name.startsWith('@') || prop.name.startsWith(':')) {
      return true;
    }
    
    // 检查动态属性值
    if (prop.value && prop.value.includes('{{')) {
      return true;
    }
  }
  
  return false;
}

补丁标志计算插件

/**
 * 补丁标志计算插件
 */
const patchFlagPlugin = (node, context) => {
  if (node.type !== 'Element' || node.isStatic) return;
  
  let patchFlag = 0;
  const dynamicProps = [];
  
  if (node.props) {
    for (const prop of node.props) {
      if (prop.name === 'class' && isDynamic(prop)) {
        patchFlag |= PatchFlags.CLASS;
        dynamicProps.push('class');
      } else if (prop.name === 'style' && isDynamic(prop)) {
        patchFlag |= PatchFlags.STYLE;
        dynamicProps.push('style');
      } else if (prop.name.startsWith('@')) {
        patchFlag |= PatchFlags.EVENTS;
        dynamicProps.push(prop.name.slice(1));
      } else if (prop.name.startsWith(':')) {
        patchFlag |= PatchFlags.PROPS;
        dynamicProps.push(prop.name.slice(1));
      }
    }
  }
  
  // 检查文本内容
  if (node.children) {
    for (const child of node.children) {
      if (child.type === 'Interpolation') {
        patchFlag |= PatchFlags.TEXT;
        break;
      }
    }
  }
  
  if (patchFlag) {
    node.patchFlag = patchFlag;
    node.dynamicProps = dynamicProps;
    context.dynamicNodes.add(node);
  }
};

/**
 * 判断属性是否为动态
 */
function isDynamic(prop) {
  return prop.value && (
    prop.value.includes('{{') ||
    prop.value.startsWith('_ctx.') ||
    prop.value.includes('$event')
  );
}

静态提升插件

/**
 * 静态提升插件
 */
const hoistStaticPlugin = (node, context) => {
  if (node.type === 'Element' && node.isStatic) {
    // 生成唯一的变量名
    const hoistName = `_hoisted_${context.hoisted.length + 1}`;
    
    // 存储到提升列表
    context.hoisted.push({
      name: hoistName,
      node: node
    });
    
    // 替换为变量引用
    const replacement = {
      type: 'HoistReference',
      name: hoistName,
      original: node
    };
    
    context.replaceNode(replacement);
  }
};

/**
 * 生成提升的代码
 */
function generateHoisted(hoisted) {
  let code = '';
  
  for (const { name, node } of hoisted) {
    code += `\nconst ${name} = createVNode(`;
    code += `'${node.tag}', `;
    code += generateProps(node.props);
    code += `, ${generateChildren(node.children)}`;
    code += `, PatchFlags.HOISTED);\n`;
  }
  
  return code;
}

常量提升原理

常量的识别

除了静态节点外,常量表达式也会被提升,我们来看下面一个模板示例:

<div>
  <p>{{ 1 + 2 }}</p>  <!-- 常量表达式 -->
</div>

{{ 1 + 2 }} 是一个常量表达式,它在编译时,也会提升:

const _hoisted_1 = 1 + 2;  // 常量表达式提升

function render(ctx) {
  return createVNode('div', null, [
    createVNode('p', null, _hoisted_1, PatchFlags.TEXT),
    createVNode('p', null, ctx.message, PatchFlags.TEXT)
  ]);
}

常量检测的实现

/**
 * 常量检测插件
 */
const constantDetectPlugin = (node, context) => {
  if (node.type === 'Interpolation') {
    // 检查表达式是否为常量
    if (isConstantExpression(node.content)) {
      node.isConstant = true;
      
      // 生成常量名
      const constantName = `_constant_${context.constants.length + 1}`;
      context.constants.push({
        name: constantName,
        value: node.content
      });
      
      // 替换为常量引用
      context.replaceNode({
        type: 'ConstantReference',
        name: constantName
      });
    }
  }
};

/**
 * 判断表达式是否为常量
 */
function isConstantExpression(expr) {
  // 简单判断:只包含字面量和算术运算符
  const constantPattern = /^[\d\s\+\-\*\/\(\)]+$/;
  return constantPattern.test(expr);
}

缓存内联事件处理函数

事件处理函数的问题

在 JavaScript 中,每次重新渲染都会创建新的函数,如以下模板示例:

<template>
  <button @click="() => count++">点击</button>
</template>

在每次渲染时,都会创建新函数:

function render(ctx) {
  return createVNode('button', {
    onClick: () => ctx.count++  // 每次都不同
  }, '点击');
}

这么处理会有什么问题呢?在每次渲染时,都会为 button 创建一个全新的事件处理对象,里面的 onClick 也会是一个全新的函数。这就会导致渲染器每次渲染都会进行一次更新,造成额外的性能浪费。

事件缓存机制

为了解决上述问题,Vue3 采用了事件缓存机制,对内联事件处理函数进行缓存:

function render(ctx, _cache) {
  return createVNode('button', {
    onClick: _cache[0] || (_cache[0] = ($event) => ctx.count++)
  }, '点击');
}

缓存插件的实现

/**
 * 事件缓存插件
 */
const cacheEventHandlerPlugin = (node, context) => {
  if (node.type === 'Element' && node.props) {
    let cacheIndex = 0;
    
    for (let i = 0; i < node.props.length; i++) {
      const prop = node.props[i];
      
      if (prop.name.startsWith('@') || prop.name === 'onClick') {
        // 生成缓存代码
        const eventName = prop.name.replace(/^@|^on/, '').toLowerCase();
        const handler = prop.value;
        
        prop.cached = true;
        prop.cacheIndex = cacheIndex++;
        prop.cachedCode = `_cache[${prop.cacheIndex}] || (_cache[${prop.cacheIndex}] = $event => ${handler})`;
      }
    }
  }
};

/**
 * 生成事件缓存代码
 */
function generateEventCode(node, context) {
  if (!node.props) return 'null';
  
  const propsObj = {};
  
  for (const prop of node.props) {
    if (prop.cached) {
      // 使用缓存
      propsObj[prop.name] = prop.cachedCode;
    } else {
      // 普通属性
      propsObj[prop.name] = prop.value;
    }
  }
  
  return JSON.stringify(propsObj);
}

结语

静态提升和补丁标志是 Vue3 性能优化的两大法宝,它们让 Vue 能够在运行时精准地只更新变化的部分。理解这些优化,不仅帮助我们写出更高效的代码,也让我们对 Vue 的设计哲学有更深的理解。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

双端 Diff 算法详解

在上一篇文章中,我们学习了 Diff 算法的基础原理和 key 的重要性。今天,我们将深入 Vue2 中经典的双端比较算法——这个算法通过四个指针的巧妙移动,实现了高效的节点更新。理解这个算法,不仅有助于掌握Vue2的diff原理,也为理解 Vue3 的更优算法打下基础。

前言:为什么需要双端比较?

我们还是以积木为例,假如我们有这样一排积木:

A B C D

然后我们想把它变成这样:

D A B C

也就是仅仅把 D 提到 A 的前面,如果我们用上一篇文章学的简单 Diff 算法,会怎么做呢?

  1. 比较位置0:A vs D,节点不同,更新为 D
  2. 比较位置1:B vs A,节点不同,更新为 A
  3. 比较位置2:C vs B,节点不同,更新为 B
  4. 比较位置3:D vs C,节点不同,更新为 C

上述 4 次更新操作中,没有复用任何节点。但实际上,这些节点除了顺序变化外,内容根本没有变。我们其实只需要通过移动 DOM 就复用它们,而且只需要移动一次(把 D 移动到 A 前面),就可以达到我们想要的效果。

双端 Diff 的核心思想

四个指针的设计

双端 Diff 算法在旧子节点数组和新子节点数组的两端各设置两个指针:

// 四个指针
let oldStartIdx = 0;              // 旧节点起始索引
let oldEndIdx = oldChildren.length - 1;   // 旧节点结束索引
let newStartIdx = 0;              // 新节点起始索引
let newEndIdx = newChildren.length - 1;    // 新节点结束索引

// 对应的节点
let oldStartVNode = oldChildren[oldStartIdx];
let oldEndVNode = oldChildren[oldEndIdx];
let newStartVNode = newChildren[newStartIdx];
let newEndVNode = newChildren[newEndIdx];

这四个指针的布局如图所示: 四个指针布局图

四种比较情况

双端比较的核心是进行四种比较:

1. 旧开始 vs 新开始

if (isSameVNodeType(oldStartVNode, newStartVNode)) {
  // 节点相同,直接复用
  patch(oldStartVNode, newStartVNode);
  oldStartIdx++;
  newStartIdx++;
}

2. 旧结束 vs 新结束

if (isSameVNodeType(oldEndVNode, newEndVNode)) {
  // 节点相同,直接复用
  patch(oldEndVNode, newEndVNode);
  oldEndIdx--;
  newEndIdx--;
}

3. 旧开始 vs 新结束

if (isSameVNodeType(oldStartVNode, newEndVNode)) {
  // 节点相同,但位置不同,需要移动
  patch(oldStartVNode, newEndVNode);
  // 将旧开始节点移动到旧结束节点之后
  insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling);
  oldStartIdx++;
  newEndIdx--;
}

4. 旧结束 vs 新开始

if (isSameVNodeType(oldEndVNode, newStartVNode)) {
  // 节点相同,但位置不同,需要移动
  patch(oldEndVNode, newStartVNode);
  // 将旧结束节点移动到旧开始节点之前
  insertBefore(oldEndVNode.el, oldStartVNode.el);
  oldEndIdx--;
  newStartIdx++;
}

通过 key 查找复用

为什么需要key查找?

当四种指标的比较都不匹配时,即非理想状况下,说明节点位置发生了较大变化。这时就需要通过 key 在旧节点中查找可复用的节点,如以下示例:

旧: A - B - C - D
新: C - A - D - B

第1轮比较时,四种指针比较都不匹配。这时就需要通过 key 查找,查找新开始节点 C 在旧节点中的位置,找到位置 2,就移动旧节点的 C 到开始位置。

// 在循环开始前建立key索引表
const keyToOldIndexMap = new Map();
for (let i = 0; i < oldChildren.length; i++) {
  const child = oldChildren[i];
  if (child.key != null) {
    keyToOldIndexMap.set(child.key, i);
  }
}

// 在四种比较都不匹配时使用
const idxInNew = keyToOldIndexMap.get(oldStartVNode.key);
if (idxInNew !== undefined) {
  // 找到了可复用的节点
  const vnodeToMove = newChildren[idxInNew];
  patch(oldStartVNode, vnodeToMove, container);
  // 移动节点
  container.insertBefore(oldStartVNode.el, oldStartVNode.el);
  // 标记该位置已处理
  newChildren[idxInNew] = undefined;
}

key查找的性能影响

场景 无key查找 有key查找 优势
头部插入 全量比较 直接定位 O(n) vs O(1)
节点移动 难以复用 精确复用 减少DOM操作
列表重排 性能差 性能优 差距可达10倍

完整的双端 Diff 实现

class DoubleEndedDiff {
  constructor(options = {}) {
    this.options = options;
  }
  
  /**
   * 执行双端比较
   */
  patchChildren(oldChildren, newChildren, container) {
    
    // 初始化指针
    let oldStartIdx = 0;
    let oldEndIdx = oldChildren.length - 1;
    let newStartIdx = 0;
    let newEndIdx = newChildren.length - 1;
    
    let oldStartVNode = oldChildren[oldStartIdx];
    let oldEndVNode = oldChildren[oldEndIdx];
    let newStartVNode = newChildren[newStartIdx];
    let newEndVNode = newChildren[newEndIdx];
    
    // 创建key索引表
    const keyToOldIndexMap = this.createKeyMap(oldChildren);
    
    // 记录移动次数
    let moveCount = 0;
    let patchCount = 0;
    let mountCount = 0;
    let unmountCount = 0;
    
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 跳过已处理的节点
      if (!oldStartVNode) {
        oldStartVNode = oldChildren[++oldStartIdx];
      } else if (!oldEndVNode) {
        oldEndVNode = oldChildren[--oldEndIdx];
      }
      // 情况1: 旧开始 = 新开始
      else if (this.isSameNode(oldStartVNode, newStartVNode)) {
        this.patch(oldStartVNode, newStartVNode, container);
        oldStartVNode = oldChildren[++oldStartIdx];
        newStartVNode = newChildren[++newStartIdx];
        patchCount++;
      }
      // 情况2: 旧结束 = 新结束
      else if (this.isSameNode(oldEndVNode, newEndVNode)) {
        this.patch(oldEndVNode, newEndVNode, container);
        oldEndVNode = oldChildren[--oldEndIdx];
        newEndVNode = newChildren[--newEndIdx];
        patchCount++;
      }
      // 情况3: 旧开始 = 新结束
      else if (this.isSameNode(oldStartVNode, newEndVNode)) {
        this.patch(oldStartVNode, newEndVNode, container);
        container.insertBefore(
          oldStartVNode.el,
          oldEndVNode.el.nextSibling
        );
        oldStartVNode = oldChildren[++oldStartIdx];
        newEndVNode = newChildren[--newEndIdx];
        moveCount++;
        patchCount++;
      }
      // 情况4: 旧结束 = 新开始
      else if (this.isSameNode(oldEndVNode, newStartVNode)) {
        this.patch(oldEndVNode, newStartVNode, container);
        container.insertBefore(
          oldEndVNode.el,
          oldStartVNode.el
        );
        oldEndVNode = oldChildren[--oldEndIdx];
        newStartVNode = newChildren[++newStartIdx];
        moveCount++;
        patchCount++;
      }
      // 情况5: 都不匹配,通过key查找
      else {
        const idxInOld = keyToOldIndexMap.get(newStartVNode.key);
        
        if (idxInOld !== undefined) {
          const vnodeToMove = oldChildren[idxInOld];
          this.patch(vnodeToMove, newStartVNode, container);
          container.insertBefore(
            vnodeToMove.el,
            oldStartVNode.el
          );
          oldChildren[idxInOld] = undefined;
          moveCount++;
          patchCount++;
        } else {
          this.mount(newStartVNode, container, oldStartVNode.el);
          mountCount++;
        }
        newStartVNode = newChildren[++newStartIdx];
      }
    
    // 处理剩余节点
    if (oldStartIdx > oldEndIdx) {
      for (let i = newStartIdx; i <= newEndIdx; i++) {
        const newVNode = newChildren[i];
        if (newVNode) {
          this.mount(newVNode, container, newChildren[newEndIdx + 1]?.el);
          mountCount++;
        }
      }
    } else if (newStartIdx > newEndIdx) {
      for (let i = oldStartIdx; i <= oldEndIdx; i++) {
        const oldVNode = oldChildren[i];
        if (oldVNode) {
          this.unmount(oldVNode);
          unmountCount++;
        }
      }
    }
  }
  
  /**
   * 创建key索引表
   */
  createKeyMap(children) {
    const map = new Map();
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      if (child?.key != null) {
        map.set(child.key, i);
      }
    }
    return map;
  }
  
  /**
   * 判断两个节点是否相同
   */
  isSameNode(n1, n2) {
    return n1 && n2 && n1.type === n2.type && n1.key === n2.key;
  }
  
  /**
   * 更新节点
   */
  patch(oldVNode, newVNode, container) {
    if (oldVNode.el) {
      newVNode.el = oldVNode.el;
      if (newVNode.children !== oldVNode.children) {
        newVNode.el.textContent = newVNode.children;
      }
    }
  }
  
  /**
   * 挂载新节点
   */
  mount(vnode, container, anchor) {
    const el = document.createElement(vnode.type);
    vnode.el = el;
    el.textContent = vnode.children;
    if (anchor) {
      container.insertBefore(el, anchor);
    } else {
      container.appendChild(el);
    }
  }
  
  /**
   * 卸载节点
   */
  unmount(vnode) {
    if (vnode.el && vnode.el.parentNode) {
      vnode.el.parentNode.removeChild(vnode.el);
    }
  }
}

源码对标:Vue2的双端 Diff

Vue2 的双端 Diff 算法实现位于 src/core/vdom/patch.js 中:

// Vue2源码中的双端比较(简化版)
function updateChildren(parentElm, oldCh, newCh) {
  let oldStartIdx = 0;
  let oldEndIdx = oldCh.length - 1;
  let newStartIdx = 0;
  let newEndIdx = newCh.length - 1;
  
  let oldStartVnode = oldCh[oldStartIdx];
  let oldEndVnode = oldCh[oldEndIdx];
  let newStartVnode = newCh[newStartIdx];
  let newEndVnode = newCh[newEndIdx];
  
  let oldKeyToIdx, idxInOld, vnodeToMove;
  
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx];
    } else if (newStartVnode == null) {
      newStartVnode = newCh[++newStartIdx];
    } else if (newEndVnode == null) {
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode);
      api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    } else if (sameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode);
      api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    } else {
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      }
      idxInOld = oldKeyToIdx[newStartVnode.key];
      if (isUndef(idxInOld)) {
        api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm);
      } else {
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode);
          oldCh[idxInOld] = undefined;
          api.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm);
        }
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
  
  if (oldStartIdx > oldEndIdx) {
    // 挂载剩余新节点
  } else if (newStartIdx > newEndIdx) {
    // 卸载剩余旧节点
  }
}

结语

双端比较算法是 Vue2 响应式系统的核心之一,理解它不仅能帮助我们写出更高效的代码,也为理解 Vue3 的更优算法打下基础。虽然 Vue3 采用了新的算法,但双端比较的思想仍然值得我们深入学习。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

Diff算法基础:同层比较与key的作用

在上一篇文章中,我们深入探讨了 patch 算法的完整实现。今天,我们将聚焦于 Diff 算法的核心思想——为什么需要它?它如何工作?key 又为什么如此重要?通过这篇文章,我们将彻底理解 Diff 算法的基础原理。

前言:从生活中的例子理解Diff

想象一下,假如我们有一排积木:

A B C D

然后我们想把它变成这样:

A C D B

这时,我们应该怎么做呢?

  • 方式一:全部推倒重来:移除所有,按照我们想要的顺序重新摆放

  • 方式二:只调整变化的部分:移动位置,替换积木,即:我们只需要调整 B C D 三块积木的位置即可。

很显然,方式二的做法更高效。这就是 Diff 算法的本质——找出最小化的更新方案。

为什么需要 Diff 算法?

没有 Diff 算法会怎样?

假设我们有一个简单的列表:

<!-- 旧列表 -->
<ul>
  <li>苹果</li>
  <li>香蕉</li>
  <li>橙子</li>
</ul>

<!-- 新列表(只改了最后一个) -->
<ul>
  <li>苹果</li>
  <li>香蕉</li>
  <li>橘子</li>
</ul>

上述两个列表中,新列表只改了最后一项数据,如果没有 Diff 算法,我们只能按照 前言 中的方式一处理:删除整个 ul,重新创建:

const oldUl = document.querySelector('ul');
oldUl.remove();

const newUl = document.createElement('ul');
newUl.innerHTML = `
  <li>苹果</li>
  <li>香蕉</li>
  <li>橘子</li>
`;
container.appendChild(newUl);

这种方式虽然可以解决问题,但存在很大的风险:

  1. 性能极差:即使只改一个字,也要重建整个 DOM 树
  2. 状态丢失:输入框内容、滚动位置都会丢失
  3. 浪费资源:创建了大量不必要的 DOM 节点

此时 Diff 算法的重要性就凸显出来了!

Diff 算法的目标

Diff 算法的核心目标可以概括为三点:

  1. 尽可能复用已有节点
  2. 只更新变化的部分
  3. 最小化 DOM 操作

还是以上述 ul 结构为例,理想中的 Diff 操作应该是:

  1. 更新第三个 li 的文本内容:将 <li>橙子</li> 替换成 <li>橘子</li>
  2. 其他节点完全复用,不作任何更改

传统 Diff 算法

function diff(oldList, newList){
  for(let i = 0; i < oldList.length; i++){
    for(let j = 0; j < newList.length; j++){
      if(oldList[i] === newList[j]){
        // 找到相同的节点,进行复用
        console.log('找到了相同的节点', oldList[i]);
        break;
      } else {
        // 没找到相同的节点,进行新增
        console.log('需要新增节点', newList[j]);
      }
    }
  }
}

上述代码的时间复杂度为:O(n²);如果再考虑到移动、删除、新增等操作,其时间复杂度可以达到:O(n³)。这显然是不合理的。

同层比较的核心思想

为了解决传统 Diff 算法的时间复杂度问题,Vue 团队通过两个关键思想,将 Diff 算法的时间复杂降低到了:O(n):

  1. 同层比较,即只比较同一层级的节点
  2. 类型相同,即不同类型节点直接替换

什么是同层比较?

同层比较的意思是:只比较同一层级的节点,不跨层级移动。 我们来看一个简单的例子: 同层比较 上图两个新旧 VNode 树中,对比过程是这样的: 同层比较示例图

为什么不跨层级比较?

我们可以再来一个更复杂的示例:

<!-- 旧列表 -->
<ul>
  <li>li-1</li>
  <li>li-2</li>
  <li>
    <span>
      <a>
        li-3
      </a>
    </span>
  </li>
</ul>

<!-- 新列表 -->
<ul>
  <li>li-1</li>
  <li>li-2</li>
  <li>
    <a>
      li-3
    </a>
  </li>
</ul>

假设新旧两个列表是这样的,如果支持跨层级比较和移动,那么上述列表应该进行如下操作:

  1. 发现旧列表中 a 标签位于 span 标签下,新列表中直接位于 li 标签下;
  2. 记录这个操作差异,保存 a 标签,删除 span 标签,再把 a 标签挂载到 li 标签下;
  3. 更新父子节点关系。

这种操作会让算法变得极其复杂,而且实际开发中,跨层级移动节点的情况非常罕见。所以 Vue 选择简化问题:如果节点跨层级了,就视为不同类型,直接替换。

function patch(oldVNode, newVNode) {
  // 如果节点类型不同,直接替换
  if (oldVNode.type !== newVNode.type) {
    unmount(oldVNode);
    mount(newVNode);
    return;
  }
  
  // 同类型节点,进行深度比较
  patchChildren(oldVNode, newVNode);
}

同层比较的优势

优势 说明 示例
算法简单 只需要比较同一层 树形结构简化为线性比较
性能可控 复杂度O(n) 1000个节点只需比较1000次
实现可靠 边界情况少 不需要处理复杂移动

key在节点复用中的作用

为什么需要key?

我们来看一个简单的代办列表:

<!-- 旧列表 -->
<li>学习Vue</li>
<li>写文章</li>
<li>休息一下</li>

<!-- 新列表(删除了中间项 写文章) -->
<li>学习Vue</li>
<li>休息一下</li>

如果没有 key,Vue 会如何进行 diff 比较呢:

  1. 比较位置0:都是"学习Vue",直接复用;
  2. 比较位置1:旧的是"写文章",新的是"休息一下" ,更新文本进行替换
  3. 比较位置2:旧的有"休息一下",新的没有,则删除

这样操作过程中,更新了一个 li 的文本,删除了一个 li 。 这个过程看起来是没有问题的,但是如果上述列表有状态呢?

<!-- 带输入框的列表 -->
<li>
  <input value="学习Vue" />
  学习Vue
</li>
<li>
  <input value="写文章" />
  写文章
</li>
<li>
  <input value="休息一下" />
  休息一下
</li>

<!-- 删除中间项后 -->
<li>
  <input value="学习Vue" />  <!-- 输入框内容被保留了 -->
  学习Vue
</li>
<li>
  <input value="休息一下" />  <!-- 这里会是"休息一下"吗? -->
  休息一下
</li>

这时候问题就出现了:输入框的内容被错误地复用了!由于没有 key 的情况下,Vue 只按位置比较,最后的实际结果是:

<li>
  <input value="学习Vue" />  <!-- 输入框内容被保留了 -->
  学习Vue
</li>
<li>
  <input value="写文章" />  <!-- label变成了"写文章" -->
  休息一下
</li>

这个例子也同样解释了为什么不推荐,或者说不能用 index 作为 key 的原因。正确的做法是使用唯一的、稳定的标识作为 key。

key的作用图解

key的作用可以这样理解: key的作用图解

手写实现:简单Diff算法

class SimpleDiff {
  constructor(options) {
    this.options = options;
  }
  
  /**
   * 执行diff更新
   * @param {Array} oldChildren 旧子节点数组
   * @param {Array} newChildren 新子节点数组
   * @param {HTMLElement} container 父容器
   */
  diff(oldChildren, newChildren, container) {
    // 1. 创建key到索引的映射(如果有key)
    const oldKeyMap = this.createKeyMap(oldChildren);
    const newKeyMap = this.createKeyMap(newChildren);
    
    // 2. 记录已处理的节点
    const processed = new Set();
    
    // 3. 第一轮:尝试复用有key的节点
    this.patchKeyedNodes(oldChildren, newChildren, oldKeyMap, newKeyMap, processed, container);
    
    // 4. 第二轮:处理剩余节点
    this.processRemainingNodes(oldChildren, newChildren, processed, container);
  }
  
  /**
   * 创建key到索引的映射
   */
  createKeyMap(children) {
    const map = new Map();
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      if (child.key != null) {
        map.set(child.key, i);
      }
    }
    return map;
  }
  
  /**
   * 处理有key的节点
   */
  patchKeyedNodes(oldChildren, newChildren, oldKeyMap, newKeyMap, processed, container) {
    // 遍历新节点
    for (let i = 0; i < newChildren.length; i++) {
      const newVNode = newChildren[i];
      
      // 如果新节点没有key,跳过第一轮处理
      if (newVNode.key == null) continue;
      
      // 尝试在旧节点中找相同key的节点
      const oldIndex = oldKeyMap.get(newVNode.key);
      
      if (oldIndex !== undefined) {
        const oldVNode = oldChildren[oldIndex];
        
        // 标记为已处理
        processed.add(oldIndex);
        
        // 执行patch更新
        this.patchVNode(oldVNode, newVNode, container);
      } else {
        // 没有找到对应key,说明是新增节点
        this.mountVNode(newVNode, container);
      }
    }
  }
  
  /**
   * 处理剩余节点
   */
  processRemainingNodes(oldChildren, newChildren, processed, container) {
    // 1. 卸载未处理的旧节点
    for (let i = 0; i < oldChildren.length; i++) {
      if (!processed.has(i)) {
        this.unmountVNode(oldChildren[i]);
      }
    }
    
    // 2. 挂载新节点中未处理的节点
    for (let i = 0; i < newChildren.length; i++) {
      const newVNode = newChildren[i];
      
      // 如果没有key或者key不在旧节点中,需要挂载
      if (newVNode.key == null) {
        this.mountVNode(newVNode, container);
      } else {
        const oldIndex = oldChildren.findIndex(old => old.key === newVNode.key);
        if (oldIndex === -1) {
          this.mountVNode(newVNode, container);
        }
      }
    }
  }
  
  /**
   * 更新节点
   */
  patchVNode(oldVNode, newVNode, container) {
    console.log(`更新节点: ${oldVNode.key || '无key'}`);
    
    // 复用DOM元素
    newVNode.el = oldVNode.el;
    
    // 更新属性
    this.updateProps(newVNode.el, oldVNode.props, newVNode.props);
    
    // 更新子节点
    if (newVNode.children !== oldVNode.children) {
      newVNode.el.textContent = newVNode.children;
    }
  }
  
  /**
   * 挂载新节点
   */
  mountVNode(vnode, container) {
    console.log(`挂载新节点: ${vnode.key || '无key'}`);
    
    // 创建DOM元素
    const el = document.createElement(vnode.type);
    vnode.el = el;
    
    // 设置属性
    this.updateProps(el, {}, vnode.props);
    
    // 设置内容
    if (vnode.children) {
      el.textContent = vnode.children;
    }
    
    // 插入到容器
    container.appendChild(el);
  }
  
  /**
   * 卸载节点
   */
  unmountVNode(vnode) {
    console.log(`卸载节点: ${vnode.key || '无key'}`);
    if (vnode.el && vnode.el.parentNode) {
      vnode.el.parentNode.removeChild(vnode.el);
    }
  }
  
  /**
   * 更新属性
   */
  updateProps(el, oldProps = {}, newProps = {}) {
    // 移除不存在的属性
    for (const key in oldProps) {
      if (!(key in newProps)) {
        el.removeAttribute(key);
      }
    }
    
    // 设置新属性
    for (const key in newProps) {
      if (oldProps[key] !== newProps[key]) {
        el.setAttribute(key, newProps[key]);
      }
    }
  }
}

// 创建VNode的辅助函数
function h(type, props = {}, children = '') {
  return {
    type,
    props,
    key: props.key,
    children,
    el: null
  };
}

结语

理解 Diff 算法的基础原理,就像掌握了Vue 更新 DOM 的"思维模式"。知道它如何思考、如何决策,才能写出与框架配合最好的代码。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

patch算法:新旧节点的比对与更新

在前面的文章中,我们深入探讨了虚拟 DOM 的创建和组件的挂载过程。当数据变化时,Vue 需要高效地更新 DOM。这个过程的核心就是 patch 算法——新旧虚拟 DOM 的比对与更新策略。本文将带你深入理解 Vue3 的 patch 算法,看看它如何以最小的代价完成 DOM 更新。

前言:为什么需要patch?

想象一下,你有一个展示用户列表的页面。当某个用户的名字改变时,我们会怎么做?

  • 粗暴方式:重新渲染整个列表(性能差)
  • 聪明方式:只更新那个改变的用户名(性能好)

patch 算法就是 Vue 采用的"聪明方式"。它的核心思想是:找出新旧 VNode 的差异,只更新变化的部分,而不是重新渲染整个 DOM 树:

patch 过程图

patch函数的核心逻辑

patch的整体架构

patch 函数是整个更新过程的总调度器,它根据节点类型分发到不同的处理函数:

function patch(oldVNode, newVNode, container, anchor = null) {
  // 如果是同一个引用,无需更新
  if (oldVNode === newVNode) return;
  
  // 如果类型不同,直接替换
  if (oldVNode && !isSameVNodeType(oldVNode, newVNode)) {
    unmount(oldVNode);
    oldVNode = null;
  }
  
  const { type, shapeFlag } = newVNode;
  
  // 根据类型分发处理
  switch (type) {
    case Text:
      processText(oldVNode, newVNode, container, anchor);
      break;
    case Comment:
      processComment(oldVNode, newVNode, container, anchor);
      break;
    case Fragment:
      processFragment(oldVNode, newVNode, container, anchor);
      break;
    case Static:
      processStatic(oldVNode, newVNode, container, anchor);
      break;
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(oldVNode, newVNode, container, anchor);
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(oldVNode, newVNode, container, anchor);
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        processTeleport(oldVNode, newVNode, container, anchor);
      } else if (shapeFlag & ShapeFlags.SUSPENSE) {
        processSuspense(oldVNode, newVNode, container, anchor);
      }
  }
}

patch 的分发流程图

patch的分发流程图

判断节点类型的关键:isSameVNodeType

function isSameVNodeType(n1, n2) {
  // 比较类型和key
  return n1.type === n2.type && n1.key === n2.key;
}

为什么需要key?

我们看看下面的例子:

<!-- 旧列表 -->
<li key="a">A</li>
<li key="b">B</li>
<li key="c">C</li>

<!-- 新列表 -->
<li key="a">A</li>
<li key="c">C</li>
<li key="b">B</li>

<!-- 有key: 只移动节点,不重新创建 -->
<!-- 无key: 全部重新创建,性能差 -->

不同类型节点的处理策略

文本节点的处理

文本节点是最简单的节点类型,处理逻辑也最直接:

function processText(oldVNode, newVNode, container, anchor) {
  if (oldVNode == null) {
    // 首次挂载
    const textNode = document.createTextNode(newVNode.children);
    newVNode.el = textNode;
    container.insertBefore(textNode, anchor);
  } else {
    // 更新
    const el = (newVNode.el = oldVNode.el);
    if (newVNode.children !== oldVNode.children) {
      // 只有文本变化时才更新
      el.nodeValue = newVNode.children;
    }
  }
}

文本节点更新过程

文本节点更新过程

注释节点的处理

注释节点基本不需要更新,因为用户通常不关心注释的变化:

function processComment(oldVNode, newVNode, container, anchor) {
  if (oldVNode == null) {
    const commentNode = document.createComment(newVNode.children);
    newVNode.el = commentNode;
    container.insertBefore(commentNode, anchor);
  } else {
    // 注释节点很少变化,直接复用
    newVNode.el = oldVNode.el;
  }
}

元素节点的处理

元素节点的更新是最复杂的,需要处理属性和子节点:

function processElement(oldVNode, newVNode, container, anchor) {
  if (oldVNode == null) {
    // 首次挂载
    mountElement(newVNode, container, anchor);
  } else {
    // 更新
    patchElement(oldVNode, newVNode);
  }
}

function patchElement(oldVNode, newVNode) {
  const el = (newVNode.el = oldVNode.el);
  
  // 1. 更新props
  patchProps(el, oldVNode.props, newVNode.props);
  
  // 2. 更新children
  patchChildren(oldVNode, newVNode, el);
}

function patchProps(el, oldProps, newProps) {
  oldProps = oldProps || {};
  newProps = newProps || {};
  
  // 移除旧props中不存在于新props的属性
  for (const key in oldProps) {
    if (!(key in newProps)) {
      patchProp(el, key, oldProps[key], null);
    }
  }
  
  // 添加或更新新props
  for (const key in newProps) {
    const old = oldProps[key];
    const next = newProps[key];
    if (old !== next) {
      patchProp(el, key, old, next);
    }
  }
}

子节点的比对策略

子节点的比对是 patch 算法中最复杂、也最关键的部分。Vue3 根据子节点的类型,采用不同的策略。

子节点类型组合的处理策略

下表总结了所有可能的子节点类型组合及对应的处理方式:

旧子节点 新子节点 处理策略 示例
文本 文本 直接替换文本内容 "old" → "new"
文本 数组 清空文本,挂载数组 "text" → [vnode1, vnode2]
文本 清空文本 "text" → null
数组 文本 卸载数组,设置文本 [vnode1, vnode2] → "text"
数组 数组 执行核心diff [a,b,c] → [a,d,e]
数组 卸载所有子节点 [a,b,c] → null
文本 设置文本 null → "text"
数组 挂载数组 null → [a,b,c]

当新旧节点都为数组时,需要执行 diff 算法,diff 算法的内容在后面的文章中会专门介绍。

Fragment和Text节点的特殊处理

Fragment的处理

Fragment 是 Vue3 新增的节点类型,用于支持多根节点:

function processFragment(oldVNode, newVNode, container, anchor) {
  if (oldVNode == null) {
    // 首次挂载
    mountFragment(newVNode, container, anchor);
  } else {
    // 更新
    patchFragment(oldVNode, newVNode, container, anchor);
  }
}

function mountFragment(vnode, container, anchor) {
  const { children, shapeFlag } = vnode;
  
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 文本子节点:挂载为文本节点
    const textNode = document.createTextNode(children);
    vnode.el = textNode;
    container.insertBefore(textNode, anchor);
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    // 数组子节点:挂载所有子节点
    mountChildren(children, container, anchor);
    
    // 设置el和anchor
    vnode.el = children[0]?.el;
    vnode.anchor = children[children.length - 1]?.el;
  }
}

function patchFragment(oldVNode, newVNode, container, anchor) {
  const oldChildren = oldVNode.children;
  const newChildren = newVNode.children;
  
  // Fragment本身没有DOM,直接patch子节点
  patchChildren(oldVNode, newVNode, container);
  
  // 更新el和anchor
  if (Array.isArray(newChildren)) {
    newVNode.el = newChildren[0]?.el || oldVNode.el;
    newVNode.anchor = newChildren[newChildren.length - 1]?.el || oldVNode.anchor;
  }
}

文本节点的优化

Vue3 对纯文本节点做了特殊优化,避免不必要的 VNode 创建:

// 模板:<div>{{ message }}</div>
// 编译后:
function render(ctx) {
  return h('div', null, ctx.message, PatchFlags.TEXT);
}

// 在patch过程中:
if (newVNode.patchFlag & PatchFlags.TEXT) {
  // 只需要更新文本内容,不需要比较其他属性
  const el = oldVNode.el;
  if (newVNode.children !== oldVNode.children) {
    el.textContent = newVNode.children;
  }
  newVNode.el = el;
  return;
}

手写实现:完整的patch函数基础版本

基础工具函数

// 类型标志
const ShapeFlags = {
  ELEMENT: 1,
  FUNCTIONAL_COMPONENT: 1 << 1,
  STATEFUL_COMPONENT: 1 << 2,
  TEXT_CHILDREN: 1 << 3,
  ARRAY_CHILDREN: 1 << 4,
  SLOTS_CHILDREN: 1 << 5,
  TELEPORT: 1 << 6,
  SUSPENSE: 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE: 1 << 8,
  COMPONENT_KEPT_ALIVE: 1 << 9
};

// 特殊节点类型
const Text = Symbol('Text');
const Comment = Symbol('Comment');
const Fragment = Symbol('Fragment');

// 判断是否同类型节点
function isSameVNodeType(n1, n2) {
  return n1.type === n2.type && n1.key === n2.key;
}

完整的patch函数

class Renderer {
  constructor(options) {
    this.options = options;
  }
  
  patch(oldVNode, newVNode, container, anchor = null) {
    if (oldVNode === newVNode) return;
    
    // 处理不同类型的节点
    if (oldVNode && !isSameVNodeType(oldVNode, newVNode)) {
      this.unmount(oldVNode);
      oldVNode = null;
    }
    
    const { type, shapeFlag } = newVNode;
    
    // 根据类型分发
    switch (type) {
      case Text:
        this.processText(oldVNode, newVNode, container, anchor);
        break;
      case Comment:
        this.processComment(oldVNode, newVNode, container, anchor);
        break;
      case Fragment:
        this.processFragment(oldVNode, newVNode, container, anchor);
        break;
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          this.processElement(oldVNode, newVNode, container, anchor);
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          this.processComponent(oldVNode, newVNode, container, anchor);
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          this.processTeleport(oldVNode, newVNode, container, anchor);
        }
    }
  }
  
  processElement(oldVNode, newVNode, container, anchor) {
    if (oldVNode == null) {
      // 挂载
      this.mountElement(newVNode, container, anchor);
    } else {
      // 更新
      this.patchElement(oldVNode, newVNode);
    }
  }
  
  mountElement(vnode, container, anchor) {
    const { type, props, children, shapeFlag } = vnode;
    
    // 创建元素
    const el = this.options.createElement(type);
    vnode.el = el;
    
    // 设置属性
    if (props) {
      for (const key in props) {
        this.options.patchProp(el, key, null, props[key]);
      }
    }
    
    // 处理子节点
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      this.options.setElementText(el, children);
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      this.mountChildren(children, el);
    }
    
    // 插入
    this.options.insert(el, container, anchor);
  }
  
  patchElement(oldVNode, newVNode) {
    const el = (newVNode.el = oldVNode.el);
    const oldProps = oldVNode.props || {};
    const newProps = newVNode.props || {};
    
    // 更新属性
    this.patchProps(el, oldProps, newProps);
    
    // 更新子节点
    this.patchChildren(oldVNode, newVNode, el);
  }
  
  patchChildren(oldVNode, newVNode, container) {
    const oldChildren = oldVNode.children;
    const newChildren = newVNode.children;
    
    const oldShapeFlag = oldVNode.shapeFlag;
    const newShapeFlag = newVNode.shapeFlag;
    
    // 新子节点是文本
    if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) {
      if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        this.unmountChildren(oldChildren);
      }
      if (oldChildren !== newChildren) {
        this.options.setElementText(container, newChildren);
      }
    }
    // 新子节点是数组
    else if (newShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      if (oldShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        this.options.setElementText(container, '');
        this.mountChildren(newChildren, container);
      } else if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        this.patchKeyedChildren(oldChildren, newChildren, container);
      }
    }
    // 新子节点为空
    else {
      if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        this.unmountChildren(oldChildren);
      } else if (oldShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        this.options.setElementText(container, '');
      }
    }
  }
  
  processText(oldVNode, newVNode, container, anchor) {
    if (oldVNode == null) {
      const textNode = this.options.createText(newVNode.children);
      newVNode.el = textNode;
      this.options.insert(textNode, container, anchor);
    } else {
      const el = (newVNode.el = oldVNode.el);
      if (newVNode.children !== oldVNode.children) {
        this.options.setText(el, newVNode.children);
      }
    }
  }
  
  processFragment(oldVNode, newVNode, container, anchor) {
    if (oldVNode == null) {
      this.mountFragment(newVNode, container, anchor);
    } else {
      this.patchFragment(oldVNode, newVNode, container, anchor);
    }
  }
  
  mountFragment(vnode, container, anchor) {
    const { children, shapeFlag } = vnode;
    
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      const textNode = this.options.createText(children);
      vnode.el = textNode;
      this.options.insert(textNode, container, anchor);
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      this.mountChildren(children, container, anchor);
      vnode.el = children[0]?.el;
      vnode.anchor = children[children.length - 1]?.el;
    }
  }
  
  mountChildren(children, container, anchor) {
    for (let i = 0; i < children.length; i++) {
      this.patch(null, children[i], container, anchor);
    }
  }
  
  unmount(vnode) {
    const { shapeFlag, el } = vnode;
    
    if (shapeFlag & ShapeFlags.COMPONENT) {
      this.unmountComponent(vnode);
    } else if (shapeFlag & ShapeFlags.FRAGMENT) {
      this.unmountFragment(vnode);
    } else if (el) {
      this.options.remove(el);
    }
  }
}

Vue2 与 Vue3 的 patch 差异

核心差异对比表

特性 Vue2 Vue3 优势
数据劫持 Object.defineProperty Proxy Vue3可以监听新增/删除属性
编译优化 全量比较 静态提升 + PatchFlags Vue3跳过静态节点比较
diff算法 双端比较 最长递增子序列 Vue3移动操作更少
Fragment 不支持 支持 多根节点组件
Teleport 不支持 支持 灵活的DOM位置控制
Suspense 不支持 支持 异步依赖管理
性能 中等 优秀 Vue3更新速度提升1.3-2倍

PatchFlags 带来的优化

Vue3 通过 PatchFlags 标记动态内容,减少比较范围:

const PatchFlags = {
  TEXT: 1,           // 动态文本
  CLASS: 2,          // 动态class
  STYLE: 4,          // 动态style
  PROPS: 8,          // 动态属性
  FULL_PROPS: 16,    // 全量props
  HYDRATE_EVENTS: 32, // 事件
  STABLE_FRAGMENT: 64, // 稳定Fragment
  KEYED_FRAGMENT: 128, // 带key的Fragment
  UNKEYED_FRAGMENT: 256, // 无key的Fragment
  NEED_PATCH: 512,   // 需要非props比较
  DYNAMIC_SLOTS: 1024, // 动态插槽
  
  HOISTED: -1,       // 静态节点
  BAIL: -2           // 退出优化
};

结语

理解 patch 算法,就像是掌握了 Vue 更新 DOM 的"手术刀"。知道它如何精准地找到需要更新的部分,以最小的代价完成更新,这不仅能帮助我们写出更高效的代码,还能在遇到性能问题时快速定位和优化。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

❌