前言
几年前,刚刚入行的时候,天天背Vue的生命周期,生怕面试的时候面到。现在想想,也是有点好笑。这导致我在学最近这几节课程的时候,异常的兴奋和激动。
然而,悲剧的是,生命周期和组件原理的流程过于冗长,我自己在跟着课程实现简单原理的时候,总是走不通,搞不懂。也就导致这篇博客姗姗来迟。
我感觉,今天的文章依旧写不明白,但是事已至此,还是尝试写一下吧,vue里面的diff算法的课,因为这篇博文已经搁置好几天了。
下面就乌龟垫床脚--硬撑着写了。
生命周期和mixin
- beforeCreate
- created
- beforeMount
- mounted
- ...
在vue中,生命周期的钩子函数有十个,这里就不一一列举了,今天主要讲的是大致的实现原理。
在这里先提一个问题,如果mixin中有一个beforeMounted,组件中有一个beforeMounted。该生命周期会被执行几次?如果执行多次,顺序如何?
Vue.mixin({
beforeCreate() {
console.log("before create1")
}
})
Vue.mixin({
beforeCreate() {
console.log("before create2")
}
})
let vm = new Vue({
// 按找个套路,Vue就是一个类
el: '#app',
beforeCreate() {
console.log("before create3")
}
});
答案:
// 执行结果
before create1
before create2
before create3
由此可见,如果mixin中有生命周期钩子函数,组件也有,他们都会被执行,且全局的会被优先执行。
initGlobalApi
Vue在初始化的时候,会调用一个initGlobalApi方法,用来初始化全局的API,比如:
- mixin
- component
- filter
- directive
- ...
mixin
官方对mixn有如下描述:
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
也就意味着,会把mixin中的数据和组件中的数据进行融合。
export function initGlobalApi(Vue) {
Vue.options = {} // options用于存放全局配置,每个组件初始化的时候,都会和options选项进行合并
Vue.mixin = function (options) {
/**
* options是一个对象
* {
* beforeMounted() {xxx}
* }
* 这里是把 当前的options和传入的options进行合并
*/
this.options = mergeOptions(this.options, options)
return this;
}
}
在这段代码里面,最最关键的就是mergeOptions方法。这个方法里面,对当前的options和mixin传入的options进行了融合,并返回了mixin本身,这样mixin就可以链式调用了。
mergeOptions 和 策略模式
let lifeCycleHooks = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'destoryed',
'beforeDestory'
]
let strats = {} // 这里存放着各种策略
定义一个包含所有生命周期的数组,然后定义一个空对象,用来存放策略。
function mergeHook(parentVal, childVal) {
if (childVal) {
if (parentVal) {
return parentVal.concat(childVal)
} else {
return [childVal]
}
} else {
return parentVal
}
}
lifeCycleHooks.forEach(hook => {
strats[hook] = mergeHook
})
定义一个mergeHook的方法,对传入的父元素和子元素进行合并。
然后把这个mergeHook的方法,挂载strats的每一个生命周期策略上。
export function mergeOptions(parent, child) {
const options = {} // 存放合并后的结果
// 如果父亲里面有元素,则进行合并
// 合并以儿子的值为准
for (let key in parent) {
mergeField(key)
}
/**
* 如果儿子里面有:
* 1. 如果父亲里面已经有了,就不进行合并了。
* 2. 如果父亲没有,则进行合并
*/
for (let key in child) {
if (parent.hasOwnProperty(key)) {
continue
}
mergeField(key)
}
function mergeField(key) {
/**
* 获取父亲和儿子的值,只用策略模式进行合并
*
*/
let parentVal = parent[key]
let childVal = child[key]
// 通过策略模式,做不同的事情
if (strats[key]) {
// 如果有对应的策略,就调用对应的策略就好
options[key] = strats[key](parentVal, childVal)
} else {
/**
* 如果父亲和儿子都是对象,则使用扩展运算符进行合并
* 扩展运算符...后面对象的元素会覆盖前面元素的值
* 否则直接使用儿子的值
*/
if (isObject(parentVal) && isObject(childVal)) {
options[key] = { ...parentVal, ...childVal }
} else {
options[key] = child[key] || parent[key]
}
}
}
return options;
}
通过这里的合并结束之后,vue的生命周期会被合并成这样一种情况:
beforeCreated: [fn1, fn2, fn3]
然后,多个钩子函数会被依次执行。
生命周期钩子何处、何时执行?
在lifeCycle中,有一个callHook方法,执行生命周期的钩子函数,循环遍历,依次执行。
/**
* 调用钩子函数
* 调用的事哪个实例的,哪个钩子
* 对象上的数组
*/
export function callHook(vm, hook) {
let handlers = vm.$options[hook]
// 找到Hooks就依次执行就行了
// beforeCreate: [fn1, fn2, fn3]
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm)
}
}
}
beforeCreate 和 created
当Vue中的mixin已经被混入,但是数据还没有被监听,data没有被挂载的时候执行beforeCreate。
当initState方法被执行,组件的数据被监听、watche、computed都被处理成watcher之后,执行created方法。
Vue.prototype._init = function (options) {
...
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
...
}
beforeMounte 和 mounted
当页面第一次加载之前,会调用beforeMount,此时reder函数已经生成,实例已经配置完成,完成el和data的初始化。
当watcher被实例化,_render方法被执行,vnode被渲染成真实Dom挂载到页面节点的时候,mounted被执行。
export function mountComponent(vm, el) {
// 数据变化后,会再次调用更新函数
let updateComponent = () => {
// 1. 通过render生成虚拟dom
vm._update(vm._render()) // 后续更新可以调动updateComponent方法
// 2. 虚拟Dom生成真实Dom
}
callHook(vm, 'beforeMount')
new Watcher(vm, updateComponent, () => {
...
}, true)
callHook(vm, 'mounted')
}
component的执行流程
同样是在globalApi中,关于component有如下代码
Vue.options._base = Vue // 无论后续创建多少个子类,都可以通过_base找到父类
Vue.options.components = {} // 组件可能不是一个,可能会注册多个
Vue.component = function (id, definition) {
/**
* id: 组件的名称
* definition: 组件的定义
* 为了父子关系,还要创建一个子类,保证组件隔离
* 每个组件产生一个类,继承父类
*/
definition = this.options._base.extend(definition)
this.options.components[id] = definition
}
/**
* extend的作用:产生一个类
*/
Vue.extend = function (opts) {
// 产生一个继承与Vue的类,并且身上应该有父类的所有的功能
const Super = this
// 根据类产生了一个类,这个类继承自父类
// 这个继承要重写constructor
const Sub = function VueComponent(options) {
// 调用当前组件的init方法
this._init(options)
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
// 子类的options要包含全局的options和自己的opt
Sub.options = mergeOptions(Super.options, opts)
return Sub
}
-
_base在option上面,挂载了一个Vue,这样无论后期创建多少个子类,都可以通过_base找到Vue -
components 用来存放组件,组件会注册多个,而不是一个
-
extend 的作用是将一个对象转换成一个类,这个类继承自父类,并且生成的子类的optioins压迫包含父类的options
vdom和patch.js
在vdom中,判断这个组件的名称是不是html内置的,如果不是,则说明是自定义组件,则执行createComponent方法。
export function createElement(vm, tag, data = {}, ...children) {
if (isReservedTag(tag)) {
return vnode(vm, tag, data, data.key, children, undefined)
} else {
const Ctor = vm.$options.components[tag]
return createComponent(vm, tag, data, data.key, children, Ctor)
}
}
function createComponent(vm, tag, data, key, children, Ctor) {
// 最核心的是要的:组件的构造函数
// 这里的Ctor有可能是对象,有可能是函数
// 如果是对象,就需要把它保证成一个函数
if (isObject(Ctor)) {
Ctor = vm.$options._base.extend(Ctor)
}
console.log('Ctor', Ctor)
data.hook = {
init(vnode) {
// 初始化组件
let vm = vnode.componentInstance = new Ctor({
_isComponent: true
}) // new Sub 会用此选项和组价的配置进行合并
console.log('vmmmmm', vm)
vm.$mount()
}
}
return vnode(vm, `vue-component-${tag}`, data, key, undefined, undefined, { Ctor, children })
}
在patch中,主要对组件进行渲染到页面。
export function patch(oldVnode, vnode) {
if (!oldVnode) {
return createElm(vnode); // 如果没有el元素,那就直接根据虚拟节点返回真实节点
}
if (oldVnode.nodeType == 1) {
// 用vnode 来生成真实dom 替换原本的dom元素
const parentElm = oldVnode.parentNode; // 找到他的父亲
let elm = createElm(vnode); //根据虚拟节点 创建元素
// 在第一次渲染后 是删除掉节点,下次在使用无法获取
parentElm.insertBefore(elm, oldVnode.nextSibling);
parentElm.removeChild(oldVnode)
return elm
}
}
// 创建真实节点的
function createComponent(vnode) {
let i = vnode.data; // vnode.data.hook.init
if ((i = i.hook) && (i = i.init)) {
i(vnode); // 调用init方法,传入vnode值
}
if (vnode.componentInstance) { // 有属性说明子组件new完毕了,并且组件对应的真实DOM挂载到了componentInstance.$el
return true;
}
}
function createElm(vnode) {
let { tag, data, children, text, vm } = vnode
if (typeof tag === 'string') { // 元素
// debugger;
if (createComponent(vnode)) {
// 返回组件对应的真实节点
return vnode.componentInstance.$el;
}
vnode.el = document.createElement(tag); // 虚拟节点会有一个el属性 对应真实节点
children.forEach(child => {
vnode.el.appendChild(createElm(child))
});
} else {
vnode.el = document.createTextNode(text);
}
return vnode.el
}
好了,今天学习就到这里了。感觉自己也讲的云里雾里,以后再做更新。晚上11点了,顶不住了。