JavaScript:以函数式方式重新组织画布渲染

4
有没有更好或更“实用”的方法来组织这段代码?
let activeCanvas = null;

export function createLayer() {
    if (activeCanvas)
        activeCanvas.disableInteraction();

    let newCanvas = createCanvas({ transparent: !!activeCanvas});

    let parentCanvas = activeCanvas;
    activeCanvas = newCanvas;

    return {
        stage: newCanvas.stage,
        destroy() {
            newCanvas.destroy();
            parentCanvas.enableInteraction();
            activeCanvas = parentCanvas;
        }
    }
}

这将创建一个新的图层并将舞台返回给调用者,同时也可以销毁该图层,然后使父级图层成为活动图层。

图层可以相互叠加。

...那么这个"activeCanvas"变量是模块作用域的,有没有一种更"函数式"的方法将它缠绕在调用栈的某个地方?

更新

代码正常工作 - 这只是关于风格、优雅和如何以函数式编程的方式编写代码的问题。

我对代码很满意 - 只是觉得有一种更"开明"的方式来做到这一点,但我想不出来。


每次调用 destroy 方法时,您是否应该更新父层和活动层,还是仅更新活动层并保持相同的父层? - nem035
你是如何使用 createLayer 的?因为现在你有一个状态,即 activeLayer,它在所有对 createLayerdestroy 方法的调用中都是共享的,这意味着通过调用 createLayer 创建的所有 API 都共享该状态。实际上,它是一个单例。换句话说,任何图层都可以通过调用其 destroy 方法来更改 activeLayer 变量。这是有意的吗?activeLayer 应该是每个创建的图层的状态还是所有图层的状态? - nem035
它应该是一个“singleton” - 跟踪当前活动层,以便可以再次销毁它。在创建时,当前活动层成为父级。在销毁时,旧的父级变为活动状态。这样,我可以很好地将层堆叠在彼此上,并在销毁后再次“展开”。我只是想知道是否有一种更优雅的方法通过调用栈本身来完成这个任务。 - Wolfgang
每次销毁时,您不应该同时更新parentCanvas吗?否则,在第一次调用destroy之后,这将是trueparentCanvas === activeCanvas,下一次您将在父画布和活动画布上都执行操作。 - nem035
如果活动层被销毁,父层将成为新的活动层。 - Wolfgang
你是如何创建这些层的?是通过用户事件(例如点击、计时器)还是一些编程条件来实现的? - nem035
2个回答

2

注意:在下面的示例中,“透明”画布是蓝色的,普通画布是绿色的。禁用交互画布的不透明度低于启用的画布。


本质上,您在这里拥有的是一种带有一些状态封装的堆栈形式。

您需要关注的是:

  1. 将项目推到顶部(添加新层)
  2. 从顶部弹出项目(销毁图层)
  3. 跟踪当前阶段

function createLayers(canvases = []) {
  function top() {
    return canvases[canvases.length - 1];
  }

  function disableTop() {
    const last = top();
    if (last) last.disableInteraction();
  }

  function enableTop() {
    const last = top();
    if (last) last.enableInteraction();
  }

  function destroyTop() {
    const last = top();
    if (last) last.destroy();
  }

  function push() {
    disableTop();
    canvases.push(createCanvas({ transparent: !!top() }));
    enableTop();
  }

  function pop() {
    destroyTop();
    canvases.pop();
    enableTop();
  }

  return {
    push,
    pop,
    stage: () => top().stage
  };
}

const layers = createLayers();

document.querySelector("#push").addEventListener("click", layers.push);

document.querySelector("#pop").addEventListener("click", layers.pop);

