defer-promise搞定异步弹窗组件

最近在看vue组件库 Plain UI 时,发现一个比较有趣的异步弹框组件写法,操作如下:

<div class="dialog">
    <input type="text" name="message">
    <button type="button">cancel</button>
    <button type="button">confirm</button>
</div>
(async ()=>{
	let message = await openDialog();
	console.log("弹窗信息",message)
})()

在异步函数中打开弹窗 openDialog 方法,当用户点击 confirm 按钮后,弹窗关闭,返回输入框信息。

openDialog方法可以很方便的通过promise实现,不过在看组件库源码时,发现对方是用defer 实现的,在promise兼容性还不是很好的时代 JQuery 就已经有 deferred.promise() 方法了,这里顺便也做了温习。

defer方法:

const defer = () => {
	const def = {}
	def.promise = new Promise((resolve, reject) => {
	  def.resolve = resolve
	  def.reject = reject
	})
	return def
}

defer方法其实返回的也是一个promise,并且将 resolvereject 方法拆开,这样我们就可以选择在适当的时机调用 resolve 或者 reject 方法了。

const dialogController = () => {
  let dfd = null

  const confirmBtn = document.getElementById('confirm')
  // 点击确定按钮
  confirmBtn.addEventListener('click', () => {
  // 隐藏弹窗
    dialogEl.hide()
  // resolve输入框信息给用户
    dfd.resolve(inputEl.value)
  })

  return () => {
    dfd = defer()
    dialogEl.show()
    return dfd.promise
  }
}

获得打开弹窗promise方法:

const openDialog = dialogController()

控制弹窗的打开,在异步函数中如果用户点击了弹窗确定按钮,关闭弹窗,获得输入信息。

const controlBtn = document.getElementById('control')
controlBtn.addEventListener('click', async () => {
  const message = await openDialog()
  console.log("弹窗输入框信息:",message)
})

这种方式可以方便我们封装常用的业务组件,之前在看 axios.cancel 源码时里面也是使用这种套路,灵活且实用。

通过 defer 方式实现的弹窗代码:

<html>
  <head>
    <title>defer promise</title>
    <style>
      .dialog {
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        display: flex;
        position: fixed;
        align-items: center;
        pointer-events: none;
        justify-content: center;
      }
      .dialog .mask {
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        position: absolute;
        opacity: 0;
        transition: 0.3s;
        background-color: rgba(0, 0, 0, 0.4);
      }

      .dialog-content {
        padding: 20px;
        transition: 0.2s;
        opacity: 0;
        transform: scale(0.95);
        background-color: #fff;
      }

      .dialog.visible {
        pointer-events: all;
      }
      .dialog.visible .mask {
        opacity: 1;
      }
      .dialog.visible .dialog-content {
        opacity: 1;
        transform: scale(1);
      }
    </style>
  </head>
  <body>
    <div class="container">
      <button id="control">显示弹窗</button>
      <div class="dialog" id="dialog">
        <div class="mask" onclick="this.parentNode.classList.remove('visible')"></div>
        <div class="dialog-content">
          <input type="text" id="content" />
          <button id="confirm">确定</button>
        </div>
      </div>
    </div>

    <script>
      const defer = () => {
        const def = {}
        def.promise = new Promise((resolve, reject) => {
          def.resolve = resolve
          def.reject = reject
        })

        return def
      }

      ;(() => {
        const inputEl = document.getElementById('content')
        const dialogEl = document.getElementById('dialog')
        dialogEl.show = () => dialogEl.classList.add('visible')
        dialogEl.hide = () => dialogEl.classList.remove('visible')

        const dialogController = () => {
          let dfd = null
          const confirmBtn = document.getElementById('confirm')
          confirmBtn.addEventListener('click', () => {
            dialogEl.hide()
            dfd.resolve(inputEl.value)
          })

          return () => {
            dfd = defer()
            dialogEl.show()
            return dfd.promise
          }
        }

        const openDialog = dialogController()
        const controlBtn = document.getElementById('control')
        controlBtn.addEventListener('click', async () => {
          const message = await openDialog()
          console.log('弹窗输入框信息:', message)
        })
      })()
    </script>
  </body>
</html>