浅出 javascript generator 函数

直击主题

js 复制代码
function* say() {
  console.log('start');
  
  let r1 = yield 'hello';
  console.log(r1);
  
  let r2 = yield 'world';
  console.log(r2);
  
  return true;
}

let iSay = say();
// 输出
// 空,无输出

console.log(iSay.next(1));
// 输出
// start
// { value: 'hello', done: false }

console.log(iSay.next(2));
// 输出
// 2
// { value: 'world', done: false }

console.log(iSay.next(3));
// 输出
// 3
// { value: true, done: true }

console.log(iSay.next()); // { value: true, done: true }

say 就是一个 generator function(生成器函数),调用它就会返回一个 generator(生成器)。生成器有一个 next 方法,每次调用会返回一个对象。该对象有两个属性,value 和 done,value 为生成器函数中 yield 的返回值,done 表示生成器函数是否执行完成。

生成器函数执行后会返回一个生成器对象,但是此时生成器函数中的代码不会执行,直到生成器对象调用 next()。

生成器对象调用 next 方法会执行生成器函数中代码到下一个(或第一个) yield 处, 此时生成器函数的执行暂停,直到生成器对象调用 next。

调用 next 可以传参,本次 next 调用会从暂停处 yield 处开始运行,暂停处的 yield 的返回值即是 next 的传参。

由于第一次暂停是调用生成器函数时发生的,所以第一个 next(1) 不会从某个 yield 暂停处开始继续执行,所以第一个 next 传参无 yield 进行返回。可以理解为第一次 next 调用时没有暂停的 yield,所以没有 yield 来承接第一个 next 传参。

那这样的函数有什么用呢?它能做什么? 简单分析,可以看出它有一个非常有趣的特征,就是生成器函数不会一调用就从头执行到尾,而是受调用方驱动。调用方执行一下 next,函数往下走一步(走到下个 yield)。需要不停的调用 next 直到整个函数执行完成。

了解到了这个运行特征,它就会让代码变得很有趣了。比如解决个异步回调潜逃问题: 假设有个代码是这么写的:

js 复制代码
function getData(onSucceed){
    http.request('https://xx.xx.com/data',function(result){
        onSucceed(result);
    });
}

function getUser(onSucceed){
     http.request('https://xx.xx.com/user',function(result){
            onSucceed(result);
      });
}

function showUserData(){
    getData(function(data){
        getUser(function(user){
            var userData = data.filter(d=>d.user_name===use.name);
            console.log('展示用户数据',userData);
        })
    })
}

众所周知嵌套层级多了,将导致代码非常难维护。那通过生成器函数如何解决这个问题呢?也许你已经想到了:

js 复制代码
// 获取到生成器对象
var gShowUserData = showUserData();
// 启动生成器函数,运行到 let data = yield getData() 的 yield 处, getData() 会执行
gShowUserData.next();

function getData(){
    http.request('https://xx.xx.com/data',function(result){
       // next 会启动生成器函数运行到下一个 yield, 也就是 let user = yield getUser(), getUser () 会执行并暂停在此处。 getUser() 会执行。
       // 本次 next 传参数会被 let data = yield 的 yield 返回
       gShowUserData.next(result);
    });
}

function getUser(){
     http.request('https://xx.xx.com/user',function(result){
        // next 会启动生成器函数运行到最后(后面没有 yield 了,所以不会再暂停了)
        // next 传参数会被 let user = yield 处的 yield 返回
        gShowUserData.next(result);
      });
}

function* showUserData(){
   let data = yield getData();
   let user = yield getUser();
   
   var userData = data.filter(d=>d.user_name===user.name);
   console.log('展示用户数据',userData);
}

简单解释下代码的核心思想,就是将回调驱动后续流程的代码变为了,异步回调驱动生成器函数继续往下执行,异步回调执行之前生成器函数是暂停的,不会继续执行,这样生成器函数就可以等到数据都获取完成了再执行最后的数据处理的代码。

那么你也许会说,现在 promise 通过链式调用也能解决嵌套的问题,async、await 也类同步代码能解决嵌套的问题。没错,那有没有发现刚写的 showUserData 和 async 和 await 很像,如果你把 * 想象为 async、 把 yield 想象为 await 的话。你可以理解为 async、await 就是生成器函数的语法糖。

生成器函数 yield 的另一个特性:yield*。 yield* 会自动将 function* 进行解构,举个简单例子:

