Vue源码核心模块解析(壹)

202 阅读16分钟

基于 vue2.6.11

GitHub - vuejs/vue at v2.6.11

  • 代码目录
├─dist                   # 项目构建后的文件
├─scripts                # 与项目构建相关的脚本和配置文件
├─flow                   # flow的类型声明文件
├─src                    # 项目源代码
│    ├─complier          # 与模板编译相关的代码
│    ├─core              # 通用的、与运行平台无关的运行时代码
│    │  ├─observe        # 实现变化侦测的代码
│    │  ├─vdom           # 实现virtual dom的代码
│    │  ├─instance       # Vue.js实例的构造函数和原型方法
│    │  ├─global-api     # 全局api的代码
│    │  └─components     # 内置组件的代码
│    ├─server            # 与服务端渲染相关的代码
│    ├─platforms         # 特定运行平台的代码,如weex
│    ├─sfc               # 单文件组件的解析代码
│    └─shared            # 项目公用的工具代码
└─test                   # 项目测试代码
  • Vue 的整个项目包含了类型检测相关、单元测试相关、与平台无关的核心代码以及跨平台运行的相关代码。
  • 这里只是学习 Vue.js 的设计思想以及代码实现的相关逻辑,所以暂不去关心类型检测、单元测试以及特定平台运行等相关逻辑实现,仅关注它的核心代码,即 src/core 和 src/complier 这两个目录下的代码,并且接下来后续的学习也都是只在这两个目录的范围之内

学习路线

  1. 响应式实现:学习 Vue 中如何实现数据的响应式系统,从而达到数据驱动视图。
  2. 虚拟 DOM:学习什么是虚拟 DOM,以及 Vue 中的 DOM-Diff 原理
  3. 模板编译:学习 Vue 内部是怎么把 template 模板编译成虚拟 DOM,从而渲染出真实 DOM
  4. 实例方法:学习 Vue 中所有实例方法(即所有以$开头的方法)的实现原理
  5. 全局 API:学习 Vue 中所有全局 API 的实现原理
  6. 生命周期:学习 Vue 中组件的生命周期实现原理
  7. 指令:学习 Vue 中所有指令的实现原理
  8. 过滤器:学习 Vue 中所有过滤器的实现原理
  9. 内置组件:学习 Vue 中内置组件的实现原理

Vue.js 的响应式实现

众所周知,Vue 最大的特点之一就是数据驱动视图,那么什么是数据驱动视图呢?在这里,我们可以把数据理解为状态,而视图就是用户可直观看到页面。页面不可能是一成不变的,它应该是动态变化的,而它的变化也不应该是无迹可寻的,它或者是由用户操作引起的,亦或者是由后端数据变化引起的,不管它是因为什么引起的,我们统称为它的状态变了,它由前一个状态变到了后一个状态,页面也就应该随之而变化,所以我们就可以得到如下一个公式:

UI = render(state) 上述公式中:状态 state 是输入,页面 UI 输出,状态输入一旦变化了,页面输出也随之而变化。我们把这种特性称之为数据驱动视图。

Object 的响应式实现

  1. Vue.js 2.x
    • Object.defineProperty 定义 getter 和 setter
    • 无法检测到对象属性的添加或删除
  2. Vue.js 3.x
    • Proxy
    • Reflect
let car = {};
let val = 3000;
Object.defineProperty(car, "price", {
	configurable: true,
	enumerable: true,
	get() {
		console.log("price 属性被访问了");
		return val;
	},
	set(newValue) {
		console.log("price 属性被修改了");
		val = newValue;
	},
});
使对象变得可观测

首先定义一个observer类,将一个普通的object对象转换为可观测的object。并且会给value新增一个__ob__属性,相当于给value打了一个标记,表示这个value已经转化为响应式数据,避免重复操作 判断数据的类型,是否是数组,只有object的数据才调用walk把每一个属性转换成getter/setter的形式监听

