前端推薦!10 分鐘帶你瞭解 Konva 運行原理
Konva 是一個很優秀的 Canvas 框架,API 封裝簡潔易懂,基於 TypeScript 實現,有 React 和 Vue 版本。本文總結梳理了 Konva 的架構設計、原理及其缺點,希望可以爲大家瞭解 KonvaJS 的相關問題提供一些參考。
一、前言
用過 Canvas 的都知道它的 API 比較多,使用起來也很麻煩,比如我想繪製一個圓形就要調一堆 API,對開發算不上友好。
const canvas = document.querySelector('canvas');
const context = canvas.getContext('2d');
// 設置字體樣式
context.font = '24px SimSun, Songti SC';
context.fillText('24px的宋體呈現', 20, 50);
// 繪製完整圓
context.fillStyle = 'RGB(255, 0, 0)';
context.beginPath();
context.arc(150, 75, 50, 0, Math.PI * 2);
context.stroke();
爲了解決這個痛點,誕生了例如 PIXI、ZRender、Fabric 等 Canvas 庫。今天要講的 Konva 也是一個很優秀的 Canvas 框架,API 封裝簡潔易懂,基於 TypeScript 實現,有 React 和 Vue 版本。
const stage = new Konva.Stage({
container: 'root',
width: 1000,
height: 1000,
});
const layer = new Konva.Layer();
const group = new Konva.Group();
const text = new Konva.Text({
text: 'Hello, this is some good text',
fontSize: 30,
});
const circle = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 70,
fill: 'red',
stroke: 'black',
strokeWidth: 4
});
group.add(text);
group.add(circle);
layer.add(group);
stage.add(layer);
二、架構設計
(一)Konva Tree
從前言裏面給的那段代碼可以看出來,Konva 有一定的嵌套結構,有些類似 DOM 結構。通過 add 和 remove 就能實現子節點的添加和刪除。
Konva Tree 主要包括這麼四部分:
-
Stage 根節點:這是應用的根節點,會創建一個 div 節點,作爲事件的接收層,根據事件觸發時的座標來分發出去。一個 Stage 節點可以包含多個 Layer 圖層。
-
Layer 圖層:Layer 裏面會創建一個 Canvas 節點,主要作用就是繪製 Canvas 裏面的元素。一個 Layer 可以包含多個 Group 和 Shape。
-
Group 組:Group 包含多個 Shape,如果對其進行變換和濾鏡,裏面所有的 Shape 都會生效。
-
Shape:指 Text、Rect、Circle 等圖形,這些是 Konva 封裝好的類。
(二)build dom
Stage 創建的時候會去創建兩個 Canvas 節點以及 content 容器節點,這兩個 Canvas 節點是用於 perfectDrawEnabled 的,後面會講到。
這裏需要注意的就是這個 content 節點,作爲整個 Konva 畫布的容器,之後的 Layer 都會被 append 進去。
_buildDOM() {
this.bufferCanvas = new SceneCanvas({
width: this.width(),
height: this.height(),
});
this.bufferHitCanvas = new HitCanvas({
pixelRatio: 1,
width: this.width(),
height: this.height(),
});
if (!Konva.isBrowser) {
return;
}
var container = this.container();
if (!container) {
throw 'Stage has no container. A container is required.';
}
// clear content inside container
container.innerHTML = '';
// content
this.content = document.createElement('div');
this.content.style.position = 'relative';
this.content.style.userSelect = 'none';
this.content.className = 'konvajs-content';
this.content.setAttribute('role', 'presentation');
container.appendChild(this.content);
this._resizeDOM();
}
在調用 Stage.add 的時候,不僅會調用 Layer 的繪製方法,還會把 Layer 的 Canvas 節點 append 進去。
add(layer: Layer, ...rest) {
if (arguments.length > 1) {
for (var i = 0; i < arguments.length; i++) {
this.add(arguments[i]);
}
return this;
}
super.add(layer);
var length = this.children.length;
if (length > MAX_LAYERS_NUMBER) {
Util.warn(
'The stage has ' +
length +
' layers. Recommended maximum number of layers is 3-5. Adding more layers into the stage may drop the performance. Rethink your tree structure, you can use Konva.Group.'
);
}
layer.setSize({ width: this.width(), height: this.height() });
// draw layer and append canvas to container
layer.draw();
if (Konva.isBrowser) {
this.content.appendChild(layer.canvas._canvas);
}
// chainable
return this;
}
三、渲染
(一)批量渲染
從前面的代碼中可以看到,沒有手動調用繪製方法,但依然會進行繪製,說明會在一定的時機進行渲染。這個時機就在 add 方法裏面,不管 Group、Layer、Stage 哪個先 add,最終都會觸發渲染。
他們三個都繼承了 Container 類,在 Container 類裏面有一個 add 方法,我們來一探究竟。
add(...children: ChildType[]) {
if (arguments.length > 1) {
for (var i = 0; i < arguments.length; i++) {
this.add(arguments[i]);
}
return this;
}
var child = children[0];
// 如果要添加的子節點已經有個父節點,那就先將其從父節點移除,再插入到當前節點裏面
if (child.getParent()) {
child.moveTo(this);
return this;
}
this._validateAdd(child);
// 設置子節點的 index 和 parent
child.index = this.getChildren().length;
child.parent = this;
child._clearCaches();
this.getChildren().push(child);
this._fire('add', {
child: child,
});
// 請求繪製
this._requestDraw();
return this;
}
除了一些常規的處理之外,渲染的關鍵就在_requestDraw 方法裏面。這裏調用了 Layer 上面的 batchDraw 進行批量重繪。
_requestDraw() {
if (Konva.autoDrawEnabled) {
const drawNode = this.getLayer() || this.getStage();
drawNode?.batchDraw();
}
}
這個批量重繪的原理是利用 requestAnimationFrame 方法將要繪製的內容放到下一幀來繪製。這樣同時修改多個圖形多個屬性就不需要反覆繪製了。
batchDraw() {
// _waitingForDraw 保證只會執行一次 requestAnimFrame
if (!this._waitingForDraw) {
this._waitingForDraw = true;
// 如果調用多次方法修改 Shape 屬性,這裏就會批量繪製
// 避免了多次繪製帶來的開銷
Util.requestAnimFrame(() => {
this.draw();
this._waitingForDraw = false;
});
}
return this;
}
(二)Shape 繪製
所有涉及到圖形繪製的地方都是調用 Shape 實現類上的_sceneFunc 方法,以 Circle 爲例:
_sceneFunc(context) {
context.beginPath();
context.arc(0, 0, this.attrs.radius || 0, 0, Math.PI * 2, false);
context.closePath();
context.fillStrokeShape(this);
}
在 Shape 和 Node 兩個基類上面只負責調用,具體的實現放到具體的 Shape 實現上面。這樣帶來兩個好處,一個是可以實現自定義圖形,另一個是以後要是支持 SVG、WebGL 會很方便。
(三)離屏渲染
什麼是離屏渲染?就是在屏幕之外預渲染一個 Canvas,之後通過 drawImage 的形式將其繪製到屏幕要顯示的 Canvas 上面,對形狀相似或者重複的對象繪製性能提升非常高。
假設我們有個列表頁,每次滾動的時候全部重新繪製開銷會比較大。但如果我們實現一個 Canvas 池,把已經繪製過的列表項存起來。下次滾動到這裏的時候,就可以直接從 Canvas 池裏面取出來 drawImage 到頁面上了。
在 Node 類上面有個 cache 方法,這個方法可以實現細粒度的離屏渲染。cache 方法內部會創建三個 Canvas,分別是:
-
cachedSceneCanvas:用於繪製圖形的 Canvas 的離屏渲染。
-
cachedFilterCanvas:用於處理濾鏡效果。
-
cachedHitCanvas:用於處理 hitCanvas 的離屏渲染。
_drawCachedSceneCanvas(context: Context) {
context.save();
context._applyOpacity(this);
context._applyGlobalCompositeOperation(this);
// 獲取離屏的 Canvas
const canvasCache = this._getCanvasCache();
context.translate(canvasCache.x, canvasCache.y);
var cacheCanvas = this._getCachedSceneCanvas();
var ratio = cacheCanvas.pixelRatio;
// 將離屏 Canvas 繪製到要展示的 Canvas 上面
context.drawImage(
cacheCanvas._canvas,
0,
0,
cacheCanvas.width / ratio,
cacheCanvas.height / ratio
);
context.restore();
}
(四)perfectDrawEnabled
Canvas 在繪製 stroke 和 fill 的時候,如果遇到透明度的時候,stroke 會和 fill 的一部分重合到一起,就不符合我們的預期了。
比如下面這段代碼:
const canvas = document.getElementById("canvas");
const bufferCanvas = document.createElement("canvas");
const bufferCtx = bufferCanvas.getContext("2d");
const ctx = canvas.getContext("2d");
ctx.strokeStyle="green";
ctx.lineWidth=10;
ctx.strokeRect(30,30,50,50);
ctx.globalAlpha = 0.5;
ctx.fillStyle="RGB(255, 0, 0)";
ctx.fillRect(30,30,50,50);
它的實際展示效果是這樣的,中間的 stroke 和 fill 有一部分重疊。
在這種情況下,KonvaJS 實現了一個 perfectDrawEnabled 功能,它會這樣做:
-
在 bufferCanvas 上繪製 Shape
-
繪製 fill 和 stroke
-
在 layer 上應用透明度
-
將 bufferCanvas 繪製到 sceneCanvas 上面
可以看到開啓 perfectDrawEnabled 和關閉 perfectDrawEnabled 的區別很明顯:
它會在 Stage 裏面創建一個 bufferCanvas 和 bufferHitCanvas,前者就是針對 sceneCanvas 的,後者是針對 hitCanvas 的。
在 Shape 的 drawScene 方法裏面,會判斷是否使用 bufferCanvas:
// if buffer canvas is needed
if (this._useBufferCanvas() && !skipBuffer) {
stage = this.getStage();
bufferCanvas = stage.bufferCanvas;
bufferContext = bufferCanvas.getContext();
bufferContext.clear();
bufferContext.save();
bufferContext._applyLineJoin(this);
// layer might be undefined if we are using cache before adding to layer
var o = this.getAbsoluteTransform(top).getMatrix();
bufferContext.transform(o[0], o[1], o[2], o[3], o[4], o[5]);
// 在 bufferCanvas 繪製 fill 和 stroke
drawFunc.call(this, bufferContext, this);
bufferContext.restore();
var ratio = bufferCanvas.pixelRatio;
if (hasShadow) {
context._applyShadow(this);
}
// 在 sceneCanvas 應用透明度
context._applyOpacity(this);
context._applyGlobalCompositeOperation(this);
// 將 bufferCanvas 繪製到 sceneCanvas
context.drawImage(
bufferCanvas._canvas,
0,
0,
bufferCanvas.width / ratio,
bufferCanvas.height / ratio
);
}
四、事件
Konva 裏面的事件是在 Canvas 外層創建了一個 div 節點,在這個節點上面接收了 DOM 事件,再根據座標點來判斷當前點擊的是哪個 Shape,然後進行事件分發。
所以關鍵就在如何判斷當前點擊的 Shape 是哪個?相比 ZRender 裏面比較複雜的計算,Konva 使用了一個相當巧妙的方式。
(一)事件分發
Konva 目前支持下面這麼多事件,EVENTS 是事件名 - 事件處理方法的映射。
EVENTS = [
[MOUSEENTER, '_pointerenter'],
[MOUSEDOWN, '_pointerdown'],
[MOUSEMOVE, '_pointermove'],
[MOUSEUP, '_pointerup'],
[MOUSELEAVE, '_pointerleave'],
[TOUCHSTART, '_pointerdown'],
[TOUCHMOVE, '_pointermove'],
[TOUCHEND, '_pointerup'],
[TOUCHCANCEL, '_pointercancel'],
[MOUSEOVER, '_pointerover'],
[WHEEL, '_wheel'],
[CONTEXTMENU, '_contextmenu'],
[POINTERDOWN, '_pointerdown'],
[POINTERMOVE, '_pointermove'],
[POINTERUP, '_pointerup'],
[POINTERCANCEL, '_pointercancel'],
[LOSTPOINTERCAPTURE, '_lostpointercapture'],
];
// 綁定事件
_bindContentEvents() {
if (!Konva.isBrowser) {
return;
}
EVENTS.forEach(([event, methodName]) => {
// 事件綁定在 content 這個 dom 節點上面
this.content.addEventListener(event, (evt) => {
this[methodName](evt);
});
});
}
我們以 mousedown 這個具體的事件作爲例子來分析,它的處理方法在_pointerdown 裏面。_pointerdown 先執行 setPointersPositions,計算當前鼠標點擊的座標,減去 content 相對頁面的座標,得到了當前點擊相對於 content 的座標。同時將其存入了_changedPointerPositions 裏面。
然後遍歷_changedPointerPositions,通過 getIntersection 獲取到了點擊的 Shape 圖形。這個 getIntersection 遍歷調用了每個 Layer 的 getIntersection 方法,通過 Layer 獲取到了對應的 Shape。
因爲可以存在多個 Layer,每個 Layer 也可以在同一個位置繪製多個 Shape,所以理論上可以獲取到多個 Shape,Konva 這裏只取了第一個 Shape,按照 Layer->Shape 的順序來的。
然後 Stage 會調用 Shape 上面的_fireAndBubble 方法,這個方法調用_fire 發送 Konva 自己的事件,此時通過 on 綁定的事件回調就會觸發,有點兒像 jQuery 那樣。
然後 Konva 會繼續往上找到父節點,繼續調用父節點的_fireAndBubble 方法,直到再也找不到父節點爲止,這樣就實現了事件冒泡。
對於不想被點擊到的 Shape 來說,可以設置 isListening 屬性爲 false,這樣事件就不會觸發了。
(二)匹配 Shape
那麼 Layer 是怎麼根據點擊座標獲取到對應的 Shape 呢?如果是規則的圖形(矩形、圓形)還比較容易計算,要是下面這種不規則圖形呢?
衆所周知,在 Canvas 裏面有個 getImageData 方法,它會根據傳入的座標來返回一個 ImageData 信息,裏面有當前座標對應的色值。那麼我們能不能根據這個色值來獲取到對應的 Shape 呢?
canvas = new SceneCanvas();
hitCanvas = new HitCanvas({
pixelRatio: 1,
});
constructor(config?: Config) {
super(config);
// set colorKey
let key: string;
while (true) {
// 生成隨機色值
key = Util.getRandomColor();
if (key && !(key in shapes)) {
break;
}
}
this.colorKey = key;
// 存入 shapes 數組
shapes[key] = this;
}
每次在 sceneCanvas 上面繪製的時候,同樣會在內存中的 hitCanvas 裏面繪製一遍,並且將上面隨機生成的色值作爲 fill 和 stroke 的顏色填充。
當點擊 sceneCanvas 的時候,獲取到點擊的座標點,通過調用 hitCanvas 的 getImageData 就可以獲取到 colorKey,然後再通過 colorKey 就能找到對應的 Shape 了,真是相當巧妙的實現。
drawHit(can?: HitCanvas, top?: Node, skipDragCheck = false) {
if (!this.shouldDrawHit(top, skipDragCheck)) {
return this;
}
var layer = this.getLayer(),
canvas = can || layer.hitCanvas,
context = canvas && canvas.getContext(),
// 如果有 hitFunc,就不使用 sceneFunc
drawFunc = this.hitFunc() || this.sceneFunc(),
cachedCanvas = this._getCanvasCache(),
cachedHitCanvas = cachedCanvas && cachedCanvas.hit;
if (!this.colorKey) {
Util.warn(
'Looks like your canvas has a destroyed shape in it. Do not reuse shape after you destroyed it. If you want to reuse shape you should call remove() instead of destroy()'
);
}
// ...
drawFunc.call(this, context, this);
// ...
}
(三)拖拽事件
Konva 的拖拽事件沒有使用原生的方法,而是基於 mousemove 和 touchmove 來計算移動的距離,進而手動設置 Shape 的位置,實現邏輯比較簡單,這裏不細說。
五、濾鏡
Konva 支持多種濾鏡,在使用濾鏡之前需要先將 Shape cache 起來,然後使用 filter() 方法添加濾鏡。在 cache 裏面除了創建用於離屏渲染的 Canvas,還會創建濾鏡 Canvas。濾鏡處理在_getCachedSceneCanvas 裏面。
首先將 sceneCanvas 通過 drawImage 繪製到 filterCanvas 上面,接着 filterCanvas 獲取所有的 ImageData,遍歷所有設置的濾鏡方法,將 ImageData 傳給濾鏡方法來處理。
處理完 ImageData 之後,再將其通過 putImageData 繪製到 filterCanvas 上面。
if (filters) {
if (!this._filterUpToDate) {
var ratio = sceneCanvas.pixelRatio;
filterCanvas.setSize(
sceneCanvas.width / sceneCanvas.pixelRatio,
sceneCanvas.height / sceneCanvas.pixelRatio
);
try {
len = filters.length;
filterContext.clear();
// copy cached canvas onto filter context
filterContext.drawImage(
sceneCanvas._canvas,
0,
0,
sceneCanvas.getWidth() / ratio,
sceneCanvas.getHeight() / ratio
);
imageData = filterContext.getImageData(
0,
0,
filterCanvas.getWidth(),
filterCanvas.getHeight()
);
// apply filters to filter context
for (n = 0; n < len; n++) {
filter = filters[n];
if (typeof filter !== 'function') {
Util.error(
'Filter should be type of function, but got ' +
typeof filter +
' instead. Please check correct filters'
);
continue;
}
filter.call(this, imageData);
filterContext.putImageData(imageData, 0, 0);
}
} catch (e) {
Util.error(
'Unable to apply filter. ' +
e.message +
' This post my help you https://konvajs.org/docs/posts/Tainted_Canvas.html.'
);
}
this._filterUpToDate = true;
}
return filterCanvas;
}
那濾鏡效果怎麼畫上去的呢?在 konva 裏面進行了特殊處理,如果存在 filterCanvas,那就不會使用 cacheCanvas 了,也就是我們原本用於緩存的離屏 Canvas 會被 filterCanvas 進行替代。
最終 filterCanvas 會通過 drawImage 的方式繪製到 sceneCanvas 上面。
六、選擇器
Konva 實現了選擇器,方便我們快速查找到某個 Shape。目前主要有三種選擇器,分別是 id 選擇器、name 選擇器、type 選擇器。
前兩者需要在實例化的時候傳入一個 id 或者 name 屬性,後者則是根據類名(Rect、Line 等)來查找的。
選擇器查找的時候需要調用 find 方法,這個 find 方法掛載在 Container 類上面。它調用了_descendants 進行子節點的遍歷,將遍歷的 node 節點調用 isMatch 方法來判斷是否匹配上。
_generalFind(
selector: string | Function,
findOne: boolean
) {
var retArr: Array = [];
// 調用 _descendants 獲取所有的子節點
this._descendants((node: ChildNode) => {
const valid = node._isMatch(selector);
if (valid) {
retArr.push(node);
}
// 如果是 findOne,後面的就不繼續執行了
if (valid && findOne) {
return true;
}
return false;
});
return retArr;
}
private _descendants(fn: (n: Node) => boolean) {
let shouldStop = false;
const children = this.getChildren();
for (const child of children) {
shouldStop = fn(child);
if (shouldStop) {
return true;
}
if (!child.hasChildren()) {
continue;
}
// 如果子節點也有子節點,那就遞歸遍歷
shouldStop = (child as any)._descendants(fn);
// 如果應該停止查找(一般是 findOne 的時候就不需要查找後面的了)
if (shouldStop) {
return true;
}
}
return false;
}
</childnode extends node = node>
在 isMatch 裏面可以看到後根據是什麼類型的選擇器來分別進行匹配。
// id selector
if (sel.charAt(0) === '#') {
if (this.id() === sel.slice(1)) {
return true;
}
} else if (sel.charAt(0) === '.') {
// name selector
if (this.hasName(sel.slice(1))) {
return true;
}
} else if (this.className === sel || this.nodeType === sel) {
return true;
}
七、序列化
Konva 還支持對 Stage 的序列化和反序列化,簡單來說就是把 Stage 的數據導出成一份 JSON 數據以及把 JSON 數據導入,方便我們在 NodeJS 端進行服務端渲染。
序列化主要在 toObject 方法裏面,它會對函數和 DOM 節點進行過濾,只保留一份描述信息,比如 Layer 的信息、Shape 的信息等等,有點兒類似 React 裏面的 Virtual DOM。
toObject() {
var obj = {} as any,
attrs = this.getAttrs(),
key,
val,
getter,
defaultValue,
nonPlainObject;
obj.attrs = {};
for (key in attrs) {
val = attrs[key];
nonPlainObject =
Util.isObject(val) && !Util._isPlainObject(val) && !Util._isArray(val);
if (nonPlainObject) {
continue;
}
getter = typeof this[key] === 'function' && this[key];
delete attrs[key];
// 特殊處理函數,將其執行後把結果掛載到當前key上面
defaultValue = getter ? getter.call(this) : null;
// restore attr value
attrs[key] = val;
if (defaultValue !== val) {
obj.attrs[key] = val;
}
}
obj.className = this.getClassName();
return Util._prepareToStringify(obj);
}
而反序列化則是對傳入的 JSON 信息進行解析,根據 className 來創建不同的對象,對深層結構進行遞歸,然後 add 到父節點裏面。
static _createNode(obj, container?) {
var className = Node.prototype.getClassName.call(obj),
children = obj.children,
no,
len,
n;
// if container was passed in, add it to attrs
if (container) {
obj.attrs.container = container;
}
if (!Konva[className]) {
Util.warn(
'Can not find a node with class name "' +
className +
'". Fallback to "Shape".'
);
className = 'Shape';
}
// 根據傳入的 className 來實例化
const Class = Konva[className];
no = new Class(obj.attrs);
if (children) {
len = children.length;
for (n = 0; n < len; n++) {
// 如果還有子節點,那就遞歸創建
no.add(Node._createNode(children[n]));
}
}
return no;
}
八、React
Konva 和 React 綁定沒有使用重新封裝一遍組件的方式,而是採用了和 react-dom、react-native 一樣的形式,基於 react-reconciler 來實現一套 hostConfig,從而定製自己的 Host Component(宿主組件)。
(一)react-reconciler
React Fiber 架構誕生之後,他們就將原來的 React 核心代碼做了抽離。主要包括 react、react-reconciler 和 platform 實現(react-dom、react-native 等)三部分。
在 react-reconciler 裏面實現了大名鼎鼎的 Diff 算法、時間切片、調度等等,它還暴露給了我們一個 hostConfig 文件,允許我們在各種鉤子函數中實現自己的渲染。
在 React 裏面,有兩種組件類型,一種是 Host Component(宿主組件),另一種是 Composition Component(複合組件)。
在 DOM 裏面,前者就是 h1、div、span 等元素,在 react-native 裏面,前者就是 View、Text、ScrollView 等元素。後者則是我們基於 Host Component 自定義的組件,比如 App、Header 等等。
在 react-reconciler 裏面,它允許我們去自定義 Host Component 的渲染(增刪查改),這也意味着跨平臺的能力。我們只需要編寫一份 hostConfig 文件,就能夠實現自己的渲染。
參考上面的架構圖,會發現不管是渲染到 native、Canvas,甚至是小程序都可以。業界已經有方案是基於這個來實現了。
(二)react-konva
react-konva 的主要實現就在 ReactKonvaHostConfig.js 裏面,它利用 Konva 原本的 API 實現了對 Virtual DOM 的映射,響應了 Virtual DOM 的增刪查改。
這裏從中抽取了部分源碼:
// 創建一個實例
export function createInstance(type, props, internalInstanceHandle) {
let NodeClass = Konva[type];
const propsWithoutEvents = {};
const propsWithOnlyEvents = {};
for (var key in props) {
var isEvent = key.slice(0, 2) === 'on';
if (isEvent) {
propsWithOnlyEvents[key] = props[key];
} else {
propsWithoutEvents[key] = props[key];
}
}
// 根據傳入的 type 來創建一個實例,相當於 new Layer、new Rect 等
const instance = new NodeClass(propsWithoutEvents);
// 將傳入的 props 設置到實例上面
// 如果是普通的 prop,就直接通過 instance.setAttr 更新
// 如果是 onClick 之類的事件,就通過 instance.on 來綁定
applyNodeProps(instance, propsWithOnlyEvents);
return instance;
}
// 插入子節點,直接調用 konva 的 add 方法
export function appendChild(parentInstance, child) {
if (child.parent === parentInstance) {
child.moveToTop();
} else {
parentInstance.add(child);
}
updatePicture(parentInstance);
}
// 移除子節點,直接調用 destroy 方法
export function removeChild(parentInstance, child) {
child.destroy();
child.off(EVENTS_NAMESPACE);
updatePicture(parentInstance);
}
// 通過設置 zIndex 實現 insertBefore
export function insertBefore(parentInstance, child, beforeChild) {
// child._remove() will not stop dragging
// but child.remove() will stop it, but we don't need it
// removing will reset zIndexes
child._remove();
parentInstance.add(child);
child.setZIndex(beforeChild.getZIndex());
updatePicture(parentInstance);
}
九、Vue-Konva
在 Vue 上面,Konva 通過 Vue.use 註冊了一個插件,這個插件裏面分別註冊了每個組件。
const components = [
{
name: 'Stage',
component: Stage
},
...KONVA_NODES.map(name => ({
name,
component: KonvaNode(name)
}))
];
const VueKonva = {
install: (Vue, options) => {
let prefixToUse = componentPrefix;
if(options && options.prefix){
prefixToUse = options.prefix;
}
components.forEach(k => {
Vue.component(`${prefixToUse}${k.name}`, k.component);
})
}
};
export default VueKonva;
if (typeof window !== 'undefined' && window.Vue) {
window.Vue.use(VueKonva);
}
再來看看 KonvaNode 的實現,在 KonvaNode 裏面,對於節點的增刪查改都在 Vue 的生命週期裏面實現的。在 Vue 的 created 生命週期裏面調用 initKonva 去 new 一個 NodeClass,和上面 React 的方式幾乎一樣。
initKonva() {
const NodeClass = window.Konva[nameNode];
if (!NodeClass) {
console.error('vue-konva error: Can not find node ' + nameNode);
return;
}
this._konvaNode = new NodeClass();
this._konvaNode.VueComponent = this;
this.uploadKonva();
},
而在 Updated 的時候去進行 Props 的更新,在 destroyed 裏面對節點進行 destroy,實現上更加簡潔一些。
updated() {
this.uploadKonva();
checkOrder(this.$vnode, this._konvaNode);
},
destroyed() {
updatePicture(this._konvaNode);
this._konvaNode.destroy();
this._konvaNode.off(EVENTS_NAMESPACE);
},
十、缺陷
髒矩形
在性能方面,Konva 對比 PIXI、ZRender 這些庫還是不太夠看。如果我們 Layer 上有非常多的 Shape,如果想更新某個 Shape,按照 Konva 的實現方式依然會全量繪製。
雖然 Konva 支持單個 Shape 重繪,但實現上是無腦覆蓋原來的位置,這也意味着如果你的圖形在其他節點圖形下面,就會出現問題。
所以這裏缺少非常重要的局部更新能力,也就是我們常說的髒矩形。
髒矩形就是指當我們更新一個 Shape 的時候,利用碰撞檢測計算出和他相交的所有 Shape,將其進行合併,計算出一塊兒髒區域。然後我們通過 clip 限制 Canvas 只在這塊兒髒區進行繪製,這樣就實現了局部更新。
可惜 Konva 的包圍盒實現的非常簡單,不適合做碰撞檢測,它也沒有提供髒矩形的能力。
** 作者簡介**
尹光耀
騰訊文檔前端工程師
騰訊文檔前端工程師,騰訊校企合作講師,畢業於武漢大學,個人公衆號—前端小館,慕課網《Web 前端開發修煉指南》作者。目前負責騰訊文檔渲染層開發工作,有豐富的移動 Web 開發經驗,深入 React 全家桶原理。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/zUPy6lvcbUP2AjyDt3yNqg