Is it ok to use a generator function to replace a

2019-08-01 17:20发布

问题:

For this code example, you have to imagine a series of animations on moving a robot (move left / right, go forward)

In reality it is a site with more complicated animations (loading ajax, loading images, multiple animations, etc ..) that I currently manage with promises, but as the site evolves, the code of this part becomes a dish of spagettti.

This is the first time I do something like this, and I wonder if this is really a good idea, because this way of doing things seems really strange for me.
I have the impression that I will eventually find myself with insoluble problems.
In any case my current site becomes a real nightmare, because I have to change some animations, add new ones ...

Does this sample code look correct?
Should I change something there?

const Root   = document.documentElement
  ,   gRoot  = getComputedStyle(Root)
  ,   moving = [ {T:-30,L:0},  {T:0,L:+30}, {T:+30,L:0}, {T:0,L:-30} ]
  ;
var RotateDeg = 0
  , RotateMov = 0
  , posT      = parseInt(gRoot.getPropertyValue('--PosT'))
  , posL      = parseInt(gRoot.getPropertyValue('--PosL'))
  ;
function F_1() // move forward
  {
  posT += moving[RotateMov].T
  posL += moving[RotateMov].L

  Root.style.setProperty('--PosT', posT + "px")
  Root.style.setProperty('--PosL', posL + "px")
  }
function T_L() // turn Left
  {
  RotateMov = (RotateMov +3) %4
  RotateDeg -=90
  Root.style.setProperty('--turn', RotateDeg + "deg")
  }
function T_R() // turn Right
  {
  RotateMov = (RotateMov +1) %4
  RotateDeg +=90
  Root.style.setProperty('--turn', RotateDeg + "deg")
  }
function R_0() // rotate to zero
  {
  RotateMov = 0
  RotateDeg = 0
  Root.style.setProperty('--turn', RotateDeg + "deg")
  }
function disableButtons(OnOff)
  {
  Bt_Tab_A.disabled = OnOff
  Bt_Tab_B.disabled = OnOff
  }
function* Sequence(Tab_fct)
  {
  for( let fct of Tab_fct) yield fct
  }

var iterator = Sequence([])

function nextSequence()
  {
  let command = iterator.next()
  if (!command.done) command.value()
  else disableButtons(false)
  }

Bt_Tab_A.onclick=_=>
  { 
  disableButtons(true)
  iterator = Sequence( [ F_1, T_L, F_1, T_R, F_1, T_R, F_1, F_1, T_R, F_1, F_1, T_R, F_1, R_0  ] )
  nextSequence()
  }
Bt_Tab_B.onclick=_=>
  { 
  disableButtons(true)
  iterator = Sequence( [ T_L, F_1, T_R, F_1, T_R, F_1, T_R, F_1, R_0 ] )
  nextSequence()
  }
robot.addEventListener('transitionend',  nextSequence )
:root {
  --turn  : 0deg;
  --PosT  : 110px;
  --PosL  : 90px;
}
#robot {
  font-size   : 16px;
  width       : 30px;
  height      : 30px;
  background-color: aqua;
  text-align  : center;
  line-height : 1.8em;
  transition  : all .5s linear;
  transform   : rotate( var(--turn) );
  position:fixed;
  top : var(--PosT);
  left: var(--PosL);
}
<div id="robot">R</div>

<button id="Bt_Tab_A"> Sequence A</button>
<button id="Bt_Tab_B"> Sequence B</button>

Tips and advice are welcome;)

回答1:

In your case, I feel like Promise is the way to go.

In my own rule of thumb (beware, it's opinionated):

  • Use promise when you need to execute async operations sequentially.
  • Use generator function when you need to generate result when you need it.

In your snippet, all you need to do is to have the animations run sequentially, not when you need it.

Besides, you will notice you have multiple functions that are dependant on the current state (result) of a single variable (i.e., iterator). What's bad about this is that when there's a bug in between your sequence, you will take more time and effort to debug the situation because one change in one function might affect the other functions. You also have a global transitionend event listener that is quite consufing at the first glance.

In short, using generator functions, the flow of sequential operations is hard to understand.

The approach below is using async/await. Only Sequence and nextSequence methods are modified (comment explanations inside). Every operation is contained within its own function scope. Reliant of global variables is reduced:

(Sorry I formatted the code to my code style when I wrote them)

const Root = document.documentElement;
const gRoot = window.getComputedStyle(Root);
const moving = [
  {
    T: -30,
    L: 0
  },
  {
    T: 0,
    L: +30
  },
  {
    T: +30,
    L: 0
  },
  {
    T: 0,
    L: -30
  }
];

let RotateDeg = 0;
let RotateMov = 0;
let posT = parseInt(gRoot.getPropertyValue('--PosT'));
let posL = parseInt(gRoot.getPropertyValue('--PosL'));

function F_1(){
  posT += moving[RotateMov].T;
  posL += moving[RotateMov].L;

  Root.style.setProperty('--PosT', posT + 'px');
  Root.style.setProperty('--PosL', posL + 'px');
}

function T_L(){
  RotateMov = (RotateMov + 3) % 4;
  RotateDeg -= 90;
  Root.style.setProperty('--turn', RotateDeg + 'deg');
}

function T_R(){
  RotateMov = (RotateMov + 1) % 4;
  RotateDeg += 90;
  Root.style.setProperty('--turn', RotateDeg + 'deg');
}

function R_0(){
  RotateMov = 0;
  RotateDeg = 0;
  Root.style.setProperty('--turn', RotateDeg + 'deg');
}

function disableButtons(OnOff){
  Bt_Tab_A.disabled = OnOff
  Bt_Tab_B.disabled = OnOff
}

async function Sequence(Tab_fct){
  // Disable buttons before start
  disableButtons(true);
  
  for (let fct of Tab_fct)
    // Run the animation one by one
    await nextSequence(fct);

  // Reenable buttons before end
  disableButtons(false);
}

function nextSequence(fct){
  return new Promise(res => {
    // Move event listener here so that they dont depend on a global one.
    // Use { once: true } to run this callback only once
    window.addEventListener('transitionend', res, { once: true });
    
    // Run the animation
    fct();
  })
}


Bt_Tab_A.onclick = () => {
  Sequence([F_1, T_L, F_1, T_R, F_1, T_R, F_1, F_1, T_R, F_1, F_1, T_R, F_1, R_0]);
}

Bt_Tab_B.onclick = () => {
  Sequence([ T_L, F_1, T_R, F_1, T_R, F_1, T_R, F_1, R_0 ]);
}
:root {
  --turn  : 0deg;
  --PosT  : 110px;
  --PosL  : 90px;
}
#robot {
  font-size   : 16px;
  width       : 30px;
  height      : 30px;
  background-color: aqua;
  text-align  : center;
  line-height : 1.8em;
  transition  : all .5s linear;
  transform   : rotate( var(--turn) );
  position:fixed;
  top : var(--PosT);
  left: var(--PosL);
}
<div id="robot">R</div>

<button id="Bt_Tab_A">Sequence A</button>
<button id="Bt_Tab_B">Sequence B</button>