事件代理功能點分享
- 2020 年 12 月 18 日
- 筆記
事件代理功能點分享
看過前面幾篇文章的讀者相信也應該有所了解了,我們藉助於團隊內部開發的編輯器實現了很多成功的項目案例,已經多次看到我們點擊一個文本、一個按鈕等等…去彈出一個表格之類的交互,有些同學難免有些好奇我們是如何實現在 canvas 圖形對象上實現事件派發和監聽的,接下來聽我娓娓道來。
準備工作
其實在實現事件代理對象之前,我實現過一個文本對象,就是在 canvas 上繪製出一個固定默認寬高的矩形,用戶雙擊時可輸入文字,它的實現可謂是非常簡單。但是其中也有不容忽視的小細節需要注意。
文本對象的實現
雙擊文本對象時,實現一個臨時的 DOM 節點
// 創建input
createTextInput() {
this.setVisible(false)
const textInput = document.createElement("input")
this.c("textInput", textInput)
// 設置基本樣式
...
//input剛創建出來並不會自動聚焦
//這裡需要調用一次自身focus 讓input聚焦
textInput.focus()
const remove = () => {
// 刪除Input
this.removeTextInput()
textInput.removeEventListener("blur", remove, false)
}
textInput.addEventListener("blur", remove, false)
}
// 更新input的位置
updateTextInput() {
this.setVisible(false)
const textInput = this.getClient("textInput")
const style = textInput.style
const zoom = this._network.getZoom()
const rect = this._network.getViewRect()
const { x, y } = this.getLocation()
style.display = "block"
style.left = x * zoom + -rect.x + "px"
style.top = y * zoom + -rect.y + "px"
}
記錄並存儲用戶鍵入的內容和操作
這一步由於我們採用的是 input 所以我們只需要在 input 失去焦點的時候,獲取到 input.value 的內容就行,同時也一定要注意要刪除掉 input 節點,否則頁面上的文本一旦多了起來,會很大程度影響性能.
- 監聽用戶離開,不再操作臨時 DOM 節點,將鍵入的內容快取取出設置到
文本對象上 - 在 UI 繪製中控制 canvas 繪製用戶輸入的內容
- 擴展部分:
暴露出部分可控的介面和方法在外部,用戶可在屬性面板配置操作輕鬆實現文字陰影、跑馬燈、漸變色…等各種功能
事件代理對象
由此想到了我們是不是也可以實現一個類似的事件代理對象,這樣一來我們就能輕鬆操作我們框架上 canvas 畫布里的各種對象了。接下來就讓我們一起去瞧瞧團隊里是如何實現的事件代理對象。
用什麼實現代理
使用虛擬的一個 HTMLNODE 節點代理目標對象事件
// BaseHTMLNode是自己封裝的一個原生DOM節點對象
class EventProxyNode extends BaseHTMLNode {
constructor(network) {
// 關聯的目標對象
this._attacher = null;
}
...
}
設計的時候要考慮到使用時傳入一個目標對象關聯綁定
關聯目標對象
在這裡我們通過綁定事件和派發事件去使得目標對象和代理對象關聯
在編輯器中已經預留了派發事件和監聽事件的操作面板。
在工具中步驟截圖如下:
- 打開事件代理開關
- 在目標對象上派發一個事件 配置派發的事件
- 然後在監聽對象綁定監聽事件並且寫上處理邏輯,例如跳轉切換等等…
由於我們在畫布交互對象中註冊了交互事件監聽,每當我們的事件代理對象觸發事件時就派發
一個畫布的事件出去
- 預覽效果
(不知道是哪位小夥伴寫的注釋,給他點贊 👍)
關於事件代理是如何執行代理的程式碼如下所示,每次代理對象觸發了事件後就通過我們的canvas畫布對象把事件派發出去,傳遞的參數當中是代理對象關聯的目標對象,這樣就相當於是我們的目標對象觸發了這個事件,然後只需要在監聽對象上綁定監聽寫上處理邏輯即可。
/**
* 事件代理網元觸發事件的時候把事件派發到宿主網元上
* @param {*} network
*/
function addEventProxyInteraction(network) {
network.addInteractionListener((e) => {
if (e.element instanceof EventProxyNode) {
network.fireInteractionEvent({
// 事件名稱
kind: e.kind,
// 事件源
element: e.element._attacher,
// 事件對象
event: e.event,
});
}
});
}
管理事件代理
在這裡我們的管理事件代理對象的就是目標對象,當我們初始化渲染頁面生成圖元時,
就會生成對應的事件代理對象,並且在屬性面板預留了一個開關用來控制是否啟用事件代理對象
// 創建事件代理對象
createEventProxyNode() {
if (this._eventProxyNode) {
return
}
this._eventProxyNode = new EventProxyNode(this._network)
const parent = this.getParent()
this._eventProxyNode.setParent(parent)
const zoom = this._network.getZoom()
this._eventProxyNode.scaleWithZoom(zoom)
const { x, y, width, height } = this.getRect()
this._eventProxyNode.setLocation(x, y)
this._eventProxyNode.setSize(width, height)
this._eventProxyNode.append()
this._eventProxyNode._attacher = this
}
// 初始化事件代理對象
initProxyNodes() {
const box = this._box
box.forEach(node => {
if (node.getEventProxy && node.getEventProxy()) {
node.createEventProxyNode()
}
})
}
...
結尾總結
這樣一來我們就能點擊一個明明不是 DOM 節點,也不是我們框架當中的一個內置對象,卻依然能夠觸發一系列的事件,並且還能在配置當中靈活配置相應的事件,方便了我們去實現各種交互。
有時當我們實現一個很小的功能點的時候,可以根據這個功能點放大,宏觀的去思考一下有沒有可能實現一個通用的、可復用的功能點,功能雖小,實現雖簡單,但是卻能方便我們做很多事,高樓大廈也是從一磚一瓦來的。
如果對可視化感興趣,可以和我交流,微信541002349. 另外關注公眾號「ITMan彪叔」 可以及時收到更多有價值的文章。