JS 中的鉤子(Hook)實現

1. Hook 簡介

Hook 這個詞不少人都聽過,就算不知道,也直接或間接地用到過。它一般是系統或框架開放出來供上層應用開發者執行代碼的一種方式。例如,Vue 的生命週期鉤子,本質就是框架內部在對應時機調用了組件定義的鉤子函數;此外,Webpack 所使用 tapable 更是將 hook 的應用發揮的淋漓盡致,tapable 最值得稱讚的就是,它對 hook 作了很好的抽象和分類。數組

2. Hook 的表現形式

對於開發者,Hook 一般以鉤子函數形式存在。開發者註冊鉤子函數,系統或者框架決定在何時調用鉤子函數,某種意義上,它和事件回調函數有殊途同歸之妙。從數據結構的設計上,咱們能夠使用鍵值對(散列表,JS中的普通對象)來表示系統提供的鉤子,其中,鍵表明鉤子名稱,值是鉤子函數數組。promise

例如,下面的代碼包含一個init鉤子,這個鉤子註冊了3個鉤子函數:fn1, fn2, fn3markdown

const hooks = {
  init: [fn1, fn2, fn3]
}
複製代碼

若是咱們將 Hook 看做是一種和隊列同樣的抽象數據類型(ADT),那麼 Hook 的操做集合包含註冊(Register)調用(Call)。簡單實現就是:數據結構

// 註冊鉤子
function regHook(hookName, hookFn) {
  if (!hooks[hookName]) hooks[hookName] = []
  hooks[hookName].push(hookFn)
}
// 調用鉤子
function callHook(hookName, ...args) {
  hooks[hookName].forEach(fn => fn(...args))
}
複製代碼

3. Hook 的分類

3.1 串行和並行

根據鉤子函數的執行順序,能夠分爲:併發

  • 串行鉤子:根據註冊順序調用鉤子,後面的鉤子必須等到前面的鉤子執行完後才能調用,串行鉤子能夠是同步的,也能夠是異步的
  • 並行鉤子:按順序調用鉤子,但可同時執行,即後面的鉤子不用等到前面的鉤子執行完成,顯然,並行鉤子必須是異步的

3.2 同步和異步

根據鉤子函數的執行方式,能夠分爲:框架

  • 同步鉤子: 鉤子執行會阻塞主線程,鉤子函數返回即表明鉤子執行結束
  • 異步鉤子: 鉤子執行不會阻塞主線程,鉤子函數返回不表明鉤子執行結束,須要使用回調函數或者使用promise.then 來獲取鉤子執行結束的消息

4. Hook 調用

註冊鉤子比較簡單,只需將鉤子函數按順序加入鉤子函數數組便可。而調用鉤子,須要根據鉤子類型來採起不一樣調用方法。異步

4.1 同步鉤子的調用

同步鉤子的調用是最簡單,按順序調用一遍鉤子函數便可,而且只有串行執行。async

function callSync(hookName, ...args) {
  hooks[hookName].forEach(fn => fn(...args))
}
複製代碼

4.2 異步鉤子的調用

異步鉤子的調用要分爲串行和並行。函數

4.2 異步串行

4.2.1 基於回調函數
function callAsyncSeries(hookName, done, ...args) {
  const fns = hooks[hookName]
  let i = fns.length
  let next = done
  while(i) {
    let fn = fns[--i]
    let _next = next
    next = () => fn(...args, _next)
  }
  next()
}
複製代碼

使用示例:this

regHook('asyncSer', (a, b, done) => { setTimeout(() => { console.log('timout 1', a, b); done() }, 1000) })
regHook('asyncSer', (a, b, done) => { setTimeout(() => { console.log('timout 2', a, b); done() }, 2000) })
regHook('asyncSer', (a, b, done) => { setTimeout(() => { console.log('timout 3', a, b); done() }, 3000) })

callAsyncSeries('asyncSer', () => { console.log('done') }, 'aa', 'bb')

// timout 1 aa bb
// timout 2 aa bb
// timout 3 aa bb
// done
複製代碼
4.2.2 基於 Promise
function callPromiseSeries(hookName, ...args) {
  return new Promise(resolve => {
    const fns = hooks[hookName]
    let i = fns.length
    let next = resolve
    while(i) {
      let fn = fns[--i]
      let _next = next
      next = () => fn(...args).then(_next)
    }
    next()
  })
}
複製代碼

使用示例:

regHook('promiseSer', (a, b) => {
  return new Promise(resolve => 
    setTimeout(() => { console.log('promiseSer 1', a, b); resolve() }, 2000)
  )
})

regHook('promiseSer', (a, b) => {
  return new Promise(resolve => 
    setTimeout(() => { console.log('promiseSer 2', a, b); resolve() }, 3000)
  )
})

regHook('promiseSer', (a, b) => {
  return new Promise(resolve => 
    setTimeout(() => { console.log('promiseSer 3', a, b); resolve() }, 1000)
  )
})

callPromiseSeries('promiseSer', 'aa', 'bb').then(() => { console.log('done') })

// promiseSer 1 aa bb
// promiseSer 2 aa bb
// promiseSer 3 aa bb
// done
複製代碼

4.3 異步並行鉤子的調用

