Angular+Rxjs drag and drop 高效能實踐嘗試
最近在做一個相當於網站編輯器的專案。遇到一個相當蛋疼的問題。在對於元素的操作。即拖拽換位的事件方面。效能問題及其嚴重,首次實現拖拽換位的功能,使用了第三方
ofollow,noindex">bevacqua/dragula叫dragula的一個第三方庫。但是很遺憾。在整合進專案中。由於同一頁面事件過多以及ngZone的干預下(我一個頁面也許有幾百個ckeditor),導致頁面及其卡頓。故此方案拋棄。
接下來採用第二種方案,即rxjs的流控制
原始碼如下: let dom: HTMLElement = this.moveElement.nativeElement let dragStart = fromEvent(this.triggerElement.nativeElement, 'mousedown') let dragMoving = fromEvent(document, 'mousemove') let dragEnd = fromEvent(document, 'mouseup') dragStart.pipe(map((event: any) => { let DomRect: any = dom.getBoundingClientRect() this.downeventOffset = {x: event.clientX - DomRect.x, y: event.clientY - DomRect.y} return dragMoving.pipe( takeUntil(dragEnd)) } ), concatAll(), map((event: any) => ({ x: event.clientX, y: event.clientY })))
此處省略掉部分變數解釋。該程式碼思路來源於臺灣一個 it幫的網站。安排了滑鼠點選->滑鼠移動->滑鼠抬起的控制流。 雖然該方式大幅度的提高了之前dragula帶來的效能問題。但是經過測試 還是會在初始化的時候bind event。即在滑鼠未點選的時候 事件就綁定了。帶來了額外的效能開銷。所以此種方案也不是最佳方案
雖然當時覺得效能還行。但是始終不是完全之策,萬幸的是material2 cdk 出來了一款高效能元件 drag and drop 。
閱讀原始碼之後修改了我的drag and drop
首先先放上一張思路圖

光看圖可能比較抽象。現在我們看下程式碼。
依然是afterViewInit 中初始化事件
ngAfterViewInit() { //等待ngzone stable this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => { const passiveEventListenerOptions = supportsPassiveEventListeners() ? {passive: true} as EventListenerOptions : false; const rootElement = this._triggerElement; rootElement.addEventListener('mousedown', this._pointerDown, passiveEventListenerOptions); rootElement.addEventListener('touchstart', this._pointerDown, passiveEventListenerOptions); }); }
此處的triggerElement 即為我們拖拽時觸發的物件。可以是cursor 也可以是dom本身。我再此設定了@Input

可以看到給dom設定了pointerdown事件 接下來看看pointerdown做了什麼
_pointerDown = (event: MouseEvent | TouchEvent) => { if (this.isDragging || (!this._isTouchEvent(event) && event.button !== 0)) { return; } this.isStartedDragging = false; this._pointerMoveSubscription = this.drag_event_register.pointerMove.subscribe(this._pointerMove); this._pointerUpSubscription = this.drag_event_register.pointerUp.subscribe(this._pointerUp); this._pickupPositionOnPage = this._getPointerPositionOnPage(event); this.drag_event_register.startDragging(this.triggerElement, event); };
此處會發現我們的 drag_ event __register 中的兩個事件被訂閱了。回到上圖

就是代表此處的圓圈了,
擷取該service中的部分程式碼

此處可以看到 依然是脫離 ngZone 建立的event listeners ,由此可見。如果你在遇到事件性不佳的時候,可以考慮從ngZone入手提升效能。當然由於detectChange 對於某些angular內部的event emmiter 還是需要回到ngZone 內部的。此處不做贅述
由這兩段程式碼可見。整個事件流形成了一個閉環。只為dom元素綁定了一個click事件。(而不是額外的mouse move 等等)用過訂閱去監聽+建立事件。極大的提高了效能(反正使用此種拖拽方式目前我並沒有卡頓。
(注意,該處程式碼並非material原始碼。本人直接拷貝過來有過改動。因為有些其他特殊事件需要處理)
另外,目前material2 的dnd 還有一些feature 尚待完善。希望準備去用的朋友可以考慮略微等待一下。
小結:高效能的元件最佳實現方式還是需要多多考量專案本身的需求。Angular本身是一個複雜的框架。因此多關注material官方庫的一些實現對自己的專案是有挺大的幫助的。