前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >前端推荐!10分钟带你了解Konva运行原理

前端推荐!10分钟带你了解Konva运行原理

作者头像
腾讯云开发者
发布2021-10-13 16:10:14
4K0
发布2021-10-13 16:10:14
举报

导语 | Konva是一个很优秀的Canvas框架,API封装简洁易懂,基于TypeScript实现,有React和Vue版本。本文总结梳理了Konva的架构设计、原理及其缺点,希望可以为大家了解KonvaJS的相关问题提供一些参考。

一、前言

用过Canvas的都知道它的API比较多,使用起来也很麻烦,比如我想绘制一个圆形就要调一堆API,对开发算不上友好。

代码语言:javascript
复制
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版本。

代码语言:javascript
复制
      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主要包括这么四部分:

  1. Stage根节点:这是应用的根节点,会创建一个div节点,作为事件的接收层,根据事件触发时的坐标来分发出去。一个Stage节点可以包含多个Layer图层。
  2. Layer图层:Layer里面会创建一个Canvas节点,主要作用就是绘制Canvas里面的元素。一个Layer可以包含多个Group和Shape。
  3. Group组:Group包含多个Shape,如果对其进行变换和滤镜,里面所有的Shape都会生效。
  4. Shape:指Text、Rect、Circle等图形,这些是Konva封装好的类。

(二)build dom

Stage创建的时候会去创建两个Canvas节点以及content容器节点,这两个Canvas节点是用于perfectDrawEnabled的,后面会讲到。

这里需要注意的就是这个content节点,作为整个Konva画布的容器,之后的Layer都会被append进去。

代码语言:javascript
复制
  _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进去。

代码语言:javascript
复制
  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方法,我们来一探究竟。

代码语言:javascript
复制
  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进行批量重绘。

代码语言:javascript
复制
  _requestDraw() {    if (Konva.autoDrawEnabled) {      const drawNode = this.getLayer() || this.getStage();      drawNode?.batchDraw();    }  }

这个批量重绘的原理是利用requestAnimationFrame方法将要绘制的内容放到下一帧来绘制。这样同时修改多个图形多个属性就不需要反复绘制了。

代码语言:javascript
复制
  batchDraw() {    // _waitingForDraw 保证只会执行一次 requestAnimFrame    if (!this._waitingForDraw) {      this._waitingForDraw = true;      // 如果调用多次方法修改 Shape 属性,这里就会批量绘制      // 避免了多次绘制带来的开销      Util.requestAnimFrame(() => {        this.draw();        this._waitingForDraw = false;      });    }    return this;  }

(二)Shape绘制

所有涉及到图形绘制的地方都是调用Shape实现类上的_sceneFunc方法,以Circle为例:

代码语言:javascript
复制
  _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的离屏渲染。
代码语言:javascript
复制
  _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的一部分重合到一起,就不符合我们的预期了。

比如下面这段代码:

代码语言:javascript
复制
      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:

代码语言:javascript
复制
    // 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是事件名-事件处理方法的映射。

代码语言:javascript
复制
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呢?

代码语言:javascript
复制
canvas = new SceneCanvas();hitCanvas = new HitCanvas({  pixelRatio: 1,});
代码语言:javascript
复制
  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了,真是相当巧妙的实现。

代码语言:javascript
复制
  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上面。

代码语言:javascript
复制
    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方法来判断是否匹配上。

代码语言:javascript
复制
  _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里面可以看到后根据是什么类型的选择器来分别进行匹配。

代码语言:javascript
复制
      // 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。

代码语言:javascript
复制
  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到父节点里面

代码语言:javascript
复制
  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的增删查改。

这里从中抽取了部分源码:

代码语言:javascript
复制
// 创建一个实例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 实现 insertBeforeexport 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注册了一个插件,这个插件里面分别注册了每个组件。

代码语言:javascript
复制
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的方式几乎一样。

代码语言:javascript
复制
      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,实现上更加简洁一些。

代码语言:javascript
复制
    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全家桶原理。

 推荐阅读

Golang原生json可以一库走天下吗?

这次全了,8种超详细Web跨域解决方案!

10分钟带你玩转Kafka基于Controller的领导选举!

LLVM极简教程:9个步骤!实现一个简单编译器


本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-10-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯云开发者 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 用过Canvas的都知道它的API比较多,使用起来也很麻烦,比如我想绘制一个圆形就要调一堆API,对开发算不上友好。
  • (一)批量渲染
  • (二)Shape绘制
  • (三)离屏渲染
  • (四)perfectDrawEnabled
  • (一)事件分发
  • (二)匹配Shape
  • (三)拖拽事件
  • (一)react-reconciler
  • (二)react-konva
  • 脏矩形
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档