vant 1.6X 版本 Toast 单例问题
一、背景
在项目中使用 toast 发现不是 vant 官方所说的默认单例模式
1、操作
在 created 中多次调用 Toast.loading() 发现生成了多个 toast 元素
使用 Toast.clear() 只能关掉一个
2、项目中对 Toast 封装
Toast.loading 以及 Toast.clear 封装
Vue.prototype.$loading = msg => {
Toast.loading({
mask: true,
message: msg || langMap[lang]['loading'],
duration: 0,
position: 'center'
})
}
Vue.prototype.$close = () => {
Toast.clear()
}
3、多次调用
created() {
// 第一次
// 此处 loading 可能是其他方法调用的
// 这里用来制造问题出现的场景
this.$loading()
this.init()
},
methods: {
init() {
try {
// 第二次
this.$loading()
} catch (e) {
console.log(e)
} finally {
this.$close()
}
}
}
4、结果
浏览器页面创建了多个 Toast 元素
二、查看源码实现
1、Toast.loading
1.1 根据注册代码,可以看出,Toast.loading 实际还是调用了 Toast 本身
var createMethod = function createMethod(type) {
return function (options) {
return Toast(_extends({
type: type
}, parseOptions(options)));
};
};
// 注册 loading 方法
['loading', 'success', 'fail'].forEach(function (method) {
Toast[method] = createMethod(method);
});
1.2 查看 Toast 函数实现
可以看到主要是以 createInstance 来实现创建 toast
function Toast(options) {
// ...
// 忽略以上逻辑
var toast = createInstance(); // should add z-index if previous toast has not disappeared
// 忽略以下逻辑
// ...
// ...
// ...
return toast;
}
1.3 再去查看 createInstance 函数实现
这里使用 !queue.length || multiple || !isInDocument(queue[0].$el)
作为判断条件来实现 单例模式
!queue.length 若创建的 toast 列表中,长度为 0,则可进入创建逻辑
multiple 若支持创建多个 toast ,则可进入创建逻辑
!isInDocument(queue[0].$el) 若在页面 body 中,检测不到 toast 元素,则可进入创建逻辑
// vant 1.6.16
// vant/es/toast/index.js
function createInstance() {
/* istanbul ignore if */
if (isServer) {
return {};
}
if (!queue.length || multiple || !isInDocument(queue[0].$el)) {
// 手动加入的调试语句
// debugger
console.log('\nqueue :>> ', queue.length);
console.log('multiple :>> ', multiple);
console.log('!isInDocument(queue[0].$el :>> ', !isInDocument(queue[0] && queue[0].$el))
console.log(document.body.contains(queue[0] && queue[0].$el))
var toast = new (Vue.extend(VueToast))({
el: document.createElement('div')
});
queue.push(toast);
}
return queue[queue.length - 1];
} // transform toast options to popup props
// vant/es/utils/index.js
export function isInDocument(element) {
return document.body.contains(element);
}
在页面中查看输出语句,发现第二次调用 this.$loading 的时候,也进入了创建逻辑,明显不符合我们的要求
根据输出条件,判定是 !isInDocument(queue[0] && queue[0].$el)
为 true 造成逻辑命中,进入了创建逻辑
该语句表示若在页面 body 中不存在 toast 元素,则为 true
说明判断是不存在 toast ,但是前面确实已经创建了一个 toast
1.4 那么为什么呢?
在条件语句下面 加入 debugger 语句,进行断点调试:
1、执行创建一次 toast 之后,第二次进入创建逻辑,在 console 控制台 打印 queue
2、此时 queue 中只有一个元素,是 Vue 组件对象,打开 里面的 $el 发现不是一个 dom 对象
且此时页面空白,说明还未挂载
3、这时候执行 document.body.contains(element) 肯定会返回 false
因为我们在 created 中调用 Toast.loading , 此时组件还没被挂载, toast 元素还没有插入到 dom 中,问题出在这里
2. Toast.clear
Toast.clear 的时候若传入参数为 true,则关闭所有的 toast
若不支持创建多个,则关闭 toast 列表中的第一个 toast
Toast.clear = function (all) {
if (queue.length) {
if (all) {
queue.forEach(function (toast) {
toast.clear();
});
queue = [];
} else if (!multiple) {
queue[0].clear();
} else {
queue.shift().clear();
}
}
};
三、解决
知道问题是因为组件未挂载造成条件判断问题,因而创建了多个 toast 实例
那么相应的解决方法有两个
1、不在 created 中进行 loading 操作,或者不多于一次 loading 操作
2、Toast.clear() 方法,加入 true 参数: Toast.clear(true),关闭所有的 toast 实例
四、vant 更新修复
因为历史原因,项目使用的是 vant 1.6x 的版本
发现 vant 有 bug 还想去提个 pr 或者 issue
但是查看了 vant 2.x 版本已经修复了这个问题
其实就是去掉了上面那个 isInDocument 方法判断
// vant 2.2.16
function createInstance() {
/* istanbul ignore if */
if (isServer) {
return {};
}
// 此处去掉了 1.6x 版本中 isInDocument 导致的问题
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];
} // transform toast options to popup props
都读到最后了、留下个建议如何