本文做者:傅云贵(网易有道技术团队)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