Appearance
Vue 2.0
导航目录
Vue 挂载流程
Vue 挂载流程是组件从创建到渲染到页面的完整过程,主要包括以下步骤:
- 创建 Vue 实例并传入配置项,对配置项进行
mergeOptions合并,初始化options配置项 - 初始化组件上的 props、data、computed、watch 等选项
- 如果有 el 属性,会调用
$mount方法,通过compile 函数把模板转换成render 函数 - 模板转换完成后执行
mountComponent方法,实例化渲染 Watcher,将组件渲染、更新的控制权交给 Watcher - 当 render 函数被调用时,如果响应式数据有取值操作,dep 订阅器会依赖收集当前的 Watcher
- 最终调用
patch函数将 Vnode 转换成真实的 DOM 并渲染在页面(同时每一个虚拟节点的 el 属性会对应一个真实的 DOM,后续更新时复用) - 当响应式数据发生变化后,会触发 dep 依赖通知渲染 watcher 更新视图

Vue 数据响应式处理
Vue 2.0 是通过 Object.defineProperty 进行属性代理,实现数据响应式的。主要流程如下:
- 如果 data 数据不是对象或者已经被代理过,不做任何处理,直接返回
- 如果是对象,会给当前的对象创建一个 dep 订阅器,让当前的对象也具有收集 Watcher 的能力,同时挂载一个
__ob__属性指向当前的 observer 实例(主要是用来解决数组问题) - 递归遍历对象的每一个属性,给每一个属性新增 getter/setter 拦截,同时给每一个属性关联一个 dep 订阅器,用来收集、触发 Watcher
- 如果是数组,会对
"push", "pop", "unshift", "shift", "reverse", "sort", "splice"等方法进行 AOP 拦截,同时会遍历数组,让数组中的对象进行代理 - 组件在渲染时,如果有取值操作会触发 get 拦截,会让 dep 订阅器去收集当前的渲染 watcher(如果值是一个对象,会让当前的对象也收集渲染的 watcher,解决数组改变更新视图的问题)
- 当响应式数据发生变化时,会触发 setter 拦截,让 dep 去通知收集的 watcher 更新视图(如果新的值是一个对象,也会进行代理)
数据的特殊处理
数组的特殊处理:当数组有 push、pop、unshift、shift、reverse、sort、splice 操作时,会进行 AOP 拦截,具体处理步骤如下:
- 首先会通过 call 调用数组原有的函数,更新数组的值
- 判断是否有新增操作(push、unshift、splice),如果有会对新增的元素(如果是对象)进行代理
- 最终找到自身的
__ob__属性,触发 dep 去通知收集的 watcher 更新视图
js
const oldArrayPrototype = Array.prototype;
export const proto = Object.create(oldArrayPrototype);
["push", "pop", "unshfit", "shift", "reverse", "sort", "splice"].forEach(
(method) => {
proto[method] = function (...args) {
let ob = this.__ob__;
let r = oldArrayPrototype[method].call(this, ...args);
let inserted;
switch (method) {
case "push":
case "unshift": // 前后新增
inserted = args;
case "splice":
// arr.splice(0,1,新增的内容)
// 如果是splice,参数大于2个后门的都是新增的值
inserted = args.slice(2);
default:
break;
}
// 数组发生变化,通知dep更新
ob.dep.notify();
// 如果有新增,需要新增的做代理
if (inserted) ob.observeArray(inserted);
return r;
};
}
);模板中数组的 stringify 处理
在模板中访问数组时,会进行 stringify 处理,具体表现为:
- 如果数组里有对象,会访问对象的每一个属性触发 getter 做依赖收集
- 如果数组里有数组,由于数组本身没有 getter 拦截,需要递归让每一个数组收集 watcher
js
let arr = [1, [2, 3, [4, 5]], { name: "hl" }];
// 如果数组里有对象,会访问对象的每一个属性触发 get 做依赖收集
setTimeout(() => {
app.arr[2].name = "hulei";
}, 6000);
// 如果数组里有数组,但是数组本身没有 get 操作,需要递归让每一个数组收集 watcher
setTimeout(() => {
app.arr[1].push(6);
}, 9000);
setTimeout(() => {
app.arr[1][2].push(6);
}, 12000);js
// 每一个对象的属性都会关联一个dep实例
const dep = new Dep();
Object.defineProperty(target, key, {
get: () => {
if (Dep.target) {
dep.depend();
if (childOb) {
// 如果对象中有数组,让数组本身也依赖收集
// 主要是解决数组更新问题,数组不会被拦截
childOb.dep.depend();
// 如果数组里有数组,但是数组本身没有 get 操作,需要递归让每一个数组收集 watcher
if (Array.isArray(value)) {
this.dependArray(value);
}
}
}
return value;
},
});Vue watch 监听
watch 是基于 Watcher 来实现的,用于监听数据变化并触发回调执行。主要流程如下:
- watch 在初始化时,会实例化一个 Watcher,将回调执行权交给它,watcher 实例本身会存储监听的值和更新的watch 回调
- Watcher 会立即调用 get 函数,对监听的属性进行访问,此时触发 getter 拦截,让当前的属性触发 dep 订阅器依赖收集当前的 Watcher
- 当监听的属性发生变化后,触发 dep 去通知收集的 Watcher 执行 watch 回调
js
Vue.prototype.$watch = function (key, handler) {
new Watcher(this, key, handler, { user: true });
};Vue computed 计算属性
computed 也是基于 Watcher 来实现的,具有缓存特性。主要流程如下:
- computed 在初始化时不会立马执行,而是把所有的计算属性存储在组件实例上
- 当计算属性被访问时,如果被缓存过直接返回缓存的值
- 如果没有缓存,调用计算属性函数
evaluate,进行取值操作(触发 getter 拦截,让 dep 会收集当前的计算属性 watcher),并且标记为已经缓存 - 如果计算属性里的响应式数据发生改变,会触发 getter 拦截,让 dep 去通知计算属性 watcher 取消缓存标记
js
function initComputed(vm) {
let computed = vm.$options.computed;
vm._computedWatchers = {};
for (const key in computed) {
vm._computedWatchers[key] = new Watcher(vm, computed[key], () => {}, {
lazy: true,
});
Object.defineProperty(vm, key, {
get() {
let watcher = vm._computedWatchers[key];
// 如果没有缓存
if (watcher && watcher.dirty) {
// 实际上是调用了计算属性watcher.get,如果此时有响应式数据取值会触发get操作,让对应的dep订阅器收集计算属性watcher
watcher.evaluate();
// 调用结束后计算属性watcher出栈
}
// 此时响应式数据只收集了计算属性watcher,如果数据发生变化我们是希望页面视图能够刷新
if (Dep.target) {
// 因此我们也需要响应式数据收集渲染的watcher
watcher.depend();
}
return watcher.value;
},
});
}
}watch vs computed
computed 特点:
- 可以监听多个值
- 只有在获取数据的时候,才会触发依赖收集(计算属性 watcher)
- 支持缓存,只有当依赖的数据发生变化时才会重新计算
- 名称不能和 data 里的对象重复
watch 特点:
- 名称只能和 data 里的对象一致
- 只能监听一个值
- 初始化会触发依赖收集(watch watcher)
- 不支持缓存,数据变化时会立即触发回调
Vue diff(五种策略)
Vue 2.0 的 diff 算法只会对比同级节点,不考虑跨层级情况,如果节点类型或key不一样直接删除并创建新的节点。Vue 2.0 采用的是双指针策略,具体包括以下五种对比策略:
- 头头对比:如果 tag、key 相同复用老节点,新老起始索引递增,对比 props 和子元素
- 尾尾对比:如果 tag、key 相同复用老节点,新老起始索引递减
- 老头新尾对比:如果 tag、key 相同复用老节点(把老的移动 DOM 插入到尾部),老的起始索引递增、新的结束索引递减
- 老尾新头对比:如果 tag、key 相同复用老节点(把老的移动 DOM 插入到头部),老的结束索引递减、新的起始索引递增
- 乱序对比:用新的起始的 Vnode key 去老的 map 映射中查找,如果没有能够复用的,把新的 Vnode 转换成真实的 DOM 插入在老的起始 DOM 之前;如果能够复用老的 DOM,移动老的 DOM 插入在老的起始 DOM 之前,新的起始索引递增
当一方起始索引大于结束索引时,说明已经有一方对比完成:
- 如果老的还有剩余的,删除老的真实 DOM
- 如果新的还有剩余的,创建真实的 DOM 并插入在对应的位置
1、头头对比

