createVnodeEmitter.mjs

//warnMisbinding, 綁定時檢查常見之錯誤寫法, 否則處理函式靜默不被呼叫
//  修飾字.once、.capture、.passive使監聽鍵名加上前綴~、!、&, 事件名大小寫或連字號不符, 漏寫@而成靜態屬性, 元素上使用.native
//  編譯進處理函式內之修飾字(.stop、.prevent、.self、.ctrl等)於執行期無法低成本偵測, 不在此檢查
function warnMisbinding(vnode, name, dirName, listeners, on) {
    let msgs = []
    let keys = [...Object.keys(listeners || {}), ...Object.keys(on || {})]
    for (let k of keys) {
        if (k === name) {
            continue
        }
        if (k.replace(/^[~!&]+/, '') === name) {
            msgs.push(`@${name}不支援.once、.capture、.passive修飾字, 處理函式不會被呼叫`)
        }
        else if (k.toLowerCase().replace(/-/g, '') === name) {
            msgs.push(`事件名須為@${name}, 目前為@${k}, 處理函式不會被呼叫`)
        }
    }
    let data = (vnode && vnode.data) || {}
    if (data.attrs && Object.prototype.hasOwnProperty.call(data.attrs, name)) {
        msgs.push(`${name}="${data.attrs[name]}"為靜態屬性, 應為@${name}`)
    }
    let isComponent = !!(vnode && vnode.componentOptions)
    if (!isComponent && data.nativeOn && data.nativeOn[name]) {
        msgs.push(`元素上之@${name}.native無效, 應為@${name}`)
    }
    for (let m of msgs) {
        console.warn(`[v-${dirName}] ${m}`)
    }
}


//getListeners, 取vnode之監聽物件: 元件標籤之自訂事件在componentOptions.listeners, 其data.on為.native監聽(Vue建立元件vnode時將nativeOn移入data.on); 元素之監聽在data.on
function getListeners(vnode) {
    return {
        listeners: (vnode && vnode.componentOptions && vnode.componentOptions.listeners) || null,
        on: (vnode && vnode.data && vnode.data.on) || null,
    }
}


/**
 * 自訂指令觸發所在vnode之v-on事件處理函式, 每個綁定建立一個, 解除綁定時dispose
 *
 * 呼叫Vue建立之監聽器(invoker)本身而非其fns: invoker會依序呼叫多個處理函式(陣列), 並經Vue之錯誤處理(祖先errorCaptured、Vue.config.errorHandler)處理同步錯誤與Promise拒絕
 * 只保留監聽物件而不保留整個vnode, 避免持有綁定當下之子樹; 指令於宿主重繪後以update換成新vnode之監聽物件, 故綁定後才新增、移除或改名之監聽亦會跟隨
 * 元件標籤同名兩者皆有時只呼叫自訂事件, 沒有自訂事件時才呼叫.native監聽
 * dispose後不再呼叫, 以處理解除綁定後才完成之非同步偵測
 *
 * @param {Object} vnode 輸入指令所在之vnode
 * @param {String} name 輸入事件名稱字串
 * @param {String} dirName 輸入指令名稱字串, 用於警告訊息
 * @param {Object} [opt={}] 輸入設定物件, opt.warn給false時不檢查錯誤寫法(供重建時避免重複警告), 預設true
 * @returns {Object} 回傳物件, emit(data)觸發事件, update(vnode)改用新vnode之監聽物件, dispose()作廢
 */
function createVnodeEmitter(vnode, name, dirName, opt = {}) {
    let { listeners, on } = getListeners(vnode)
    let alive = true

    //warnMisbinding
    if (opt.warn !== false) {
        warnMisbinding(vnode, name, dirName, listeners, on)
    }

    //emit
    let emit = (data) => {
        if (!alive) {
            return
        }
        let h = (listeners && listeners[name]) || (on && on[name])
        if (typeof h === 'function') {
            h(data)
        }
    }

    //update, 宿主重繪後改用新vnode之監聽物件; 同名監聽跨重繪沿用同一invoker, 故只有新增、移除或改名之監聽需要此步
    let update = (vnodeNew) => {
        if (!alive) {
            return
        }
        let r = getListeners(vnodeNew)
        listeners = r.listeners
        on = r.on
    }

    //dispose
    let dispose = () => {
        alive = false
        listeners = null
        on = null
    }

    return {
        emit,
        update,
        dispose,
    }
}


export default createVnodeEmitter