if (Array.isArray(value)) {
    if (hasProto) {
        protoAugment(value, arrayMethods);
    } else {
        copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
} else {
    this.walk(value);
}

// walk 方法,遍历对象的每一个属性,调用 defineReactive 方法
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

defineReactive 方法,将一个对象转换成响应式对象,核心就是使用Object.defineProperty方法,给对象的属性添加getter/setter,当访问属性时就会触发getter,修改属性时就会触发setter

export function defineReactive(
	obj: Object,
	key: string,
	val: any,
	customSetter?: ?Function,
	shallow?: boolean
) {
	// 实例化一个依赖管理器,生成一个依赖管理数组 dep
	const dep = new Dep();

	// Object.getOwnPropertyDescriptor(obj, prop) // obj:叫查找其属性的对象;prop:要查找的属性名或symbol
	// MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
	const property = Object.getOwnPropertyDescriptor(obj, key);
	if (property && property.configurable === false) {
		return;
	}

	// cater for pre-defined getter/setters
	const getter = property && property.get;
	const setter = property && property.set;
	if ((!getter || setter) && arguments.length === 2) {
		val = obj[key];
	}

	let childOb = !shallow && observe(val);
	Object.defineProperty(obj, key, {
		enumerable: true,
		configurable: true,
		get: function reactiveGetter() {
			const value = getter ? getter.call(obj) : val;
			if (Dep.target) {
				// 在 getter 中收集依赖
				dep.depend();
				if (childOb) {
					childOb.dep.depend();
					if (Array.isArray(value)) {
						dependArray(value);
					}
				}
			}
			return value;
		},
		set: function reactiveSetter(newVal) {
			const value = getter ? getter.call(obj) : val;
			/* eslint-disable no-self-compare */
			if (newVal === value || (newVal !== newVal && value !== value)) {
				return;
			}
			/* eslint-enable no-self-compare */
			if (process.env.NODE_ENV !== "production" && customSetter) {
				customSetter();
			}
			// #7981: for accessor properties without setter
			if (getter && !setter) return;
			if (setter) {
				setter.call(obj, newVal);
			} else {
				val = newVal;
			}
			childOb = !shallow && observe(newVal);
			// 在 setter 中通知依赖更新
			dep.notify();
		},
	});
}
依赖收集

我们可以监听到一个数据的变化,然后去更新视图,但是不可能数据变化就把全部视图更新。 因此需要知道,那些视图依赖了哪些数据,需要给每个数据建立一个依赖数组(一个数据可以被多处使用),谁依赖了(用到了)这个数据,就把谁放到依赖数组中,当这个数据变化,我们就去对应的依赖数组中,通知每个依赖,去更新视图。 所谓谁依赖了这个数据,就是谁用到了这个数据,比如{{name}}{{age}},当渲染模板的时候,就会把nameage放到依赖数组中,当nameage变化,就会通知依赖数组中的每个依赖,去更新视图。

getter中收集依赖,在setter中通知依赖更新

我们应该给每一个数据都建立一个依赖管理器,把这个数据依赖的视图放到这个依赖管理器中,当数据变化,就通知这个依赖管理器中的每个依赖去更新视图。

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
	static target: ?Watcher;
	id: number;
	subs: Array<Watcher>;

	constructor() {
		this.id = uid++;
		this.subs = []; // 存放依赖
	}

	addSub(sub: Watcher) {
		this.subs.push(sub);
	}

	removeSub(sub: Watcher) {
		remove(this.subs, sub);
	}

	depend() {
		if (Dep.target) {
			Dep.target.addDep(this);
		}
	}

	notify() {
		// stabilize the subscriber list first
		const subs = this.subs.slice();
		if (process.env.NODE_ENV !== "production" && !config.async) {
			// subs aren't sorted in scheduler if not running async
			// we need to sort them now to make sure they fire in correct
			// order
			subs.sort((a, b) => a.id - b.id);
		}
		for (let i = 0, l = subs.length; i < l; i++) {
			subs[i].update();
		}
	}
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null;
const targetStack = [];

export function pushTarget(target: ?Watcher) {
	targetStack.push(target);
	Dep.target = target;
}

export function popTarget() {
	targetStack.pop();
	Dep.target = targetStack[targetStack.length - 1];
}
依赖到底是谁

在 Vue 中实现了一个Watcher类,Watcher类的实例就是我们前边说的“谁”。谁用到了数据,谁就是依赖,我们就给谁创建一个Watcher实例。这个类就是用来做依赖收集的,当渲染模板的时候,就会创建一个Watcher实例,这个Watcher实例就是依赖,当数据变化,就会通知这个依赖去更新视图。

export default class Watcher {
	vm: Component;
	expression: string;
	cb: Function;
	id: number;
	deep: boolean;
	user: boolean;
	lazy: boolean;
	sync: boolean;
	dirty: boolean;
	active: boolean;
	deps: Array<Dep>;
	newDeps: Array<Dep>;
	depIds: SimpleSet;
	newDepIds: SimpleSet;
	before: ?Function;
	getter: Function;
	value: any;

	constructor(
		vm: Component,
		expOrFn: string | Function,
		cb: Function,
		options?: ?Object,
		isRenderWatcher?: boolean
	) {
		this.vm = vm;
		if (isRenderWatcher) {
			vm._watcher = this;
		}
		vm._watchers.push(this);
		// options
		if (options) {
			this.deep = !!options.deep;
			this.user = !!options.user;
			this.lazy = !!options.lazy;
			this.sync = !!options.sync;
			this.before = options.before;
		} else {
			this.deep = this.user = this.lazy = this.sync = false;
		}
		this.cb = cb;
		this.id = ++uid; // uid for batching
		this.active = true;
		this.dirty = this.lazy; // for lazy watchers
		this.deps = [];
		this.newDeps = [];
		this.depIds = new Set();
		this.newDepIds = new Set();
		this.expression =
			process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
		// parse expression for getter
		if (typeof expOrFn === "function") {
			this.getter = expOrFn;
		} else {
			this.getter = parsePath(expOrFn);
			if (!this.getter) {
				this.getter = noop;
				process.env.NODE_ENV !== "production" &&
					warn(
						`Failed watching path: "${expOrFn}" ` +
							"Watcher only accepts simple dot-delimited paths. " +
							"For full control, use a function instead.",
						vm
					);
			}
		}
		this.value = this.lazy ? undefined : this.get();
	}

	/**
	 * Evaluate the getter, and re-collect dependencies.
	 */
	get() {
		pushTarget(this);
		let value;
		const vm = this.vm;
		try {
			value = this.getter.call(vm, vm);
		} catch (e) {
			if (this.user) {
				handleError(e, vm, `getter for watcher "${this.expression}"`);
			} else {
				throw e;
			}
		} finally {
			// "touch" every property so they are all tracked as
			// dependencies for deep watching
			if (this.deep) {
				traverse(value);
			}
			popTarget();
			this.cleanupDeps();
		}
		return value;
	}

	/**
	 * Add a dependency to this directive.
	 */
	addDep(dep: Dep) {
		const id = dep.id;
		if (!this.newDepIds.has(id)) {
			this.newDepIds.add(id);
			this.newDeps.push(dep);
			if (!this.depIds.has(id)) {
				dep.addSub(this);
			}
		}
	}

	/**
	 * Clean up for dependency collection.
	 */
	cleanupDeps() {
		let i = this.deps.length;
		while (i--) {
			const dep = this.deps[i];
			if (!this.newDepIds.has(dep.id)) {
				dep.removeSub(this);
			}
		}
		let tmp = this.depIds;
		this.depIds = this.newDepIds;
		this.newDepIds = tmp;
		this.newDepIds.clear();
		tmp = this.deps;
		this.deps = this.newDeps;
		this.newDeps = tmp;
		this.newDeps.length = 0;
	}

	/**
	 * Subscriber interface.
	 * Will be called when a dependency changes.
	 */
	update() {
		/* istanbul ignore else */
		if (this.lazy) {
			this.dirty = true;
		} else if (this.sync) {
			this.run();
		} else {
			queueWatcher(this);
		}
	}

	/**
	 * Scheduler job interface.
	 * Will be called by the scheduler.
	 */
	run() {
		if (this.active) {
			const value = this.get();
			if (
				value !== this.value ||
				// Deep watchers and watchers on Object/Arrays should fire even
				// when the value is the same, because the value may
				// have mutated.
				isObject(value) ||
				this.deep
			) {
				// set new value
				const oldValue = this.value;
				this.value = value;
				if (this.user) {
					try {
						this.cb.call(this.vm, value, oldValue);
					} catch (e) {
						handleError(
							e,
							this.vm,
							`callback for watcher "${this.expression}"`
						);
					}
				} else {
					this.cb.call(this.vm, value, oldValue);
				}
			}
		}
	}

	/**
	 * Evaluate the value of the watcher.
	 * This only gets called for lazy watchers.
	 */
	evaluate() {
		this.value = this.get();
		this.dirty = false;
	}

	/**
	 * Depend on all deps collected by this watcher.
	 */
	depend() {
		let i = this.deps.length;
		while (i--) {
			this.deps[i].depend();
		}
	}

	/**
	 * Remove self from all dependencies' subscriber list.
	 */
	teardown() {
		if (this.active) {
			// remove self from vm's watcher list
			// this is a somewhat expensive operation so we skip it
			// if the vm is being destroyed.
			if (!this.vm._isBeingDestroyed) {
				remove(this.vm._watchers, this);
			}
			let i = this.deps.length;
			while (i--) {
				this.deps[i].removeSub(this);
			}
			this.active = false;
		}
	}
}

2025-06-14-17-07-41-image.png

通过全局 API:Vue.useVue.delete触发对象的新增和删除

总结流程

  1. Object通过Observer转换成getter/setter的形式去追踪数据变化
  2. 当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中
  3. 当数据发生变化,会触发setter,从而向Dep中的依赖发送通知
  4. Watcher收到通知,会向外界发送通知,可能更新视图,也有可能触发某个回调函数

Array 的响应式实现

数组响应式实现

依然还是要在获取数据的时候,收集依赖。在数据变化时,通知依赖更新

在哪里收集依赖
data(){
    return{
        arr:[1,2,3,4],
        user:{},
                isLogin:true
    }
}
  • Array型数据还是在getter中收集
let arr = [1, 2, 3, 4];
arr.push(5);
Array.prototype.newPush = function (val) {
	// 这里不能使用箭头函数简写,否则会找不到`this`
	console.info("arr push");
	this.push(val);
};
arr.newPush(6);

2025-06-15-09-39-45-image.png

创建拦截器
  • 对数组的操作进行拦截封装。创建拦截器:arrayMethods
/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from "../util/index";

const arrayProto = Array.prototype;
/* 创建一个对象,作为拦截器 */
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
	"push",
	"pop",
	"shift",
	"unshift",
	"splice",
	"sort",
	"reverse",
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
	// cache original method
	const original = arrayProto[method];
	def(arrayMethods, method, function mutator(...args) {
		const result = original.apply(this, args);
		const ob = this.__ob__;
		let inserted;
		switch (method) {
			case "push":
			case "unshift":
				inserted = args;
				break;
			case "splice":
				inserted = args.slice(2);
				break;
		}
		if (inserted) ob.observeArray(inserted);
		// notify change
		ob.dep.notify();
		return result;
	});
});
  • 使用拦截器:arrayMethods
constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
    if (hasProto) {
			// value.__proto__ = arrayMethods
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}
如何收集依赖
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}
如何访问依赖
methodsToPatch.forEach(function (method) {
	// cache original method
	const original = arrayProto[method];
	def(arrayMethods, method, function mutator(...args) {
		const result = original.apply(this, args);
		const ob = this.__ob__;
		let inserted;
		switch (method) {
			case "push":
			case "unshift":
				inserted = args;
				break;
			case "splice":
				inserted = args.slice(2);
				break;
		}
		if (inserted) ob.observeArray(inserted);
		// notify change
		ob.dep.notify();
		return result;
	});
});

深度监听

Vue中,不管是 Object 还是 Array 实现的响应式数据,都是深度监听。也就是不但要监听自身数据变化,还要监听数据中所有的子数据变化

let arr = [
	{
		name: "Fa-ce",
		age: 18,
		info: {
			address: "xxx",
		},
	},
];

数组元素的新增:我们向数组中新增了一个元素,我们需要把新增的元素转换为响应式数据

不足

日常开发,可能这样写

let arr = [1, 2, 3];
arr[0] = 4; // 通过下标修改数组元素,不会触发视图更新
arr.length = 0; // 通过修改数组长度,不会触发视图更新

这两种方法都无法监听到,也不会触发响应式更新. Vue2 提供了两个方法来弥补不足之处:

  • Vue.set:修改数组中的元素,会触发响应式更新。this.$set(this.list, index, newValue)
  • Vue.delete: 删除数组中的元素,会触发响应式更新。this.$delete(this.list, index)

