yield pattern

3 분 소요

Understanding the JavaScript yield Keyword and Generators

Let’s delve into the yield keyword and its role in JavaScript generators, exploring its syntax, behavior, and potential applications.

Definition

The yield keyword is used to pause and resume generator functions (defined with function*). When a generator function encounters a yield expression during its execution, it returns the yielded value and suspends its execution, preserving its state.

Syntax and Behavior

To utilize yield, you must define a generator function by appending an asterisk (*) to the function keyword (e.g., function*). Typically, you instantiate the generator function, storing the result in a variable, and then invoke the next() method on this generator object to execute the function. The function proceeds until it encounters a yield statement. At this point, the function returns the yielded value. Subsequent calls to next() resume execution from the point where the function was paused, retaining the values of previously declared and processed variables.

function* add() {
  let a = 1;
  let b = 2;
  yield a + 2;
  yield b + 2;
}

const generator = add();
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());

// { value: 3, done: false }
// { value: 4, done: false }
// { value: undefined, done: true }

The result yielded by yield is returned as a JSON-like object containing two properties: value and done. The done property indicates whether the generator function has completed its execution, reaching the end of its code or encountering a return statement. A value of true signifies completion.

Comparison with Promises

JavaScript’s asynchronous nature prevents direct waiting for the results of other functions during execution. Promises and async/await were introduced to address this.

async function addAsync(a, b) {
  return a + b;
}

async function example() {
  let result = await addAsync(3, 5);
  console.log(result);
}

example();

function add(a,b) {
  return new Promise((resolve, reject) => {
    resolve(a+b);
  });
}

While Promises improve code organization, complex Promise chains can become difficult to read. Utilizing yield can offer a more streamlined approach in certain scenarios.

function* addGenerator(a, b) {
  yield a + b;
}

const generator = addGenerator(3, 5);
console.log(generator.next());
console.log(generator.next());

// { value: 8, done: false }
// { value: undefined, done: true }

One potential drawback is the separation of declaration and execution, which might make the code easier to read on a line-by-line basis but harder to understand the overall flow.

Addressing Callback Hell and Promise Hell

Callback hell is a notorious problem in asynchronous JavaScript.

function cadd2(val, callback) {
	return callback(val + 2);
}

console.log(cadd2(1, function (val) {
	return val + 4;
}));

Promises offer an alternative, but nesting Promises within Promises (to use the result of the first Promise in the second) leads to “Promise Hell”.

function add2(val) {
	return new Promise((resolve, reject) => {
		resolve(val + 2);
	});
}

function add4(val) {
	return new Promise((resolve, reject) => {
		resolve(val + 4);
	});
}

add2(1).then(result => {
	add4(result).then(final => {
		console.log(final);
	});
});

yield can be employed to achieve a cleaner structure.

function* add2Generator(val) {
	yield val + 2;
}
function* add4Generator(val) {
	yield val + 4;
}

const generator = add2Generator(1);
const result = generator.next();
const generator2 = add4Generator(result.value);
console.log(generator2.next());

yield and return

What happens if a return statement is encountered within a generator function before it has fully executed? Upon encountering a return statement, the done property of the generator object becomes true, and subsequent calls to next() are disallowed, effectively terminating the generator function.

const generator = add(3,5);
console.log(generator.next());
console.log(generator.next());
console.log(generator.next());

function* add(a,b) {
  yield a+b+0;
  return a+b+1;
  yield a+b+2;
}

// { value: 8, done: false }
// { value: 9, done: true }
// { value: undefined, done: true }

yield*

To delegate to another generator function from within a generator, use the yield* syntax; otherwise, an error will occur.

function* add(val) {
	yield* add4(val + 2);
}
function* add4(val) {
	yield val + 4;
}

const generator = add(1);
console.log(generator.next());
console.log(generator.next());

// { value: 7, done: false }
// { value: undefined, done: true }

Even though the example involves delegating yield -> yield, only one call to next() is required on the main generator. The yield* syntax automates the process of iterating through the delegated generator until a final yield value is met.

Conclusion

yield offers an alternative synchronous-like execution model in asynchronous JavaScript, separate from Promises. The ability to pause functions mid-execution provides greater flexibility than Promises. However, overuse can lead to code that is superficially easy to read but difficult to understand. Judicious application is recommended.

References

댓글남기기