4.3.1 基於回調函數
function callAsyncParallel(hookName, done, ...args) {
  const fns = hooks[hookName]
  let count = fns.length
  let _done = () => {
    count--
    if (count === 0) {
      done()
    }
  }
  fns.forEach(fn => fn(...args, _done))
}

// 限制併發數
function callAsyncParallelN(hookName, done, N, ...args) {
  const fns = hooks[hookName]
  let count = fns.length
  let cur = 0
  let limit = N < fns.length ? N : fns.length
  let _done = () => {
    count--
    if (count === 0) {
      done()
    } else if (cur < fns.length) {
      fns[cur++](...args, _done)
    }
  }
  for (; cur < limit; cur++) {
    fns[cur](...args, _done)
  }
}
複製代碼

使用示例:

regHook('asyncParallel', (a, b, done) => { setTimeout(() => { console.log('asyncParallel 1', a, b); done() }, 1000) })
regHook('asyncParallel', (a, b, done) => { setTimeout(() => { console.log('asyncParallel 2', a, b); done() }, 1000) })
regHook('asyncParallel', (a, b, done) => { setTimeout(() => { console.log('asyncParallel 3', a, b); done() }, 1000) })

callAsyncParallel('asyncParallel', () => { console.log('done') }, 'aa', 'bb')
callAsyncParallelN('asyncParallel', () => { console.log('done') }, 2, 'aa', 'bb')
複製代碼
4.3.2 基於 Promise
function callPromiseParallel(hookName, ...args) {
  return new Promise(resolve => {
    const fns = hooks[hookName]
    let count = fns.length
    let _done = () => {
      count--
      if (count === 0) {
        resolve()
      }
    }
    fns.forEach(fn => fn(...args).then(_done))
  })
}

// 限制併發數
function callPromiseParallelN(hookName, N, ...args) {
  return new Promise(resolve => {
    const fns = hooks[hookName]
    let count = fns.length
    let cur = 0
    let limit = N < fns.length ? N : fns.length
    let _done = () => {
      count--
      if (count === 0) {
        resolve()
      } else {
        if (cur < fns.length) {
          fns[cur++](...args).then(_done)
        }
      }
    }
    for (; cur < limit; cur++) {
      fns[cur](...args).then(_done)
    }
  })
}
複製代碼

使用示例:

regHook('promiseParallel', (a, b) => {
  return new Promise(resolve => 
    setTimeout(() => { console.log('promiseParallel 1', a, b); resolve() }, 1000)
  )
})

regHook('promiseParallel', (a, b) => {
  return new Promise(resolve => 
    setTimeout(() => { console.log('promiseParallel 2', a, b); resolve() }, 1000)
  )
})

regHook('promiseParallel', (a, b) => {
  return new Promise(resolve => 
    setTimeout(() => { console.log('promiseParallel 3', a, b); resolve() }, 1000)
  )
})

callPromiseParallel('promiseParallel', 'aa', 'bb').then(() => { console.log('done') })
callPromiseParallelN('promiseParallel', 2, 'aa', 'bb').then(() => { console.log('done') })
複製代碼

5. 代碼封裝

5.1 同步鉤子

class Hook {
  constructor() {
    this.hookFns = []
  }
  reg(fn) {
    this.hookFns.push(fn)
  }
  call(...args) {
    this.hookFns.forEach(fn => fn(...args))
  }
}
複製代碼

5.2 異步回調鉤子

class AsyncHook extends Hook {
  call(...args, done) {
    const fns = this.hookFns
    let i = fns.length
    let next = done
    while(i) {
      let fn = fns[--i]
      let _next = next
      next = () => fn(...args, _next)
    }
    next()
  }
  callParallel(...args, done) {
    const fns = this.hookFns
    let count = fns.length
    let _done = () => {
      count--
      if (count === 0) {
        done()
      }
    }
    fns.forEach(fn => fn(...args, _done))
  }
  callParallelN(...args, done) {
    const fns = this.hookFns
    let count = fns.length
    let cur = 0
    let limit = N < fns.length ? N : fns.length
    let _done = () => {
      count--
      if (count === 0) {
        done()
      } else if (cur < fns.length) {
        fns[cur++](...args, _done)
      }
    }
    for (; cur < limit; cur++) {
      fns[cur](...args, _done)
    }
  }
}
複製代碼

5.3 異步 Promise 鉤子

class PromiseHook extends Hook {
  call(...args) {
    return new Promise(resolve => {
      const fns = this.hookFns
      let i = fns.length
      let next = resolve
      while(i) {
        let fn = fns[--i]
        let _next = next
        next = () => fn(...args).then(_next)
      }
      next()
    })
  }
  callParallel(...args) {
    return new Promise(resolve => {
      const fns = this.hookFns
      let count = fns.length
      let _done = () => {
        count--
        if (count === 0) {
          resolve()
        }
      }
      fns.forEach(fn => fn(...args).then(_done))
    })
  }
  callParallelN(...args) {
    return new Promise(resolve => {
      const fns = this.hookFns
      let count = fns.length
      let cur = 0
      let limit = N < fns.length ? N : fns.length
      let _done = () => {
        count--
        if (count === 0) {
          resolve()
        } else {
          if (cur < fns.length) {
            fns[cur++](...args).then(_done)
          }
        }
      }
      for (; cur < limit; cur++) {
        fns[cur](...args).then(_done)
      }
    })
  }
}
複製代碼
相關文章
相關標籤/搜索