likes
comments
collection
share

vue2源码解析(六):生命周期的合并

作者站长头像
站长
· 阅读数 18

你好呀,我是小九,很高兴见到你。

摘要

vue2源码学习之路。

查看之前的文章

专栏:vue2源码解析

在上一篇文章中,介绍了关于异步更新nextTick的相关内容。

简单回顾一下,异步更新的核心就是“暂存”,将watcher暂存入一个队列中,等到数据修改完毕,统一更新。

使用这种方式,减少更新次数,避免浪费性能。

这篇文章继续学习生命周期的合并。

项目结构

vue2源码解析(六):生命周期的合并

初始化mixin

1. 生命周期

vue2源码解析(六):生命周期的合并

生命周期,可以理解为回调函数,创建后不会马上调用,等适当的时候再调用。

这种方式也就是发布订阅模式,先订阅好,后续会触发。

在定义周期函数时,可能写在组件上,也可能通过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中的参数,将这个参数加入数组中。

最终结果

vue2源码解析(六):生命周期的合并

4. 合并用户传入的生命周期

除了全局Vue.mixin传入的生命周期,用户在new Vue时也定义了一个生命周期函数。

所以需要将用户传入的和全局的options合并。

在init函数中调用mergeOptions方法。

Vue.prototype._init = function (options) {
    ...
    vm.$options = mergeOptions(vm.constructor.options, options);
    ...
};

vm.constructor相当于Vue。

调用生命周期

合并完成后后,就可以在不同的地方调用不同的生命周期。

vue2源码解析(六):生命周期的合并

创建生命周期初始化函数。

生命周期中的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

vue2源码解析(六):生命周期的合并

生命周期函数执行打印结果

vue2源码解析(六):生命周期的合并

总结

  1. 生命周期的合并,就是将定义的生命周期函数合并成为一个数组,然后在适当的时候按照顺序调用每一个函数。

  2. mixin的本质就是合并选项。

  3. Vue.options中保存的是全局的配置项

  4. vm.$options保存的是当前实例的配置项。

完整代码,请移步gihub vue2-source


文章就到这里,下次再见!