路客的文档

技术总结和生活记录

简单分析 element ui 中在循环中调用 this.$alert 消息提示只会显示最后一个的问题

需求背景

有一个需求,使用element-ui 中的$alert 方法提示 用户几条信息,不能一次性提示。仅能一条一条的提示,提示完第一条,点击确定后如果还有待提示消息,就弹出提示第二条,以此类推,直到消耗完所有需要提示的消息结束;现在模拟复现一下这个需求的期望如下图:

result.gif

那这还不简单??? 我们听完需求心中已经想好了代码怎么写了。于是我们摩拳擦掌 说干就干, 一顿 C & V 操作,于是有了下面的代码:


 showMsg(){
    for(let i = 0; i < 4; i++){
      this.$alert('show message !'+ i,'提示')      
    }
 }
 
复制代码

然后满心欢喜,胸有成竹的一运行,发现事与愿违,来看下运行的结果:

r2.gif

我们发现并没有按我们的预期来执行,而是直接只显示了最后提示,就结束了,那么是什么问题导致了这个问题的出现呢:

问题分析

那我们本着弄清楚事实的心态来搞清楚问题发生的原因,那就翻一翻element-ui 中 $alert 方法到底是怎么实现的?

首先找到 $alert 的入口,他的入口位于 element/blob/dev/src/index.js Line:199


Vue.prototype.$alert = MessageBox.alert;

复制代码

我们看到他在Vue 的原型上扩展了 $alert 方法,方法指向了 MessageBox.alert

那我们继续跟进 MessageBox.alert Line:161 看看他到底是怎么回事?

我们截取其中的代码片段:


MessageBox.alert = (message, title, options) => {
  if (typeof title === 'object') {
    options = title;
    title = '';
  } else if (title === undefined) {
    title = '';
  }

  return MessageBox(merge({
    title: title,
    message: message,
    $type: 'alert',
    closeOnPressEscape: false,
    closeOnClickModal: false
  }, options));

};
复制代码

我们看到这个方法代码比较简洁,先是对参数进行了一些处理,最后把处理后的参数merge到默认的一些参数上,返回了 MessageBox 这个方法,根据他的命名我们大概能猜到 他是一个类;

那我们继续看 MessageBox 类的实现:


const MessageBox = function(options, callback) {

  // 判断是否为服务端 ? 跳过 ...
  if (Vue.prototype.$isServer) return; 
  
  // 对传入的options 参数进行了判断,是不是 String 或者 vNode ,然后处理参数。这里主要是 处理 title 或 message 
  if (typeof options === 'string' || isVNode(options)) { 
    options = {
      message: options
    };

    if (typeof arguments[1] === 'string') {
      options.title = arguments[1];
    }
  //   处理了参数 callback , 吧options.callback 赋给 callback  ...  
  } else if (options.callback && !callback) {
    callback = options.callback;
  }
  // 判断是否 可以使用 Promise 对象,
  if (typeof Promise !== 'undefined') {
    // 就把参数包装到 Promise 中 push 进入 msgQueue 队列中...  
    return new Promise((resolve, reject) => { // eslint-disable-line
      msgQueue.push({
        options: merge({}, defaults, MessageBox.defaults, options),
        callback: callback,
        resolve: resolve,
        reject: reject
      });
      // 执行 showNextMsg() 方法
      showNextMsg();
    });

  } else {
    // 不支持 promise 就直接 吧参数 push 进队列,再执行 showNextMsg  
    msgQueue.push({
      options: merge({}, defaults, MessageBox.defaults, options),
      callback: callback
    });
    showNextMsg();
  }
};
复制代码

从上面的代码中 我们知道,

  • 处理了传入的参数
  • 维护了一个 msgQueue 队列,用于存放 不同场景(是否支持Promise)下的参数
  • 都执行了showNextMsg 方法

那我们继续向下 查看 showNextMsg 方法的实现:


const initInstance = () => {
  instance = new MessageBoxConstructor({
    el: document.createElement('div')
  });
  instance.callback = defaultCallback;

};
复制代码

const showNextMsg = () => {

  // 首先对 instance 进行了判断,如果没有就 初始化一个,初始化的方法 在上面;由此可见,instance 是一个单例 
  if (!instance) {
    initInstance();
  }
  instance.action = '';
  // 判断 instance 没有显示 或者 存在 closeTimer  的场景
  if (!instance.visible || instance.closeTimer) {
    if (msgQueue.length > 0) { // 判断了队列长度
      currentMsg = msgQueue.shift(); // 移除队列第一项,拿到参数信息
      let options = currentMsg.options; // 下面对参数一顿处理
      for (let prop in options) {
        if (options.hasOwnProperty(prop)) {// 吧参数 复制给 instance 
          instance[prop] = options[prop];
        }
      }

      if (options.callback === undefined) { // 判断有没有 callback 没有就使用 默认的 callback
        instance.callback = defaultCallback;
      }

      let oldCb = instance.callback; // 从新对 callback 进行制定
      instance.callback = (action, instance) => {
        oldCb(action, instance);
        showNextMsg(); // 进行了递归调用,消耗队列
      };
      // 判断了 message 是不是 vnode  如果是 就使用默认的 slot 渲染 ....  
      if (isVNode(instance.message)) {
        instance.$slots.default = [instance.message];
        instance.message = null;
      } else {
        delete instance.$slots.default;
      }

      // 继续处理 参数,如果 这些参数没设置,就全部给他设置为 true
      ['modal', 'showClose', 'closeOnClickModal', 'closeOnPressEscape', 'closeOnHashChange'].forEach(prop => {
        if (instance[prop] === undefined) {
          instance[prop] = true;
        }
      });
      
      // 吧这个 el 挂载到 body 上面, 此时候的 el 还是隐藏的
      document.body.appendChild(instance.$el);
      
      // 使用Vue 的nextTick 异步更新队列,去设置 instance 的显示
      Vue.nextTick(() => {
        instance.visible = true;
      });
    }
  }
};
复制代码

到这里基本就分析完了 他使用 Vue.nextTick 异步更新队列 去设置了 instance.visible = true;

异步更新队列 默认使用了 Promise.then 微任务 去处理callback

所以 我们在循环中调用 $alert , 是在最后 微任务队列清空的时候 去设置了 instance.visible = true;

so. 我们只看到最后一条提示。

故此 我们已经基本知道了问题的原因,那我对于我们的需求就立马能想到了处理方案, 我们仅需吧 每一条消息 放到 宏任务队列中去,即可:

那我们上代码:

showMsg(){
    for(let i = 0; i < 4; i++){
      let timer = setTimeout(()=>{
        this.$alert('show message !'+ i,'提示')
        clearTimeout(timer)
        timer = null    
      })  
    }
}
复制代码

这样就成功完成了需求所描述的功能 ...

非常的nice

DEMO演示

感受一下这个 demo 吧

最后的最后

下面请上我们今天的主角:有请小趴菜

小趴菜.jpeg