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
posted @ 2020-06-03 16:01  青S衫%  阅读(1336)  评论(0编辑  收藏  举报