Posts 什么是Mutator
Post
Cancel

什么是Mutator

Blockly 里面的 Blocks 有一个很重要的概念 Mutator,在改它实现的时候慢慢悟出 Mutator 概念的精妙之处。

Mutator 是什么?

Wikipedia 上的解释:

1
In computer science, a mutator method is a method used to control changes to a variable. They are also widely known as setter methods. Often a setter is accompanied by a getter (also known as an accessor), which returns the value of the private member variable.

 大概的理解是把某种改变的控制交给一个变量,想一想这不就是跟 Vue 或者 React 的思想很像嘛,一个 State 就是一个状态的快照,随时可以根据 State 还原当时的 View。但这只是页面方面,其实 Mutator 还有很多有意思的用法。

Wikipedia 上还有一个关于 JS 构造函数的例子:

常规构造函数

1
2
3
4
5
6
7
8
9
10
11
function Student(name) {
  var _name = name;

  this.getName = function () {
    return _name;
  };

  this.setName = function (value) {
    _name = value;
  };
}

非常规构造函数:

1
2
3
4
5
6
7
8
9
10
11
function Student(name) {
  var _name = name;

  this.__defineGetter__("name", function () {
    return _name;
  });

  this.__defineSetter__("name", function (value) {
    _name = value;
  });
}

使用 prototype 继承

1
2
3
4
5
6
7
8
9
10
11
12
function Student(name) {
  this._name = name;
}

Student.prototype = {
  get name() {
    return this._name;
  },
  set name(value) {
    this._name = value;
  },
};

不使用 prototype 的写法

1
2
3
4
5
6
7
8
var Student = {
  get name() {
    return this._name;
  },
  set name(value) {
    this._name = value;
  },
};

使用 defineProperty

1
2
3
4
5
6
7
8
9
10
11
function Student(name) {
  this._name = name;
}
Object.defineProperty(Student.prototype, "name", {
  get() {
    return this._name;
  },
  set(value) {
    this._name = value;
  },
});

Mutator 在 Blockly 中的应用

将积木状态存储在 xml 中

假如有这样一块积木:

01.png

它是一个可以动态变换的积木,里面的状态还包括是否拼接了子积木,如果条件是否有条件判断等,更重要的是拼接过程还要【支持撤销】,这时候如何记录状态就尤为重要了。

在 Blockly 中是使用 xml 来进行状态记录的,可以根据一个某种格式的 xml,进行积木状态的还原,上图所示的积木对应的 xml 是这种格式。

1
2
3
4
5
6
7
8
9
10
<xml>
  <block type="controls_if" id="nnQ^(`Hcw.5eDg4[j=}y" inline="true" x="77" y="-266">
    <mutation else="1"></mutation>
    <value name="IF0">
      <empty type="logic_empty" id="z=n#r418P4JeMeo!P9?i" editable="false">
        <field name="BOOL" value=""></field>
      </empty>
    </value>
  </block>
</xml>

看到 mutation 标签没有,竟然是用这种那么简单的 attribute 方式来进行状态记录。以此类推,点击加号之后样式变化了,相应转换回来的 xml 也变化,一切有迹可循。

02.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<xml>
  <block type="controls_if" id="nnQ^(`Hcw.5eDg4[j=}y" inline="true" x="77" y="-266">
    <mutation elseif="1" else="1"></mutation>
    <value name="IF0">
      <empty type="logic_empty" id="z=n#r418P4JeMeo!P9?i" editable="false">
        <field name="BOOL" value=""></field>
      </empty>
    </value>
    <value name="IF1">
      <empty type="logic_empty" id="(,/${}`GUwkF{F@c[9g*" editable="false">
        <field name="BOOL" value=""></field>
      </empty>
    </value>
  </block>
</xml>

mutation 与积木的转换逻辑

 为积木注册一个 mutation 对象:

1
2
3
4
5
6
7
8
9
const CONTROLS_IF_MUTATOR = {
  elseCount_: 1,
  elseifCount_: 0,
  domToMutation() {},
  mutationToDom() {},
  addMutation() {},
  removeMutation() {},
  updateShape_() {},
};

Mutator 的逻辑十分清晰,大多数情况下需要的 Mutator 属性可能只需要一个,不过这里两个可以更好地将逻辑进行区分。

domToMutation 就是 Dom 操作,获取 mutation 元素上的内容,然后执行 updateShape_

1
2
3
4
5
6
7
8
domToMutation(
  this:InstanceType<CodemaoBlockly['BlockSvg']>,
  xmlElement:Element,
) {
  this.elseifCount_ = parseInt(<string>xmlElement.getAttribute('elseif'), 10) || 0;
  this.elseCount_ = parseInt(<string>xmlElement.getAttribute('else'), 10) || 0;
  this.updateShape_();
},

mutationToDom 就是仅仅做一件事,生成一个 mutation 标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
mutationToDom() {
  if (!this.elseifCount_ && !this.elseCount_) {
    return null;
  }
  const container = document.createElement('mutation');
  if (this.elseifCount_) {
    container.setAttribute('elseif', String(this.elseifCount_));
  }
  if (this.elseCount_) {
    container.setAttribute('else', String(1));
  }
  return container;
},

addMutation 和 removeMutation 更简单了,就是改变 elseCount_ 或者 elseifCount_ 的值后,执行 updateShape_。注意,这里只改 state,不要进行任何逻辑操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
addMutation(
  this:InstanceType<CodemaoBlockly['BlockSvg']>,
) {
  this.elseifCount_++;
  // ...
  this.updateShape_();

}
removeMutation(
  this:InstanceType<CodemaoBlockly['BlockSvg']>,
  index:string,
) {
  this.elseifCount_--;
  // ...
  this.updateShape_();
},

最后,只要 updateShape_ 根据 state 的状态来进行渲染操作,就可以完美实现 state 控制 view 的 Mutator 了,这样的好处是可以知道之前的状态,而且 undoredo 的内存成本非常低,只记录一个 mutation 数值即可,非常好的实现了多重撤销操作。

mutation 思想应该还可以用在很多不止是 state 和 view 层面的内容上,等待后续继续挖掘。

This post is licensed under CC BY 4.0 by the author.
Trending Tags
Contents

Trending Tags