js 复制代码
function* say1(){
    yiled 1;
    yiled 2;
}

function* say2(){
    yield 4;
    yield 5;
    yield 6;
}

funciton* say(){
    yield* say1();
    yield 3;
    yield* say2();
}

这个例子里 say 函数是等价于:

js 复制代码
funciton* say(){
    yiled 1;
    yiled 2;
    yield 3;
    yield 4;
    yield 5;
    yield 6;
}

因为 yield* 会自动完成对生成器的解构。

有了以上知识,我们来尝试用生成器函数来翻译一下一个 async、await 的函数实现

js 复制代码
function async getData(){
   var data = await Promise.resolve([{name:1},{name:2}]);
   return data;
}

function async getUser(){
    var data = await Promise.resolve({name:1});
    return data;
}

function async showUserData(){
    var data = await getData();
    var user = await getUser();
    
    var userData = data.filter(d=>d.user_name===user.name);
    console.log('展示用户数据',userData);
}

按之前的设想我们可以直接把代码翻译为这样:

js 复制代码
function* getData(){
   var data = yield Promise.resolve([{name:1},{name:2}]);
   return data;
}

function* getUser(){
    var data = yield Promise.resolve({name:1});
    return data;
}

function* showUserData(){
    var data = yield* getData();
    var user = yield* getUser();
    
    var userData = data.filter(d=>d.user_name===user.name);
    console.log('展示用户数据',userData);
}

根据 yield* 的逻辑我们再翻译为这样:

js 复制代码
function* showUserData(){
    var data_getData = yield Promise.resolve([{name:1},{name:2}]);
    var data = data_getData;
    
    var data_getUser = yield Promise.resolve({name:1});
    var user = data_getUser;
    
    var userData = data.filter(d=>d.user_name===user.name);
    console.log('展示用户数据',userData);
    return '执行完成';
}

现在我们只要能让最后这个函数能跑起来就行了,但是目前来看显然是不行的,因为 yield 返回的是 promise,我们需要将生成器自动执行完。执行逻辑就是获取 yield 返回值为 promise 的对象的决议值之后再通过 next 传入决议值启动生成器继续往下执行,直到生成器函数执行完成。

js 复制代码
function runGenerator(generatorObj) {
    return new Promise((resolve, reject) => {
    
        function next(data) {
            try {
                // 通过生成器对象执行到生成器函数的最终
                var { value, done } = generatorObj.next(data);
            }catch(e){
                return reject(e);
            }
            
            if (!done) { 
                // done 为 true,表示迭代完成
                // value 不一定是 Promise,可能是一个普通值。使用 Promise.resolve 进行包装。
                Promise.resolve(value).then(val => {
                    next(val);
                }, reject);
            } else {
                // 最终值
                resolve(value);
            }
        }
        
        next(); // 启动生成器函数的执行 
    });
}

var gObj = showUserData();

runGenerator(gObj).then(data => {
    console.log(data);// 执行完成
}).catch((err) => {
    console.log('err: ', err);
});

就这样我们完成了一次对 async、await 函数的翻译。

总结

本文本主要介绍生成器函数的一些特点,以及与 async、await 的关系。当然,关于生成器的知识还有很多,就不一一展开了,比如与 Iterator 的关系(for of 依赖)也是比较有意思的。

相关推荐
莫尔道嘎老范42 分钟前
vue+vite前端项目ci过程中遇到的问题
前端·vue.js·ci/cd
谈谈叭1 小时前
Vue3中实现插槽使用
前端·vue.js·前端框架·npm
new出一个对象3 小时前
react+hook+vite项目使用eletron打包成桌面应用+可以热更新
前端·react.js·前端框架
GoFly开发者3 小时前
GoFly框架使用vue flow流程图组件说明
前端·vue.js·流程图·vue flow流程图
幸运小圣4 小时前
Vue3 -- 环境变量的配置【项目集成3】
前端·vue.js
如鹿觅水4 小时前
通过JS删除当前域名中的全部COOKIE教程
服务器·前端·javascript
#sakura4 小时前
web-02
前端
Lipn4 小时前
前端怎么获取视口大小
开发语言·前端·javascript
晓风伴月4 小时前
腾讯IM web版本实现迅飞语音听写(流式版)
前端·语音识别·讯飞语音听写
开心工作室_kaic5 小时前
ssm126基于HTML5的出租车管理系统+jsp(论文+源码)_kaic
java·前端·html5