2、尾尾对比

3、老头新尾对比、老尾新头对比

4、乱序对比

虚拟 DOM
虚拟 DOM 是用 JS 对象 来模拟 DOM 结构的一种技术,具有以下优势:
- 跨平台:可以在不同平台上使用相同的代码
- 减少真实 DOM 操作:通过 diff 算法减少不必要的 DOM 操作,提升性能
- 差异化更新:当数据发生变化时,通过对比虚拟 DOM 的差异,只更新必要的部分
js
function vnode(vm, tag, props, children, text, key) {
return {
vm,
tag,
props,
children,
text,
key,
};
}Vue.mixin
Vue.mixin 是一种代码复用机制,允许将相同的逻辑抽取到一个对象中,然后混入到多个组件中。
优点:
- 可以把相同的逻辑抽取,进行代码复用
缺点:
- 在组件中,数据来源不明确,数据会被覆盖
合并策略:
- 生命周期:生命周期在合并的时候,会按照执行的先后顺序排序形成一个队列
- 非生命周期:如果属性相同,后者会覆盖掉前者
mixin 案例
js
// 第一次有生命周期,在合并的时候。Vue.options = {};
Vue.mixin({
beforeCreate() {
console.log("beforeCreate-hulei1");
},
name: "hl",
});
// 后续在合并的时候可能没有生命周期,直接返回上一个次
Vue.mixin({
age: 18,
});
Vue.mixin({
beforeCreate() {
console.log("beforeCreate-hulei2");
},
name: "hulei",
});
const app = new Vue({
el: "#app",
beforeCreate() {
console.log("beforeCreate-hulei3");
},
data() {
return {
name: "hulei",
};
},
});mixin 全局合并
js
export function initGlobalAPI(Vue) {
Vue.options = {};
Vue.mixin = function (options) {
// 全局合并
this.options = mergeOptions(this.options, options);
};
}mixin 组件 options 合并
js
export function initMixin(Vue) {
// initoptionsAPI初始化
Vue.prototype._init = function (options) {
const vm = this;
// 合并mixin的options
vm.$options = mergeOptions(Vue.options, options);
// 触发生命周期钩子
callHook(vm, "beforeCreate");
};
}mergeOptions 实现
js
const lifecycles = {};
["beforeCreate", "created", "beforeMount", "mounted"].forEach((method) => {
// 策略模式
lifecycles[method] = function (preLifecycle, postLifecycle) {
if (!preLifecycle && postLifecycle) {
// 第一次有生命周期合并的时候,上一个肯定是空对象,也需要形成一个队列
return [postLifecycle];
}
// 第二次到N次,如果有生命周期按照前后顺序进行排序,如果没有直接把上一次的返回
if (postLifecycle) {
return preLifecycle.concat(postLifecycle);
}
return preLifecycle;
};
});
export function mergeOptions(preOptions, postOptions) {
const opts = {};
// 循环处理上一个
for (const key in preOptions) {
mergeField(key);
}
// 循环处理上一个
for (const key in postOptions) {
// 相同的属性上一个循环已经处理
if (!preOptions.hasOwnProperty(key)) {
mergeField(key);
}
}
function mergeField(key) {
if (lifecycles[key]) {
// 处理生命周期形成队列
opts[key] = lifecycles[key](preOptions[key], postOptions[key]);
} else {
// 优先使用后者
opts[key] = postOptions[key] || preOptions[key];
}
}
return opts;
}