虚拟 DOM

虚拟 DOM 是什么

虚拟 DOM 是一个 JavaScript 对象,通过对象的方式来描述一个 DOM 节点

<div class="container" id="app">
	<a />
</div>;

// 虚拟 DOM
const VNode = {
	tag: "div" /* 元素标签 */,
	attrs: {
		class: "a",
		id: "b",
	} /* 属性 */,
	children: [
		/* 子元素 */
		{
			tag: "h1",
			children: "hello world",
		},
		{
			tag: "p",
			children: "hello vue",
		},
	],
};

我们把组成一个 DOM 节点必要东西通过一个JS对象表示出来,那么这个JS对象就可以用来描述这个DOM节点。 我们把这个JS对象就称为真实DOM节点的虚拟DOM节点

Vue 中的虚拟 DOM

VNode 类
export default class VNode {
	tag: string | void;
	data: VNodeData | void;
	children: ?Array<VNode>;
	text: string | void;
	elm: Node | void;
	ns: string | void;
	context: Component | void; // rendered in this component's scope
	key: string | number | void;
	componentOptions: VNodeComponentOptions | void;
	componentInstance: Component | void; // component instance
	parent: VNode | void; // component placeholder node

	// strictly internal
	raw: boolean; // contains raw HTML? (server only)
	isStatic: boolean; // hoisted static node
	isRootInsert: boolean; // necessary for enter transition check
	isComment: boolean; // empty comment placeholder?
	isCloned: boolean; // is a cloned node?
	isOnce: boolean; // is a v-once node?
	asyncFactory: Function | void; // async component factory function
	asyncMeta: Object | void;
	isAsyncPlaceholder: boolean;
	ssrContext: Object | void;
	fnContext: Component | void; // real context vm for functional nodes
	fnOptions: ?ComponentOptions; // for SSR caching
	devtoolsMeta: ?Object; // used to store functional render context for devtools
	fnScopeId: ?string; // functional scope id support

	constructor(
		tag?: string,
		data?: VNodeData,
		children?: ?Array<VNode>,
		text?: string,
		elm?: Node,
		context?: Component,
		componentOptions?: VNodeComponentOptions,
		asyncFactory?: Function
	) {
		this.tag = tag;
		this.data = data;
		this.children = children;
		this.text = text; /* 节点的文本 */
		this.elm = elm; /* 当前虚拟节点对应的真实DOM节点 */
		this.ns = undefined; /* 命名空间 */
		this.context = context; /* 当前虚拟节点对应的Vue实例 */
		this.fnContext = undefined; /* 函数式组件对应的Vue实例 */
		this.fnOptions = undefined;
		this.fnScopeId = undefined;
		this.key =
			data &&
			data.key; /* 节点的key属性,被当作节点的标识,diff算法性能优化会用到 */
		this.componentOptions = componentOptions;
		this.componentInstance = undefined;
		this.parent = undefined;
		this.raw = false; /* 是否是原生HTML字符串,或者普通文本,innerHTML的时候为true */
		this.isStatic = false;
		this.isRootInsert = true;
		this.isComment = false; /* 是否注释节点 */
		this.isCloned = false;
		this.isOnce = false;
		this.asyncFactory = asyncFactory;
		this.asyncMeta = undefined;
		this.isAsyncPlaceholder = false;
	}

