前言
通过手写Vue2源码,更深入了解Vue;
在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅;
另外我会编写一些开发文档,阐述编码细节及实现思路;
源码地址:手写Vue2源码
Mixin混入原理
Vue.mixin可以往全局options中混入一些配置。 先思考一下,下列代码中的beforeCreate会如何合并?
Vue.mixin({
beforeCreate() {
console.log('beforeCreated1')
}
})
Vue.mixin({
beforeCreate() {
console.log('beforeCreated2')
}
})
let vm = new Vue({
el: '#root',
beforeCreate() {
console.log('beforeCreated3')
},
})
答案是:Vue.mixin会把options混入到全局的Vue.options;vm实例中的options会和全局/父类的options进行合并。最后的结果是{el: '#root', beforeCreate:[()=>{console.log('beforeCreated1')},()=>{console.log('beforeCreated2')},()=>{console.log('beforeCreated3')}]}
,生命周期会合并成一个数组。
下面我将一步一步实现options的合并以及生命周期的调用。
Vue.mixin()
思考一下实现思路:
- Vue有一个全局的配置,Vue.mixin(options)会把options与全局的配置进行合并
- 合并时包含生命周期、data、methods、components、computed等的合并,它们的合并方法可能不尽相同,需要考虑如何进行合并,以及代码的可扩展性
- 组件实例需要和全局options进行合并
先创建全局的options以及定义Vue.mixin()
方法:
// src/index.js
import { initGlobalApi } from "./global-api/index";
function Vue(options) {
this._init(options);
}
initGlobalApi(Vue);
export default Vue;
// src/global-api/index.js
import initMixin from "./mixin";
export function initGlobalApi(Vue) {
// 每个组件初始化的时候都会和Vue.options选项进行合并
Vue.options = {}; // 用来存放全局属性,例如Vue.component、Vue.filter、Vue.directive
initMixin(Vue);
}
// src/global-api/mixin.js
import { mergeOptions } from '../util/index'
export default function initMixin(Vue) {
Vue.mixin = function(mixin) {
// this 指向 VUe,this.options即Vue.options
// 将mixin合并到Vue.options中,而组件会和Vue.options合并,所以最后会把mixin合并到组件中
this.options = mergeOptions(this.options,mixin)
return this;
}
}
总结一下:
- 全局的配置为 Vue.options,初始状态是空对象
- Vue.mixin()中调用
mergeOptions(this.options,mixin)
将mixin与全局的options进行合并
options合并的核心方法是mergeOptions()
mergeOptions()
直接看代码吧:
// src/util/index.js
const strategies = {} // 存放各种合并策略
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed'
]
LIFECYCLE_HOOKS.forEach((hook) => {
strategies[hook] = mergeHook
})
// 生命周期的合并方式(合并结果是数组)
function mergeHook(parentVal, childVal) {
if (childVal) {
if (parentVal) {
return parentVal.concat(childVal)
} else {
// 第一次合并结果是数组;因为第一次合并时(在定义Vue.mixin时),Vue.options是空对象{},parentVal是undefined,会走这一步
return [childVal]
}
} else {
return parentVal
}
}
// 配置合并方法(将child合并到parent中)
export function mergeOptions(parent, child) {
const options = {} // 合并后的结果
for (let k in parent) {
mergeFiled(k)
}
for (let k in child) {
// 对child中有,parent中没有的属性进行合并
if (!parent.hasOwnProperty(k)) {
mergeFiled(k)
}
}
function mergeFiled(key) {
let parentVal = parent[key]
let childVal = child[key]
// 1. 使用策略模式合并生命周期
if (strategies[key]) {
options[key] = strategies[key](parentVal, childVal)
} else {
// 生命周期以外的属性合并,例如computed、data、methods、watch等
if (isObject(parentVal) && isObject(childVal)) {
options[key] = {...parentVal, ...childVal}
} else {
// 如果合并的一方为function或基本数据类型,儿子有则以儿子为准,否则以父亲为准
options[key] = childVal || parentVal
}
}
}
return options
}
总结一下做了什么:
- 在
mergeOptions()
方法中遍历parent与child中所有的属性,对parent中的所有属性,以及parent中没有、child中有的属性,调用mergeFiled(key)
进行合并 - 在
mergeFiled()
中使用策略模式合并生命周期,使用覆盖、扩展的方式合并method、computed、watch、data等属性。- 如果命中了策略,则调用不同的策略进行合并
- 如果没有命中策略;对于对象则使用对象的合并,对于其他类型则直接采用childVal进行覆盖。
options合并时的策略模式
为什么要使用策略模式?
合并的属性有很多,比如生命周期、methods、data等等,它们的合并方式是不同的,使用策略模式针对不同的属性定义不同的合并策略,方便扩展,低耦合。
使用策略模式合并生命周期:
// src/util/index.js
const strategies = {} // 存放各种合并策略
export const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed'
]
LIFECYCLE_HOOKS.forEach((hook) => {
strategies[hook] = mergeHook
})
// 生命周期的合并方式(合并结果是数组)
function mergeHook(parentVal, childVal) {
if (childVal) {
if (parentVal) {
return parentVal.concat(childVal)
} else {
// 第一次合并结果是数组;因为第一次合并时(在定义Vue.mixin时),Vue.options是空对象{},parentVal是undefined,会走这一步
return [childVal]
}
} else {
return parentVal
}
}
小结:
- 定义一个对象 —— strats,存放不同的策略
- 往strats添加八种生命周期的策略,都是mergeHook
- 当
strats[key]
命中了任意策略,则执行相应的方法 - 易扩展:如果以后要添加methods的合并策略,只需要在strats中添加methods属性及相应合并方法即可
生命周期合并的流程与结果:
5. Vue.mixin()
第一次合并生命周期时,是与Vue.options
(是一个空对象)进行合并的,返回的是一个长度为1的数组
6. 当第二次调用Vue.mixin()
,并且上一次Vue.mixin()
中有定义相同的生命周期,则会进行数组与函数的合并,得到一个长度为2的数组
组件实例的options如何合并
前面Vue.mixin()
是与全局options的合并,当我们使用组件时,vm实例需要将全局的options合并到实例中。
// src/init.js
import { mergeOptions } from "./util/index";
Vue.prototype._init = function (options) {
vm.$options = mergeOptions(vm.constructor.options, options);
}
小结:
- 合并方法同样使用的是mergeOptions
- 合并的对象是
vm.constructor.options
而非Vue.options;原因通常情况下vm.constructor就是Vue,但是如果当前组件是使用extends继承而来,则需要与继承的组件进行合并。
vue中的生命周期
生命周期的本质:在不同的代码执行阶段,调用对应的生命周期钩子函数。
先定义一个方法来调用生命周期钩子:
// src/lifecycle.js
export function callHook(vm, hook) {
// vm.$options[hook]经过mergeOptions合并之后,是一个数组,所以需要遍历数组
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); //生命周期里面的this指向当前实例
}
}
}
beforeCreate
与created
的调用,在组件的状态初始化过程中:
// src/init.js
import { callHook } from './lifecycle'
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate");
initState(vm); // 初始化状态,包括initProps、initMethod、initData、initComputed、initWatch等
callHook(vm, "created");
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
beforeMount
与mounted
的调用,在组件的挂载过程中:
// src/lifecycle.js
export function mountComponent(vm, el) {
vm.$el = el;
callHook(vm, "beforeMount");
let updateComponent = () => {
vm._update(vm._render());
};
new Watcher(
vm,
updateComponent,
() => {
console.log('视图更新了')
callHook(vm, "beforeUpdate");
},
true
);
callHook(vm, "mounted");
}
系列文章
- 手写Vue2源码(一)—— 环境搭建
- 手写Vue2源码(二)—— 数据劫持
- 手写Vue2源码(三)—— 模板编译
- 手写Vue2源码(四)—— 初次渲染
- 手写Vue2源码(五)—— 观察者模式
- 手写Vue2源码(六)—— 异步更新及nextTick
- 手写Vue2源码(七)—— 侦听属性
- 手写Vue2源码(八)—— 计算属性
- 手写Vue2源码(九)—— 混入原理与生命周期
- 手写Vue2源码(十)—— 组件原理
- 手写Vue2源码(十一)—— diff算法
- 手写Vue2源码(十二)—— keep-alive
- 手写Vue2源码(十三)—— 全局API
- vue-router原理解析
- vuex原理解析
- vue3原理解析