vue源码学习16:生命周期和组件原理

1,093 阅读4分钟

前言

几年前,刚刚入行的时候,天天背Vue的生命周期,生怕面试的时候面到。现在想想,也是有点好笑。这导致我在学最近这几节课程的时候,异常的兴奋和激动。

然而,悲剧的是,生命周期和组件原理的流程过于冗长,我自己在跟着课程实现简单原理的时候,总是走不通,搞不懂。也就导致这篇博客姗姗来迟。

我感觉,今天的文章依旧写不明白,但是事已至此,还是尝试写一下吧,vue里面的diff算法的课,因为这篇博文已经搁置好几天了。

image.png

下面就乌龟垫床脚--硬撑着写了。

生命周期和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点了,顶不住了。