	// DEPRECATED: alias for componentInstance for backwards compat.
	/* istanbul ignore next */
	get child(): Component | void {
		return this.componentInstance;
	}
}

export const createEmptyVNode = (text: string = "") => {
	const node = new VNode();
	node.text = text;
	node.isComment = true;
	return node;
};

export function createTextVNode(val: string | number) {
	return new VNode(undefined, undefined, undefined, String(val));
}

// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode(vnode: VNode): VNode {
	const cloned = new VNode(
		vnode.tag,
		vnode.data,
		// #7975
		// clone children array to avoid mutating original in case of cloning
		// a child.
		vnode.children && vnode.children.slice(),
		vnode.text,
		vnode.elm,
		vnode.context,
		vnode.componentOptions,
		vnode.asyncFactory
	);
	cloned.ns = vnode.ns;
	cloned.isStatic = vnode.isStatic;
	cloned.key = vnode.key;
	cloned.isComment = vnode.isComment;
	cloned.fnContext = vnode.fnContext;
	cloned.fnOptions = vnode.fnOptions;
	cloned.fnScopeId = vnode.fnScopeId;
	cloned.asyncMeta = vnode.asyncMeta;
	cloned.isCloned = true;
	return cloned;
}
  1. VNode 的类型
  • 注释节点: isComment: true,createEmptyVNode(text: string = '')
  • 文本节点: isStatic: true,createTextVNode(val: string | number)
  • 元素节点:
<div class="container" id="app">
	<a />
</div>; // 虚拟 DOM
const VNode = {
	tag: "div" /* 元素标签 */,
	attrs: {
		class: "a",
		id: "b",
	} /* 属性 */,
	children: [
		{
			tag: "h1",
			children: "hello world",
		},
		{
			tag: "p",
			children: "hello vue",
		},
	],
};
  • 组件节点
    • 组件节点除了元素节点具有的属性,还有两个特定的属性
    • componentOptions:组件的选项,比如组件的名称,组件的 props 等
    • componentInstance:组件的实例,组件渲染完成后,组件的实例会保存在这个属性中
  • 函数式组件节点
    • fnContext:函数式组件的上下文
    • fnOptions:函数式组件的选项
  • 克隆节点:cloneVNode(vnode: VNode),把一个已经存在的节点进行复制一份,主要做模板编译优化时使用
  1. VNode 的作用
    • 我们在视图渲染之前,把写好的 template 模板先编译成VNode并缓存下来,等到数据发生变化需要重新渲染的时候,我们把数据发生变化后生成的VNode和之前缓存好的VNode进行对比,找出差异,然后有差异的 VNode 对应的真实 DOM 节点就是我们需要进行更新重新渲染的节点,最后根据有差异的 VNode 节点创建出真实的 DOM 节点插入到视图中,从而更新视图

Vue 中的 DOM-diff 算法

  • Patch

    • 指对旧的 VNode 进行修补,打补丁得到新的 VNode
    • 以新的 VNode 为基准,改造旧的 VNode 使其与新的 VNode 保持一致。这就是 patch 的过程
      • 创建节点:新的 VNode 中有而旧的 oldVNode 中没有,就在旧的 VNode 中创建
      • 删除节点:新的 VNode 中没有而旧的 oldVNode 中有,就从旧的 VNode 中删除
      • 更新节点:新的 VNode 中和旧的 oldVNode 中都有的节点,就以新的 VNode 为准,更新旧的 oldVNode
  • 创建节点

    • 创建节点只有三种类型:注释节点、文本节点、元素节点。Vue 创建节点的时候需要先判断在新的 VNode,会根据节点的类型,调用不同的创建方法插入到 DOM 中
    function createElm(vnode, parentElm, refElm) {
    	const tag = vnode.tag;
    	if (isDef(tag)) {
    		// 创建元素节点
    		vnode.elm = nodeOps.createElement(tag, vnode);
    		// 创建子节点
    		createChildren(vnode, children, insertedVnodeQueue);
    		// 插入 DOM
    		insert(parentElm, vnode.elm, refElm);
    	} else if (isTrue(vnode.isComment)) {
    		vnode.elm = nodeOps.createComment(vnode.text);
    		insert(parentElm, vnode.elm, refElm);
    	} else {
    		vnode.elm = nodeOps.createTextNode(vnode.text);
    		insert(parentElm, vnode.elm, refElm);
    	}
    }
    
  • 删除节点

    function removeNode(el) {
    	const parent = nodeOps.parentNode(el);
    	// element may have already been removed due to v-html / v-text
    	if (isDef(parent)) {
    		nodeOps.removeChild(parent, el);
    	}
    }
    
  • 更新节点
