本文做者:傅雲貴(網易有道技術團隊)html
在上一篇文章 (見 Vue 中 MathJax 的使用與渲染的監聽 (上) ) 中講述了在 Vue 組件中如何使用 MathJax,卻應對不了產品的新需求:vue
待 MathJax 渲染(Typeset)數學公式後,用戶使用瀏覽器的打印功能打印網頁。web
在此需求中,須要判斷全部組件實例的 MathJax Typeset 是否完成。api
如何監聽全部組件實例中的 MathJax Typeset 是否完成呢?
瀏覽器
根據「分而治之」的思想,很容易想到:若要判斷多個組件實例是否 MathJax typeset 完成,只要判斷每個組件實例是否 MathJax typeset 完成。markdown
在組件中,咱們可使用如下方法監聽 MathJax Typeset 是否完成。app
@Component({})
class SomeComponent extends Vue {
private mathJax: typeof MathJax | null = null
private needTypeset: boolean = false
isTypesetFinished: boolean = false
private callMathJaxTypeset(): void {
const { mathJax } = this
if (mathJax) {
const { typesetElement } = this.$refs
mathJax.Hub.Queue(['Typeset', MathJax.Hub, typesetElement])
mathJax.Hub.Queue(() => {
this.isTypesetFinished = true
})
} else {
this.needTypeset = true
}
}
created(): void {
const mathJax = await loadMathJax()
this.mathJax = mathJax
if (this.needTypeset) {
this.callMathJaxTypeset()
}
}
mounted(): void {
this.isTypesetFinished = false
this.callMathJaxTypeset()
}
updated(): void {
this.isTypesetFinished = false
this.callMathJaxTypeset()
}
}
複製代碼
在組件實現 MathJax Typeset 是否完成過程當中,使用了MathJax.Hub.Queue, 那麼這個 Queue 到底是什麼呢?async
翻閱 MathJax 的源碼,能夠發現 MathJax.Hub.Queue 源於 MathJax.Callback.Queue。ide
// ...
var QUEUE = BASE.Object.Subclass({
//
// Create the queue and push any commands that are specified
//
Init: function() {
// ...
},
//
// Add commands to the queue and run them. Adding a callback object
// (rather than a callback specification) queues a wait for that callback.
// Return the final callback for synchronization purposes.
//
Push: function() {
//...
},
//
// Process the command queue if we aren't waiting on another command
//
Process: function(queue) {
// ...
},
//
// Suspend/Resume command processing on this queue
//
Suspend: function() {
// ...
},
Resume: function() {
// ...
},
//
// Used by WAITFOR to restart the queue when an action completes
//
call: function() {
// ...
},
wait: function(callback) {
// ...
},
})
// ...
BASE.Callback.Queue = QUEUE
// ...
var HUB = BASE.Hub
// ...
HUB.queue = BASE.Callback.Queue()
MathJax.Hub = {
// ...
Queue: function() {
return this.queue.Push.apply(this.queue, arguments)
},
//...
}
複製代碼
A 「callback」 is a function that MathJax calls when it completes an action that may occur asynchronously (like loading a file). Many of MathJax’s functions operate asynchronously, and MathJax uses callbacks to allow you to synchronize your code with the action of those functions. The MathJax.Callback structure manages these callbacks.oop
MathJax.Callback.Queue 是一個隊列,負責管理一系列 callback (即任務)的執行。MathJax.Hub.Queue 能夠理解爲 MathJax.Callback.Queue 的一個實例。
因爲 MathJax.Hub.Queue 中 callback 是存儲在隊列的,並不會當即執行;且在實際使用過程發現, typeset 渲染數學公式過程並不太快。那麼,組件 typeset 渲染的監聽初步實現 章節中的實現,在多組件實例、屢次updated的狀況下,MathJax.Hub.Queue 中等待任務可能會出現如下狀況:
序號 | MathJax.Hub.Queue 中等待的任務 |
---|---|
... | ... |
N+1 | someComponent1 typeset callback |
N+2 | someComponent1 isTypesetFinished = true callback |
N+3 | someComponent2 typeset callback |
N+4 | someComponent2 isTypesetFinished = true callback |
N+5 | someComponent1 typeset callback |
N+6 | someComponent1 isTypesetFinished = true callback |
N+... | ... |
如何解決以上問題呢?可能的方式有:
僅在 web app 頂層組件中調用 MathJax 進行 typeset, 即只有一個組件實例調用 MathJax.Hub.Queue 去渲染數學公式
自實現隊列,接管 MathJax.Hub.Queue中的隊列功能, MathJax.Hub.Queue 僅僅被用於 typeset
很明顯:
決定採用方案 2 後,主要開發瞭如下幾個模塊:
實現TypesetQueue 類,接管MathJax.Hub.Queue 的隊列功能, MathJax.Hub.Queue 僅僅被用於 typeset。
import { EventEmitter } from 'eventemitter3'
interface ExecutedItem {
uid: string
componentId: string
refNames: string[]
isTopPriority: boolean
startTime: number
endTime: number
}
interface WaitingItem {
uid: string
componentId: string
refNames: string[]
refs: Element[]
afterRender?: (info: ExecutedItem) => void
isTopPriority: boolean
}
const TypesetQueueEvent = {
addTask: Symbol('add-task'),
cancelTask: Symbol('cancel-task'),
finishTask: Symbol('finish-task'),
clearTasks: Symbol('clear-tasks'),
}
class TypesetQueue extends EventEmitter {
private topPriorityQueue: WaitingItem[]
private normalQueue: WaitingItem[]
private executed: ExecutedItem[]
private isRunning: boolean
private mathJax: typeof MathJax | null
constructor() {
super()
this.topPriorityQueue = []
this.normalQueue = []
this.executed = []
this.isRunning = false
this.mathJax = null
}
setupMathJax(mathJax: typeof MathJax): void {
if (this.mathJax) {
return
}
this.mathJax = mathJax
this.runTask()
}
private buildUniqueId(componentId: string, refNames: string[]): string {
const names = [...refNames]
names.sort()
const joinded = names.join('-_')
return `${componentId}-${joinded}`
}
private removeTask(uid: string): boolean {
const { normalQueue, topPriorityQueue } = this
let index = normalQueue.findIndex((item) => {
return item.uid === uid
})
if (index > -1) {
normalQueue.splice(index, 1)
return true
}
index = topPriorityQueue.findIndex((item) => {
return item.uid === uid
})
if (index > -1) {
topPriorityQueue.splice(index, 1)
return true
}
return false
}
addTask(
componentId: string,
refNames: string[],
refs: Element[],
afterRender?: (info: ExecutedItem) => void,
isTopPriority = false,
): string {
const uid = this.buildUniqueId(componentId, refNames)
this.removeTask(uid)
const { normalQueue, topPriorityQueue } = this
const queueItem: WaitingItem = {
uid,
componentId,
refNames,
refs,
afterRender,
isTopPriority,
}
if (isTopPriority) {
topPriorityQueue.unshift(queueItem)
} else {
normalQueue.push(queueItem)
}
this.emit(TypesetQueueEvent.addTask, queueItem)
this.runTask()
return uid
}
cancelTask(uid: string): void {
const isRemoved = this.removeTask(uid)
if (isRemoved) {
this.emit(TypesetQueueEvent.cancelTask)
}
}
private runTask(): void {
const { isRunning, mathJax } = this
if (isRunning || !mathJax) {
return
}
this.isRunning = true
const { topPriorityQueue, normalQueue } = this
let item: WaitingItem | undefined
if (topPriorityQueue.length) {
item = topPriorityQueue.shift()
} else if (normalQueue.length) {
item = normalQueue.shift()
}
if (!item) {
this.isRunning = false
const { executed } = this
const { length } = executed
if (length) {
const total = executed.reduce((count, executedItem) => {
return (count += executedItem.endTime - executedItem.startTime)
}, 0)
const average = total / length
const firstRun = executed[0]
const lastRun = executed[length - 1]
const duration = lastRun.endTime - firstRun.startTime
// tslint:disable-next-line
console.log(
`finished ... time( duration / total / average / times): ${duration} /${total} / ${average} / ${length}`,
)
}
return
}
const { refs, afterRender, uid, refNames, componentId, isTopPriority } = item
const startTime = Date.now()
const queueArgs: any = []
refs.forEach((ref) => {
queueArgs.push(['Typeset', MathJax.Hub, ref])
})
queueArgs.push(() => {
this.isRunning = false
const info: ExecutedItem = {
uid,
refNames,
componentId,
isTopPriority,
startTime,
endTime: Date.now(),
}
this.executed.push(info)
if (afterRender) {
afterRender(info)
}
this.emit(TypesetQueueEvent.finishTask)
this.runTask()
})
MathJax.Hub.Queue.apply(MathJax.Hub, queueArgs)
}
clearTasks(): void {
this.normalQueue = []
this.topPriorityQueue = []
this.executed = []
this.emit(TypesetQueueEvent.clearTasks)
}
reset(): void {
this.normalQueue = []
this.topPriorityQueue = []
this.executed = []
this.mathJax = null
this.removeAllListeners()
}
getProgress(): { total: number; finished: number } {
const { normalQueue, topPriorityQueue, executed } = this
const total = normalQueue.length + topPriorityQueue.length + executed.length
const finished = executed.length
return {
total,
finished,
}
}
}
export { WaitingItem, ExecutedItem, TypesetQueue, TypesetQueueEvent }
複製代碼
實現說明
import config from '@/common/config'
import { loadMathJax } from '@/common/mathjax/mathJaxLoader2'
import { TypesetQueue } from '@/common/mathjax/TypesetQueue'
import shortid from '@/common/utils/shortid'
import Vue from 'vue'
import Component /*, { mixins } */ from 'vue-class-component'
const globalTypesetQueue = new TypesetQueue()
@Component({})
class MathJaxMixin extends Vue /*mixins(ComponentNameMixin) */ {
/**************************************************************************
* data
**************************************************************************/
private componentId!: string
private typesetUidList!: string[]
private mathJaxRenderTime: number = 0
/**************************************************************************
* computed
**************************************************************************/
get isMathJaxRendered(): boolean {
const { mathJaxRenderTime } = this
return mathJaxRenderTime > 0
}
/**************************************************************************
* methods
**************************************************************************/
private async loadMathJax(): Promise<void> {
const result = await loadMathJax()
const { mathJax } = result
globalTypesetQueue.setupMathJax(mathJax)
this.onLoadMathJax()
}
private pushRefIntoTypesetQueue(refNames: string[], afterRender?: () => void, isTopPriority = false): void {
if (!refNames || !refNames.length) {
throw new Error('refNames can not be nil')
}
const { $refs, componentId } = this
if (!componentId) {
throw new Error(`Component mixin MathJaxMixin has no componentId`)
}
const refElements: Array<{ name: string; el: Element }> = []
refNames.forEach((refName) => {
const ref = $refs[refName]
if (ref) {
refElements.push({
name: refName,
el: ref as Element,
})
}
})
if (refElements && refElements.length) {
const names = refElements.map((item) => item.name)
const elements = refElements.map((item) => item.el)
const uid = globalTypesetQueue.addTask(componentId, names, elements, afterRender, isTopPriority)
const { typesetUidList } = this
if (!typesetUidList.includes(uid)) {
typesetUidList.push(uid)
}
} else {
if (config.isDev) {
const msg = `[refNames] is not valid`
// tslint:disable-next-line
console.warn(`Failed push ref into MathJax Queue: ${msg}`, refNames)
}
}
}
onLoadMathJax(): void {
// onLoadMathJax() method can be overrided
}
renderMathJaxAtNextTick(refNames: string[] | string, afterRender?: () => void, isTopPriority = false): void {
this.cancelMathJaxRender()
this.$nextTick(() => {
const names: string[] = typeof refNames === 'string' ? [refNames] : refNames
this.pushRefIntoTypesetQueue(
names,
() => {
this.mathJaxRenderTime = Date.now()
if (afterRender) {
afterRender()
}
},
isTopPriority,
)
})
}
cancelMathJaxRender(): void {
const { typesetUidList } = this
typesetUidList.forEach((uid) => {
globalTypesetQueue.cancelTask(uid)
})
this.typesetUidList = []
}
/**************************************************************************
* life cycle
**************************************************************************/
created(): void {
this.loadMathJax()
this.typesetUidList = []
this.componentId = shortid.generate()
}
beforeDestroy(): void {
this.cancelMathJaxRender()
}
}
export { MathJaxMixin, globalTypesetQueue }
複製代碼
實現說明
import { globalTypesetQueue } from '@/common/components/MathJaxMixin'
import { TypesetQueueEvent } from '@/common/mathjax/TypesetQueue'
import Vue from 'vue'
import Component /*, { mixins } */ from 'vue-class-component'
@Component({})
class MathJaxProgressMixin extends Vue /*mixins(ComponentNameMixin) */ {
/**************************************************************************
* data
**************************************************************************/
mathJaxTotal: number = 0
mathJaxFinished: number = 0
/**************************************************************************
* computed
**************************************************************************/
get isMathJaxRendered(): boolean {
const { mathJaxTotal, mathJaxFinished } = this
const value = mathJaxTotal <= mathJaxFinished
return value
}
/**************************************************************************
* methods
**************************************************************************/
private handleMathJaxProgress(): void {
window.setTimeout(() => {
const result = globalTypesetQueue.getProgress()
const { total, finished } = result
this.mathJaxTotal = total
this.mathJaxFinished = finished
}, 0)
}
private addMathJaxListener(): void {
this.removeMathJaxListener()
globalTypesetQueue.on(TypesetQueueEvent.addTask, this.handleMathJaxProgress)
globalTypesetQueue.on(TypesetQueueEvent.cancelTask, this.handleMathJaxProgress)
globalTypesetQueue.on(TypesetQueueEvent.finishTask, this.handleMathJaxProgress)
globalTypesetQueue.on(TypesetQueueEvent.clearTasks, this.handleMathJaxProgress)
}
private removeMathJaxListener(): void {
globalTypesetQueue.off(TypesetQueueEvent.addTask, this.handleMathJaxProgress)
globalTypesetQueue.off(TypesetQueueEvent.cancelTask, this.handleMathJaxProgress)
globalTypesetQueue.off(TypesetQueueEvent.finishTask, this.handleMathJaxProgress)
globalTypesetQueue.off(TypesetQueueEvent.clearTasks, this.handleMathJaxProgress)
}
progressAsText(): string {
const { mathJaxTotal, mathJaxFinished } = this
return `${mathJaxFinished} / ${mathJaxTotal}`
}
/**************************************************************************
* life cycle
**************************************************************************/
created(): void {
this.addMathJaxListener()
}
beforeDestroy(): void {
this.removeMathJaxListener()
}
}
export default MathJaxProgressMixin
複製代碼
方案 2 實現了
基本上能夠知足了產品需求
待 MathJax 渲染(Typeset)數學公式後,用戶使用瀏覽器的打印功能打印網頁。
因爲整個 App 只有一個TypesetQueue 實例(globalTypesetQueue),該方案只能知足當前 app 界面中只有一個 MathJax 渲染管理需求的狀況。
The MathJax Startup Sequence — MathJax 1.1 documentation
以上的思路及實現,是在開發過程的邏輯。
今天整理成文,發現以上的思路及實現存在一個邏輯上的漏洞:
帶着這樣的疑問, 翻看了 MathJax 的文檔及源碼,發現:
p.s.我的水平有限,以上內容僅供參考,歡迎交流。
網易技術熱愛者隊伍持續招募隊友中!網易有道,與你同道,由於熱愛因此選擇, 期待志同道合的你加入咱們,簡歷可發送至郵箱:bjfanyudan@corp.netease.com