Vant的Toast组件写的比较有意思,最大的特点是简洁的函数式调用。本文将通过梳理Toast组件实现函数式调用的整套逻辑来介绍Vue插件的使用场景。
首先,我们找到Toast引入的地方,相关代码如下:
import { Toast } from 'vant'
Vue.use(Toast)
很明显我们发现这是一个Vue插件,我们看下这个插件做了什么:
import VueToast from './Toast';
Toast.install = function () {
Vue.use(VueToast);
}
Vue.prototype.$toast = Toast;
export default Toast;
这里的VueToast其实就是我们日常写的SFC(single file component)。一般来说,我们会把SFC作为局部或者全局组件注册,并在父组件中调用。然而这里并没有这样使用,而是声明了一个Toast函数,我们看看Toast函数做了什么:
import _extends from "@babel/runtime/helpers/esm/extends";
function Toast(options) {
if (options === void 0) {
options = {};
}
var toast = createInstance(); // should add z-index if previous toast has not disappeared
if (toast.value) {
toast.updateZIndex();
}
options = parseOptions(options);
options = _extends({}, currentOptions, defaultOptionsMap[options.type || currentOptions.type], options);
if (process.env.NODE_ENV === 'development' && options.mask) {
console.warn('[Vant] Toast: "mask" option is deprecated, use "overlay" option instead.');
}
options.clear = function () {
toast.value = false;
if (options.onClose) {
options.onClose();
options.onClose = null;
}
if (multiple && !isServer) {
toast.$on('closed', function () {
clearTimeout(toast.timer);
queue = queue.filter(function (item) {
return item !== toast;
});
removeNode(toast.$el);
toast.$destroy();
});
}
};
_extends(toast, transformOptions(options));
clearTimeout(toast.timer);
if (options.duration > 0) {
toast.timer = setTimeout(function () {
toast.clear();
}, options.duration);
}
return toast;
}
这部分代码包含三个步骤:第一,创建toast实例;第二,扩展Toast的options;第三,将扩展后的options合入toast实例中并返回实例。
非常有意思的是,Vant中调用Vue组件的方式跟我们日常的方式完全不一样。日常的开发是在SFC中去更改vue组件实例的属性,例如:
<template>
<div />
</template>
<script>
export default {
data() {
a: 1
},
created() {
this.a = 2
}
}
</script>
而Vant则是拿到vue组件实例后,用_extend函数去批量更新组件实例的属性。_extend函数来自babel,源码如下:
export default function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
其实就是批量更新Vue实例的相关属性。这是题外话,只是因为跟日常开发方式不一样,个人觉得比较有意思,记录一下。
言归正传,虽然我们看懂了Toast源码的三步操作。但是为什么做了这三步操作之后,页面就能够展示出Toast组件呢?我们得看看createInstance函数到底做了什么:
function createInstance() {
/* istanbul ignore if */
if (isServer) {
return {};
}
queue = queue.filter(function (item) {
return !item.$el.parentNode || isInDocument(item.$el);
});
if (!queue.length || multiple) {
var toast = new (Vue.extend(VueToast))({
el: document.createElement('div')
});
toast.$on('input', function (value) {
toast.value = value;
});
queue.push(toast);
}
return queue[queue.length - 1];
}
createInstance函数对Vue扩展,获得了一个新的、基于Vue的构造函数。实例化以后,实例被推到队列中并返回,这就是这套代码的全部逻辑了。
虽然创建了实例,但是我们并没有看到实例对应的DOM被放到文档中。实例对应的DOM是什么时候被放到文档中,并展示出弹窗的呢?其实我们上面已经看到了:
_extends(toast, transformOptions(options));
由于options合并了defaultOptions,而defaultOptions中有getContainer这一属性:
var defaultOptions = {
icon: '',
type: 'text',
// @deprecated
mask: false,
value: true,
message: '',
className: '',
overlay: false,
onClose: null,
onOpened: null,
duration: 2000,
iconPrefix: undefined,
position: 'middle',
transition: 'van-fade',
forbidClick: false,
loadingType: undefined,
getContainer: 'body',
overlayStyle: null,
closeOnClick: false,
closeOnClickOverlay: false
}
getContainer的初始值为undefined,通过_extends方法处理后,getContainer的值变为body。我们通过Toast.js==>PopupMixin==>PortalMixin(顺便说一句,这个调用栈极其蛋疼)找到了getContainer的watcher:
watch: {
getContainer: 'portal'
},
mounted: function mounted() {
if (this.getContainer) {
this.portal();
}
},
methods: {
portal: function portal() {
var getContainer = this.getContainer;
var el = ref ? this.$refs[ref] : this.$el;
var container;
if (getContainer) {
container = getElement(getContainer);
} else if (this.$parent) {
container = this.$parent.$el;
}
if (container && container !== el.parentNode) {
container.appendChild(el);
}
if (afterPortal) {
afterPortal.call(this);
}
}
}
getContainer的值更新以后,触发了portal回调,将组件实例对应的DOM推到了文档中,页面完成Toast组件内容展示。这就是Toast组件的整个逻辑过程。
通过梳理Vant的Toast组件,我们发现了一种实现Vue插件的思路:先创建一个SFC,然后引入该SFC,对Vue构造函数进行extend,获得一个新的、基于我们想使用的组件的构造函数。实例化后通过函数传参更新组件属性,即可达到在父组件中通过props传参给子组件同样的效果。
这种函数式调用相比全局组件来说更加简洁。正如Vant的Toast组件,一般被用于展示弹窗这样需要高频调用的全局组件。如果在开发业务中发现某个全局组件被频繁使用而且有很多props需要不断更新,那么就可以考虑使用Vue插件。