vue2源码解析(六):生命周期的合并
你好呀,我是小九,很高兴见到你。
摘要
vue2源码学习之路。
查看之前的文章
在上一篇文章中,介绍了关于异步更新nextTick的相关内容。
简单回顾一下,异步更新的核心就是“暂存”,将watcher暂存入一个队列中,等到数据修改完毕,统一更新。
使用这种方式,减少更新次数,避免浪费性能。
这篇文章继续学习生命周期的合并。
项目结构
初始化mixin
1. 生命周期
生命周期,可以理解为回调函数,创建后不会马上调用,等适当的时候再调用。
这种方式也就是发布订阅模式,先订阅好,后续会触发。
在定义周期函数时,可能写在组件上,也可能通过mixin的方式混入。
mixin的功能是混合,混入一些公共方法,简单来说就是合并选项。
使用mixin时,一种是通过实例上的mixin方法,这个mixin只对当前实例本身有效;另一种是通过Vue.mixin方法,这个mixin是全局的方法,可以在任何实例上使用。
两者的本质是相同的。
2. 全局mixin函数
由于生命周期函数可能不止定义一次, 因此就需要进行生命周期的合并,最终会合并成一个数组。
创建文件,路径src-->global-api-->mixin.js
全局定义的属性和方法,最终都放在了Vue.options
上。
合并时,会进行两次合并操作:
首先需要将用户传入的方法,合并到Vue.options上。
之后用户使用new Vue创建实例时,new中传入的选项和Vue.options需要再次合并。
创建全局的mixin方法,合并选项options。
这里先只合并生命周期,data、watch、computed等合并暂不做介绍。
export function initMixin(Vue) {
Vue.mixin = function (mixin) {
// 此处的this,指的是Vue类
// mergeOptions函数用来合并选项
this.options = mergeOptions(this.options, mixin);
return this;
};
}
src-->global-api-->index.js
在initGlobalAPI中调用initMixin方法
export function initGlobalAPI(Vue) {
Vue.options = Object.create(null);
initMixin(Vue);
Vue.nextTick = nextTick;
}
合并
1. mergeOptions函数
创建文件,路径src-->util-->options.js
创建mergeOptions
函数,接收两个参数,要合并到哪里parent
以及要合并的属性child
。
合并时,可能有两种情况,一种是parent有值,child没有值;一种是parent没有值,child有值。
因此,需要分别循环parent和child,合并每个字段。
最终合并的结果是一个对象。
export function mergeOptions(parent, child) {
const options = {};
for (const key in parent) {
mergeField(key);
}
for (const key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
return options;
}
2. mergeField函数
合并字段时,需要针对不同字段不同的处理。
为了避免繁琐的if...else
判断,源码中使用了 策略模式。
创建一个策略对象,针对不同的字段,创建不同的策略函数。
举个例子,如果使用if...else
if(key === 'data'){...}
else if(key === 'computed'){...}
else if(key === 'watch'){...}
...
如果使用“策略模式”
const strats = Object.create(null);
strats['data'] = function() {}
strats['computed'] = function() {}
strats['watch'] = function() {}
创建mergeField函数
function mergeField(key) {
const strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key]);
}
如果当前的key,有对应的策略,就使用当前策略进行合并,合并后的结果放到options上。
如果没有,就使用默认策略,默认策略是以child为准。
创建默认策略
const defaultStrat = function (parentVal, childVal) {
return childVal === undefined ? parentVal : childVal;
};
3. 生命周期策略
定义一个数组,存放生命周期钩子。
直接从源码中复制过来。
创建文件,路径src-->shared-->constant.js
export const LIFECYCLE_HOOKS = [
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestroy",
"destroyed",
"activated",
"deactivated",
"errorCaptured",
"serverPrefetch",
"renderTracked",
"renderTriggered",
];
循环遍历LIFECYCLE_HOOKS,不同的生命周期使用的相同的策略。
LIFECYCLE_HOOKS.forEach((hook) => {
strats[hook] = mergeLifecycleHook;
});
创建mergeLifecycleHook
函数
mergeLifecycleHook
接收两个参数,parent的key和child的key
合并时分为几种情况
childVal没有值,parentVal有值,此时直接采用parentVal;
childVal有值,parentVal有值,将parentVal和childVal拼接合并;
childVal有值,parentVal没有值,将childVal包装成一个空数组。
export function mergeLifecycleHook(parentVal, childVal) {
if (childVal) {
if (parentVal) {
res = parentVal.concat(childVal);
} else {
res = [childVal];
}
} else {
res = parentVal;
}
return res ? dedupeHooks(res) : res;
}
最终的合并结果是一个数组。
源码中使用的是三元运算符。
export function mergeLifecycleHook(parentVal, childVal) {
const res = childVal
? parentVal
? parentVal.concat(childVal)
: isArray(childVal)
? childVal
: [childVal]
: parentVal;
return res ? dedupeHooks(res) : res;
}
dedupeHooks函数(去重)
function dedupeHooks(hooks) {
const res = [];
for (let i = 0; i < hooks.length; i++) {
if (res.indexOf(hooks[i]) === -1) {
res.push(hooks[i]);
}
}
return res;
}
至此,通过Vue.mixin传入的生命周期合并完毕。
举例
Vue.mixin({
created() {
console.log(1);
},
});
Vue.mixin({
created() {
console.log(2);
},
});
const app = new Vue({
el: "#app",
data: {
a: 100,
obj: {
name: "小明",
age: 10,
},
},
created() {
console.log(3);
},
});
第一次合并,parent是Vue.options这个空对象,child是第一个Vue.mixin中的参数
{
created() {
console.log(1);
},
}
第二次合并,parent是Vue.options.created,里面中已经存入了一个函数,child是第二个Vue.mixin中的参数,将这个参数加入数组中。
最终结果
4. 合并用户传入的生命周期
除了全局Vue.mixin传入的生命周期,用户在new Vue时也定义了一个生命周期函数。
所以需要将用户传入的和全局的options合并。
在init函数中调用mergeOptions
方法。
Vue.prototype._init = function (options) {
...
vm.$options = mergeOptions(vm.constructor.options, options);
...
};
vm.constructor相当于Vue。
调用生命周期
合并完成后后,就可以在不同的地方调用不同的生命周期。
创建生命周期初始化函数。
生命周期中的this都是当前实例。
export function callHook(vm, hook) {
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
handlers[i].call(vm);
}
}
}
在不同的时期调用不同的生命周期
Vue.prototype._init = function (options) {
...
// 初始化之前调用beforeCreate
callHook(vm, "beforeCreate");
// 初始化状态
initState(vm);
callHook(vm, "created");
...
};
export function mountComponent(vm, el) {
// 挂载之前调用beforeMount
callHook(vm, "beforeMount");
const updateComponent = () => {
vm._update(vm._render());
};
...
// 挂载后调用mounted
callHook(vm, "mounted");
}
测试
Vue.mixin({
created() {
console.log('global created 1');
},
mounted() {
console.log("global created 1");
},
});
Vue.mixin({
created() {
console.log('global created 2');
},
});
const app = new Vue({
el: "#app",
data: {
a: 100,
obj: {
name: "小明",
age: 10,
},
},
beforeCreate() {
console.log("vm beforeCreate");
},
created() {
console.log("vm created");
},
mounted() {
console.log("vm mounted");
},
});
当前的vm.$options
生命周期函数执行打印结果
总结
-
生命周期的合并,就是将定义的生命周期函数合并成为一个数组,然后在适当的时候按照顺序调用每一个函数。
-
mixin的本质就是合并选项。
-
Vue.options中保存的是全局的配置项
-
vm.$options保存的是当前实例的配置项。
完整代码,请移步gihub vue2-source
文章就到这里,下次再见!
转载自:https://juejin.cn/post/7151933503163072548