var createCanvas=function(e=0){return function(a={}){const t=e;e+=1;const n=document.createElement("div");return n.className=`fake-canvas ${a.transparent?"transparent":""}`,document.body.appendChild(n),n.appendChild(document.createTextNode(t)),{__elem:n,enableInteraction(){n.classList.remove("disabled")},disableInteraction(){n.classList.add("disabled")},destroy(){n.remove()}}}}();
.fake-canvas{border:3px solid #018bbc;width:75px;height:50px;display:flex;align-items:center;justify-content:center;margin-left:10px;position:relative;}.fake-canvas.disabled{opacity:.5;pointer-events:none}body{height:100vh;display:flex;align-items:center}.fake-canvas.transparent{border-color:#2ecc71;}button{margin:5px;padding:10px;appearance:none;background-color:#018bbc;border:0;color:#fff;text-transform:uppercase}#buttons{display:flex;flex-direction:column;}
<div id="buttons"><button id="push">push</button><button id="pop">pop</button></div>

另一种思考这个问题的方式是将其视为一个链表。

为了使事情更加函数化,您需要在列表构建和修改中引入纯度和不可变性。

列表永远不会被改变,而是任何修改都会创建一个新的列表。

为了处理画布操作,我们可以引入一个副作用函数。为了使事情完全函数化,我们实际上希望在每个操作中创建一个新图层和一个新画布,但这可能很昂贵,因此我的示例在现有画布上运行。

var createCanvas=function(e=0){return function(t){const a=e;e+=1;const n=document.createElement("div");return n.className=`fake-canvas ${t.transparent?"transparent":""}`,document.body.appendChild(n),n.appendChild(document.createTextNode(a)),{__elem:n,enableInteraction(){n.classList.remove("disabled")},disableInteraction(){n.classList.add("disabled")},destroy(){n.remove()}}}}();

// function to create a layer
function layer(prev) {
  return {
    canvas: createCanvas({ transparent: !!prev }),
    prev: typeof prev === 'function' ? prev : () => prev,
  };
}

// function to compose an array of functions
function compose(...fns) {
  return function composition(arg) {
    return fns.reduceRight((prevResult, fn) => fn(prevResult), arg);
  }
}

// generic canvas-side-effect-enducing function
function canvasSideEffect(name) {
  return function sideEffect(l) {
    if (l && l.canvas) {
      l.canvas[name]();
    }
    return l;
  }
}

// create functions to perform canvas side-effect operations
const enable = canvasSideEffect("enableInteraction");
const disable = canvasSideEffect("disableInteraction");
const destroy = canvasSideEffect("destroy");

const push = compose(
  enable, // then we enable the new layer's canvas
  layer,  // then we create a new layer
  disable // first we disable the top layer's canvas
)

const pop = compose(
  enable,                // then we enable the new top layer's canvas
  l => l ? l.prev() : l, // then we remove the layer
  destroy                // first we destroy the previous layer's canvas
)

let l;

document.querySelector("#push").addEventListener("click", () => {
  // each layer creation creates a new list
  l = push(l)
});

document.querySelector("#pop").addEventListener("click", () => {
  // each layer removal creates a new list
  l = pop(l);
});
.fake-canvas{border:3px solid #018bbc;width:75px;height:50px;display:flex;align-items:center;justify-content:center;margin-left:10px;position:relative;}.fake-canvas.disabled{opacity:.5;pointer-events:none}body{height:100vh;display:flex;align-items:center}.fake-canvas.transparent{border-color:#2ecc71;}button{margin:5px;padding:10px;appearance:none;background-color:#018bbc;border:0;color:#fff;text-transform:uppercase}#buttons{display:flex;flex-direction:column;}
<div id="buttons"><button id="push">push</button><button id="pop">pop</button></div>


2

是的,模块范围的可变变量是一种明显的代码气味。一些面向对象编程可以解决这个问题,但让我们进行函数式编程。可以很容易地使一个栈结构成为不可变的:

export function createLayer(parent) {
    if (parent)
        parent._canvas.disableInteraction();
    const canvas = createCanvas({ transparent: !!parent });
    return {
        _canvas: canvas,
        stage: canvas.stage,
        destroy() {
            canvas.destroy();
            if (parent) // I think you missed this
                parent._canvas.enableInteraction();
            return parent;
        },
        create() {
            return createLayer(this);
        }
    };
}

这还有点奇怪,因为您的第一层是透明的。另外,没有数据结构表示空栈。为了解决这些问题,我们可以使用

function createLayer(parent, parentCanvas, transparent) {
    parentCanvas.disableInteraction();
    const canvas = createCanvas({ transparent });
    return {
        stage: canvas.stage,
        destroy() {
            canvas.destroy();
            parentCanvas.enableInteraction();
            return parent;
        },
        create() {
            return createLayer(this, canvas, false)
        }
    };
}
export const noLayer = {
    canvas: null,
    destroy() {
        throw new Error("cannot pop the bottom of the layer stack");
    },
    create() {
        return createLayer(this, {
            disableInteraction() {},
            enableInteraction() {}
        }, true);
    }
};

功能导向的用户会这样使用你的库:

然后,功能导向的用户会编写类似以下代码:

import { noLayer } from '…';

const a = noLayer.create();
const b = a.create();
const c = b.destroy(); // == a
const d = c.create();
const e = d.create();
const f = e.destroy(); // == d
const g = f.destroy(); // == c == a
const h = g.create();
const i = h.destroy(); // == g == c == a
const j = i.destroy(); // == noLayer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接