基于 vue2.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 这两个目录下的代码,并且接下来后续的学习也都是只在这两个目录的范围之内
学习路线
- 响应式实现:学习 Vue 中如何实现数据的响应式系统,从而达到数据驱动视图。
- 虚拟 DOM:学习什么是虚拟 DOM,以及 Vue 中的 DOM-Diff 原理
- 模板编译:学习 Vue 内部是怎么把 template 模板编译成虚拟 DOM,从而渲染出真实 DOM
- 实例方法:学习 Vue 中所有实例方法(即所有以$开头的方法)的实现原理
- 全局 API:学习 Vue 中所有全局 API 的实现原理
- 生命周期:学习 Vue 中组件的生命周期实现原理
- 指令:学习 Vue 中所有指令的实现原理
- 过滤器:学习 Vue 中所有过滤器的实现原理
- 内置组件:学习 Vue 中内置组件的实现原理
Vue.js 的响应式实现
众所周知,Vue 最大的特点之一就是数据驱动视图,那么什么是数据驱动视图呢?在这里,我们可以把数据理解为状态,而视图就是用户可直观看到页面。页面不可能是一成不变的,它应该是动态变化的,而它的变化也不应该是无迹可寻的,它或者是由用户操作引起的,亦或者是由后端数据变化引起的,不管它是因为什么引起的,我们统称为它的状态变了,它由前一个状态变到了后一个状态,页面也就应该随之而变化,所以我们就可以得到如下一个公式:
UI = render(state) 上述公式中:状态 state 是输入,页面 UI 输出,状态输入一旦变化了,页面输出也随之而变化。我们把这种特性称之为数据驱动视图。
Object 的响应式实现
- Vue.js 2.x
Object.defineProperty定义 getter 和 setter- 无法检测到对象属性的添加或删除
- 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}},当渲染模板的时候,就会把name和age放到依赖数组中,当name或age变化,就会通知依赖数组中的每个依赖,去更新视图。
在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;
}
}
}
通过全局 API:Vue.use和Vue.delete触发对象的新增和删除
总结流程
Object通过Observer转换成getter/setter的形式去追踪数据变化- 当外界通过
Watcher读取数据时,会触发getter从而将Watcher添加到依赖中- 当数据发生变化,会触发
setter,从而向Dep中的依赖发送通知- 当
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);
创建拦截器
- 对数组的操作进行拦截封装。创建拦截器:
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;
}
- 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),把一个已经存在的节点进行复制一份,主要做模板编译优化时使用
- VNode 的作用
- 我们在视图渲染之前,把写好的 template 模板先编译成
VNode并缓存下来,等到数据发生变化需要重新渲染的时候,我们把数据发生变化后生成的VNode和之前缓存好的VNode进行对比,找出差异,然后有差异的 VNode 对应的真实 DOM 节点就是我们需要进行更新重新渲染的节点,最后根据有差异的 VNode 节点创建出真实的 DOM 节点插入到视图中,从而更新视图
- 我们在视图渲染之前,把写好的 template 模板先编译成
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里面找不到与之相同的子节点,那么需要新增,即创建子节点。这里需要找到合适的位置插入新的子节点,这个位置是所有的未处理节点之前,而不是已处理节点之后。 -
删除子节点 如果
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"];
优化策略
在上图中,我们把:
newChildren数组里的所有未处理子节点的第一个子节点称为:新前;newChildren数组里的所有未处理子节点的最后一个子节点称为:新后;oldChildren数组里的所有未处理子节点的第一个子节点称为:旧前;oldChildren数组里的所有未处理子节点的最后一个子节点称为:旧后;
- 新前与旧前相同
- 新后与旧后相同
- 新后与旧前相同
此时,出现了移动节点的操作,如下图:
- 新前与旧后相同
同样,这种情况的节点移动位置逻辑与“新后与旧前”的逻辑类似,那就是 newChildren 数组里的第一个子节点与 oldChildren 数组里的最后一个子节点相同,那么我们就应该在 oldChildren 数组里把最后一个子节点移动到第一个子节点的位置,如下图
源码
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,然后再进行下一次循环。 - 这样一次更新将会移动两个节点,也就是
newStartIdx和oldStartIndex只会向后移动(只会++),但是oldEndIdx和newEndIdx只会向前移动(--),当开始位置大于结束位置,所有节点全部便利完成