乐趣区

关于javascript: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 次要包含这么几局部:

  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 进去。

  _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,别离是:

  1. cachedSceneCanvas:用于绘制图形的 canvas 的离屏渲染。
  2. cachedFilterCanvas:用于解决滤镜成果。
  3. 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 性能,它会这样做:

  1. 在 bufferCanvas 上绘制 Shape
  2. 绘制 fill 和 stroke
  3. 在 layer 上利用透明度
  4. 将 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 呢?

因而,Konva 在创立 Layer 的时候会创立两个 Canvas,一个用于 sceneCanvas 用于绘制 Shape,另一个 hitCanvas 在内存外面,用于判断是否被打击。

canvas = new SceneCanvas();
hitCanvas = new HitCanvas({pixelRatio: 1,});

当 Shape 初始化的时候,会生成一个随机的色彩,以这个色彩作为 key 存入到 shapes 数组外面。

  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 了,真是相当奇妙的实现。

但这种形式也有缺点,因为生成的随机 hex 色彩是有下限的,最多会会有 256 256 256 = 16777216 种,如果超过了这么多就会导致匹配不精确。

不过考虑一下如果有 16777216 个 DOM 节点,浏览器就会卡爆了,换成这么多 Canvas 图形一样会导致性能爆炸。

自定义 hitFunc

如果你想自定义事件响应区域,Konva 也提供了 hitFunc 办法给你实现。在绘制 hitCanvas 的时候,本来的绘制 sceneFunc 就生效了,取而代之的是绘制 hitFunc。

  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<ChildNode extends Node = Node>(
    selector: string | Function,
    findOne: boolean
  ) {var retArr: Array<ChildNode> = [];
    
    // 调用 _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;
  }

在 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,甚至是小程序都能够。业界曾经有计划是基于这个来实现了,能够参考蚂蚁金服的 remax:[Remax – 应用真正的 React 构建小程序
][11]

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 的突围盒实现的非常简单,不适宜做碰撞检测,它也没有提供脏矩形的能力。

退出移动版