Appearance
vue2.0
Vue 挂载流程
- 1、创建Vue实例传入配置项,对配置项进行
mergeOptions
合并,并且初始化options
配置项 - 2、初始化组件上的
props
、data
、computed
,watch
- 3、如果有el属性,会调用$mount,通过
compile 函数
把模版转换成render 函数
- 4、模版转换完成后执行
mountComponent
,实例化 渲染Watcher,将组件渲染、更新的控制权交给 Watcher - 5、当render渲染在被调用时,如果响应式数据有取值操作,dep订阅器会
依赖收集
当前的Watcher - 6、最终调用 patch函数 将 Vnode 转换成真实的 dom 渲染在页面(同时每一个虚拟节点 el 属性会对应一个真实的 dom,后续更新时复用)
- 7、当响应式数据发生变化后,就会触发 dep 依赖通知渲染 watcher 更新视图
Vue 数据响应式处理
- vue2 是通过 Object.defineProperty 进行属性代理
- 1、如果data数据
不是对象
或者已经被代理过
,不做任何处理,直接返回 - 2、如果是对象,会给当前的对象创建一个dep订阅器,让当前的对象也具有
收集Watcher
的能力,同时也会挂载一个ob
属性指向当前的 observer 实例(主要是用来解决数组问题) - 3、递归遍历对象的每一个属性,给每一个属性新增 getter/setter 拦截,同时也是给每一个 属性 关联一个 dep 订阅器,用来收集、触发Watcher
- 4、如果是数组,会对
"push", "pop", "unshfit", "shift", "reverse", "sort", "splice"
进行 AOP 拦截,同时会遍历数组,让数组中的对象进行代理 - 5、组件在渲染时,如果有取值操作会触发 get 拦截,会让 dep 订阅器去收集当前的渲染 watcher
(如果值是一个对象,会让当前的对象也收集渲染的 watcher,解决数组改变更新视图)
- 6、当响应式数据发生变化时,会触发 setter拦截,让 dep 去通知收集的 watcher 更新视图(如果新的值是一个对象,也会进行代理)
数据的特殊处理
- 如果数组有
push
、pop
,unshfit
,shift
,reverse
,sort
,splice
操作,会进行AOP
拦截 - 1、首先会通过call调用数组原有的函数,更新数组的值
- 2、在判断是不是有
新增操作
(push、unshift、splice)、如果有会对新增的选项,并且是对象需要代理 - 3、最终找到自身的 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 来实现的,数据发生变化后,触发回调执行
- 1、watch 在初始化时,会实例化一个 Watcher,将回调执行权交给它,watcher实例本身会存储
监听的值
和更新的watch回调
- 2、Watcher 会立马
调用 get 函数
,对监听的属性进行访问
,此时触发 getter拦截
,让当前的属性触发 dep 订阅器依赖收集当前的 Watcher - 3、当监听的属性发生变化后,触发 dep 去通知收集的 Watcher 执行 watch 回调
js
Vue.prototype.$watch = function (key, handler) {
new Watcher(this, key, handler, { user: true });
};
Vue computed 计算属性
- computed 也是基于 Watcher 来实现
- 1、computed 在初始化时并
不会立马执行
,而是把所有的计算属性存储在组件实例上 - 2、当计算属性被访问时,如果被
缓存过
直接返回缓存的值 - 3、如果没有缓存,调用计算属性函数
evaluate
,进行取值操作(触发 getter拦截,让dep 会收集当前的计算属性 watcher)
,并且标记为已经缓存 - 6、如果计算属性里的响应式数据发生改变,会触发 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 可以监听多个值
computed 只有在获取数据的时候,才会触发依赖收集(计算属性 watcher)
computed 支持缓存
computed 名称不能和 data 里的对象重复
watch 名称只能和 data 里的对象一致
watch 只能监听一个值
watch 初始化会触发依赖收集(watch watcher)
watch 不支持缓存
Vue diff(五种策略)
只会对比同级节点,不考虑跨层级情况,如果
节点类型
、key
不一样直接删除,创建新的节点vue2 采用的是双指针的策略
1、
头头对比
,如果 tag、key 相同复用老节点,新老起始索引递增
,对比 props 和子元素2、
尾尾对比
,如果 tag、key 相同复用老节点,新老起始索引递减
3、
老头新尾对比
,如果 tag、key 相同复用老节点(把老的移动 dom 插入到尾部),老的起始索引递增
、新的结束索引递减
4、
老尾新头对比
,如果 tag、key 相同复用老节点(把老的移动 dom 插入到头部),老的结索引递减
、新的起始索引递增
5、
乱序对比
,用新的起始的Vnode key
,去老的 map 映射 去查找,如果没有能够复用的,把新的 Vnode 转换成真实的 dom,插入在老的起始 dom 之前,如果能够复用老的 dom,移动老的 dom 插入在老的起始 dom 之前,新的起始索引递增
6、当前有一方起始索引大于结束的索引时,说明已经有一方对比完成
7、如果老的还有剩余的,删除老的真实 dom
7、如果新的还有剩余的,创建真实的 dom,插入在对应的位置
1、头头对比
2、尾尾对比
3、老头新尾对比、老尾新头对比
4、乱序对比
虚拟 DOM
- 用 js 对象来模拟 DOM 结构
- 可以实现跨平台
- 减少真实 dom 的操作
- 改生变化后可以差异化更新
js
function vnode(vm, tag, props, children, text, key) {
return {
vm,
tag,
props,
children,
text,
key,
};
}
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;
}