<span>静态节点</span>
  • VNode 和 oldVNode 都是静态节点的话,直接跳过,无需处理
  • 如果 VNode 是文本节点,那么只需要看 oldVNode 是否文本节点,如果是,直接对比文本内容是否一致,如果不是,就会调用 setTextNode 方法,把他改成文本节点,然后再对比文本内容
  • VNode 是元素节点
    • 该节点包含子节点:
      • 看旧节点是否包含子节点,如果包含,那么就递归对比更新子节点
      • 如果旧的节点不包含子节点,那么旧节点可能是空节点/文本节点
        • 如果是空节点,就把新的节点和子节点创建一份然后插入旧的子节点
        • 如果是文本节点,则直接清空文本,然后创建新的子节点插入旧的子节点
    • 该节点不包含子节点:
      • 说明该节点为空节点不论旧节点是否包含子节点,都需要清空子节点
function patchVnode(
	oldVnode,
	vnode,
	insertedVnodeQueue,
	ownerArray,
	index,
	removeOnly
) {
	// 新旧节点完全相同
	if (oldVnode === vnode) {
		return;
	}

	if (isDef(vnode.elm) && isDef(ownerArray)) {
		// clone reused vnode
		vnode = ownerArray[index] = cloneVNode(vnode);
	}

	const elm = (vnode.elm = oldVnode.elm);

	if (isTrue(oldVnode.isAsyncPlaceholder)) {
		if (isDef(vnode.asyncFactory.resolved)) {
			hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
		} else {
			vnode.isAsyncPlaceholder = true;
		}
		return;
	}

	// reuse element for static trees.
	// note we only do this if the vnode is cloned -
	// if the new node is not cloned it means the render functions have been
	// reset by the hot-reload-api and we need to do a proper re-render.

	// 是否是静态节点,并且key相同
	if (
		isTrue(vnode.isStatic) &&
		isTrue(oldVnode.isStatic) &&
		vnode.key === oldVnode.key &&
		(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
	) {
		vnode.componentInstance = oldVnode.componentInstance;
		return;
	}

	let i;
	const data = vnode.data;
	if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
		i(oldVnode, vnode);
	}

	const oldCh = oldVnode.children;
	const ch = vnode.children;
	if (isDef(data) && isPatchable(vnode)) {
		for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
		if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode);
	}
	// VNode 是否有 text 属性
	if (isUndef(vnode.text)) {
		// 判断子节点是否存在
		if (isDef(oldCh) && isDef(ch)) {
			// 如果子节点都存在,判断子节点是否相同,不同则更新子节点
			if (oldCh !== ch)
				updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly);
		} else if (isDef(ch)) {
			// 如果只有新的 VNode 存在子节点,那么创建子节点
			if (process.env.NODE_ENV !== "production") {
				checkDuplicateKeys(ch);
			}
			// 判断oldVnode是否有文本,如果有,那么清空,再把 VNode 的子节点插入到真实 DOM 中
			// 如果oldVnode没有文本,那么直接把 VNode 的子节点插入到真实 DOM 中
			if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, "");
			//
			addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
		} else if (isDef(oldCh)) {
			removeVnodes(oldCh, 0, oldCh.length - 1);
		} else if (isDef(oldVnode.text)) {
			nodeOps.setTextContent(elm, "");
		}
	} else if (oldVnode.text !== vnode.text) {
		nodeOps.setTextContent(elm, vnode.text);
	}
	if (isDef(data)) {
		if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode);
	}
}

更新子节点

当新的 Vnode 和旧的 Vnode 都是元素节点并且都包含子节点

