yield pattern
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.
댓글남기기