for (let i = 0; i < newChildren.length; i++) {
	const newChild = newChildren[i];
	for (let j = 0; j < oldChildren.length; j++) {
		const oldChild = oldChildren[j];
		if (newChild === oldChild) {
			// ....
		}
	}
}
  • 更新子节点 如果newChildren里的某个子节点在oldChildren里找到了与之相同的子节点,并且位置也相同,那么就直接更新oldChildren里的该节点,使之与newChildren里的节点相同

  • 创建子节点 如果newChildren里面的某个子节点在oldChildren里面找不到与之相同的子节点,那么需要新增,即创建子节点。这里需要找到合适的位置插入新的子节点,这个位置是所有的未处理节点之前,而不是已处理节点之后2025-06-18-21-43-11-image.png 2025-06-18-21-43-24-image.png

  • 删除子节点 如果newChildren里面的每一个子节点都循环完毕,发现在oldChildren里还有未处理的子节点,那么这些子节点就是需要删除的节点

  • 移动子节点 如果newChildren里和oldChildren都存在一个相同的子节点,但是这个子节点位置不同,那么需要以newChildren里的子节点位置为基准,移动旧的子节点(位置依然是所有的未处理节点之前。)

  • 更新子节点 如果newChildren里的某个子节点在oldChildren里找到了与之相同的子节点,并且位置也相同,那么就直接更新oldChildren里的该节点,使之与newChildren里的节点相同

    if (isUndef(idxInOld)) {
    	// New element 如果在 OldChildren 里找不到当前循环的 newChildren 里的子节点
    	// 创建子节点并插入到合适的位置
    	createElm(
    		newStartVnode,
    		insertedVnodeQueue,
    		parentElm,
    		oldStartVnode.elm,
    		false,
    		newCh,
    		newStartIdx
    	);
    } else {
    	// 如果在旧的节点中找到了当前循环的 newChildren 里的子节点
    	vnodeToMove = oldCh[idxInOld];
    	/* 如果两个节点相同 */
    	if (sameVnode(vnodeToMove, newStartVnode)) {
    		// 调用 patchVnode 更新节点
    		patchVnode(
    			vnodeToMove,
    			newStartVnode,
    			insertedVnodeQueue,
    			newCh,
    			newStartIdx
    		);
    		oldCh[idxInOld] = undefined;
    		// canMove 表示是否需要移动节点
    		canMove &&
    			nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
    	} else {
    		// same key but different element. treat as new element
    		createElm(
    			newStartVnode,
    			insertedVnodeQueue,
    			parentElm,
    			oldStartVnode.elm,
    			false,
    			newCh,
    			newStartIdx
    		);
    	}
    }
    
  • 创建子节点 如果newChildren里面的某个子节点在oldChildren里面找不到与之相同的子节点,那么需要新增,即创建子节点。这里需要找到合适的位置插入新的子节点,这个位置是所有的未处理节点之前,而不是以处理节点之后。(位置依然是所有未处理的节点之前)

更新节点算法优化 假如我们现有一份新的 newChildren 数组和旧的 oldChildren 数组,如下所示:

const newChildren = ["新子节点 1", "新子节点 2", "新子节点 3", "新子节点 4"];
const oldChildren = ["旧子节点 1", "旧子节点 2", "旧子节点 3", "旧子节点 4"];

2025-06-18-22-12-44-image.png

优化策略

2025-06-18-22-14-42-image.png

在上图中,我们把:

  • newChildren 数组里的所有未处理子节点的第一个子节点称为:新前;
  • newChildren 数组里的所有未处理子节点的最后一个子节点称为:新后;
  • oldChildren 数组里的所有未处理子节点的第一个子节点称为:旧前;
  • oldChildren 数组里的所有未处理子节点的最后一个子节点称为:旧后;
  1. 新前与旧前相同
    2025-06-18-22-47-18-image.png
  2. 新后与旧后相同
    2025-06-18-22-47-32-image.png
  3. 新后与旧前相同
    2025-06-18-22-47-44-image.png 此时,出现了移动节点的操作,如下图:
    2025-06-18-22-48-05-image.png
  4. 新前与旧后相同
    2025-06-18-22-48-16-image.png 同样,这种情况的节点移动位置逻辑与“新后与旧前”的逻辑类似,那就是 newChildren 数组里的第一个子节点与 oldChildren 数组里的最后一个子节点相同,那么我们就应该在 oldChildren 数组里把最后一个子节点移动到第一个子节点的位置,如下图
    2025-06-18-22-51-38-image.png

源码

function updateChildren(
	parentElm,
	oldCh,
	newCh,
	insertedVnodeQueue,
	removeOnly
) {
	let oldStartIdx = 0; // oldChildren 开始索引
	let newStartIdx = 0; // newChildren 结束索引
	let oldEndIdx = oldCh.length - 1; // oldChildren 结束索引
	let oldStartVnode = oldCh[0]; // oldChildren 所有未处理节点的第一个
	let oldEndVnode = oldCh[oldEndIdx]; // oldChildren 所有未处理节点的最后一个
	let newEndIdx = newCh.length - 1; // newChildren 结束索引
	let newStartVnode = newCh[0]; // newChildren 所有未处理节点的第一个
	let newEndVnode = newCh[newEndIdx]; // newChildren 所有未处理节点的最后一个
	let oldKeyToIdx, idxInOld, vnodeToMove, refElm;

	// removeOnly is a special flag used only by <transition-group>
	// to ensure removed elements stay in correct relative positions
	// during leaving transitions
	const canMove = !removeOnly;

	if (process.env.NODE_ENV !== "production") {
		checkDuplicateKeys(newCh);
	}

	/**
	 * @description: 以`新前`,`新后`,`旧前`,`旧后`为基准,循环遍历,对比新旧节点,进行更新
	 */
	while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
		if (isUndef(oldStartVnode)) {
			/* 如果oldStartVnode 不存在,直接跳过,将oldStartIdx+1,对比下一个 */
			oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
		} else if (isUndef(oldEndVnode)) {
			/* 如果 oldEndVnode不存在,则直接跳过,将 oldEndIdx-1 */
			oldEndVnode = oldCh[--oldEndIdx];
		} else if (sameVnode(oldStartVnode, newStartVnode)) {
			/**
			 * 1.
			 * 如果`新前`与`旧前`节点相同,则把两个节点进行 patch 更新.
			 * 同时 oldStartIdx+1,newStartIdx+1
			 */
			patchVnode(
				oldStartVnode,
				newStartVnode,
				insertedVnodeQueue,
				newCh,
				newStartIdx
			);
			oldStartVnode = oldCh[++oldStartIdx];
			newStartVnode = newCh[++newStartIdx];
		} else if (sameVnode(oldEndVnode, newEndVnode)) {
			/**
			 * 2.
			 * 如果`新后`与`旧后`节点相同,则把两个节点进行 patch 更新
			 * 同时 oldEndIdx-1,newEndIdx-1,左移一个位置
			 *  */
			patchVnode(
				oldEndVnode,
				newEndVnode,
				insertedVnodeQueue,
				newCh,
				newEndIdx
			);
			oldEndVnode = oldCh[--oldEndIdx];
			newEndVnode = newCh[--newEndIdx];
		} else if (sameVnode(oldStartVnode, newEndVnode)) {
			/**
			 * 3.
			 * 如果`新后`与`旧前`节点相同,则把两个节点进行 patch 更新,然后把旧前节点移动到`oldChildren`中所有未处理节点之后
			 * 同时 oldStartIdx+1 后移一个位子,newEndIdx-1 前移一个位子
			 */
			// Vnode moved right
			patchVnode(
				oldStartVnode,
				newEndVnode,
				insertedVnodeQueue,
				newCh,
				newEndIdx
			);
			canMove &&
				nodeOps.insertBefore(
					parentElm,
					oldStartVnode.elm,
					nodeOps.nextSibling(oldEndVnode.elm)
				);
			oldStartVnode = oldCh[++oldStartIdx];
			newEndVnode = newCh[--newEndIdx];
		} else if (sameVnode(oldEndVnode, newStartVnode)) {
			/**
			 * 4.
			 *  如果`新前`与`旧后`节点相同,则把两个节点进行 patch 更新,然后把旧后节点移动到`oldChildren`中所有未处理节点之前
			 *  同时 oldEndIdx-1 前移一个位子,newStartIdx+1 后移一个位子
			 *  */
			// Vnode moved left
			patchVnode(
				oldEndVnode,
				newStartVnode,
				insertedVnodeQueue,
				newCh,
				newStartIdx
			);
			canMove &&
				nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
			oldEndVnode = oldCh[--oldEndIdx];
			newStartVnode = newCh[++newStartIdx];
		} else {
			/**
			 * 5.
			 * 除了以上 4 种情况,都是循环遍历所有未处理节点,进行 patch 更新
			 */
			if (isUndef(oldKeyToIdx))
				oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
			idxInOld = isDef(newStartVnode.key)
				? oldKeyToIdx[newStartVnode.key]
				: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
			if (isUndef(idxInOld)) {
				// New element
				// 创建子节点并插入到合适的位置
				createElm(
					newStartVnode,
					insertedVnodeQueue,
					parentElm,
					oldStartVnode.elm,
					false,
					newCh,
					newStartIdx
				);
			} else {
				// 如果在旧的节点中找到了相同的节点
				// 但是key不相同,说明是同一个元素,但是不同的节点
				vnodeToMove = oldCh[idxInOld];
				if (sameVnode(vnodeToMove, newStartVnode)) {
					/* 如果两个节点相同 */
					patchVnode(
						vnodeToMove,
						newStartVnode,
						insertedVnodeQueue,
						newCh,
						newStartIdx
					);
					oldCh[idxInOld] = undefined;
					canMove &&
						nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
				} else {
					// same key but different element. treat as new element
					createElm(
						newStartVnode,
						insertedVnodeQueue,
						parentElm,
						oldStartVnode.elm,
						false,
						newCh,
						newStartIdx
					);
				}
			}
			newStartVnode = newCh[++newStartIdx];
		}
	}
	if (oldStartIdx > oldEndIdx) {
		/**
		 * 如果oldChildren比newChildren先循环完毕,那么newChildren里面剩余的节点都是需要新增的节点
		 * 把[newStartIdx, newEndIdx]之间的所有节点都新增,插入到DOM中
		 **/
		refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
		addVnodes(
			parentElm,
			refElm,
			newCh,
			newStartIdx,
			newEndIdx,
			insertedVnodeQueue
		);
	} else if (newStartIdx > newEndIdx) {
		/**
		 * 如果 newChildren 比 oldChildren 先循环完毕,那么 oldChildren 剩余的节点都是需要删除的节点
		 */
		removeVnodes(oldCh, oldStartIdx, oldEndIdx);
	}
}
  • 当循环的时候,每处理一个节点,就把start下标+1 或者end下标-1,然后再进行下一次循环。
  • 这样一次更新将会移动两个节点,也就是 newStartIdxoldStartIndex 只会向后移动(只会++),但是 oldEndIdxnewEndIdx 只会向前移动(--),当开始位置大于结束位置,所有节点全部便利完成