lazyMan链式调用以及迭代器的认识

435 阅读6分钟

js链式调用

reference:zhuanlan.zhihu.com/p/136230955

实现一个懒汉,并且提供一系列的行为方法,调用 eat 就打印吃饭信息,调用 sleep 方法则进行延迟传入时间,再进行下一步的操作。

限制不使用promise

实现一个LazyMan,可以按照以下方式调用:
LazyMan(“Hank”)输出:
Hi! This is Hank!
LazyMan(“Hank”).sleep(10).eat(“dinner”)输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~
LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出
Hi This is Hank!
Eat dinner~
Eat supper~

LazyMan(“Hank”).sleepFirst(5).eat(“supper”)输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
以此类推。

分析

链式调用

从LazyMan("hank").sleep(5000).eat("supper")可以看出每一个方法解围都可以通过 . 来 注册或者执行下一步,所以想到返回this

sleep延迟

如何使用sleep方法延迟后续方法执行,可以使用settimeout,那么setTimeout如何能通知在一定时间内再把方法推进事件队列,但我们无法阻碍下一步方法的调用.能这么想,说明我们把链式调用想象为马上执行,就好像是下面代码

// 错误的代码
sleep (timeout) {
    setTimeout(() => {
        console.log(`${this.name} end sleep ~`)
    }, timeout * 1000)
    return this
}

这是错误的,我们无法等待一个异步执行完后才去执行下一个同步,执行sleep函数后会立刻返回this之后进行下一个函数.

那么如果我们维护一个队列,就像是promise那样,函数调用只是注册,之后到点了才执行.

维护队列和调度顺序

我们把链式调用定义为一个注册行为,事情就好办了,使用观察者模式(??,一个坑,是和发布订阅模式相似的那个么),首先lazyman在链式调用的每一个环节都会往队列中注册一个方法,在队列调度期间,程序从最前面的方法执行,队列中每一个方法运行结束都会调用下一个方法执行,事情变得井然有序(想到了promise并发执行但是有limit的情况),我们何时开始队列的第一个函数执行?

调度时机

开始触发队列中第一个方法的时机是什么?当然可以使用一个额外的start方法触发队列的调度,但是这题不许,那么怎么办呢,事实上,由于队列循环机制,可以使用setTimeout来解决,在构造函数开始一步调度.(这个选择非常妙,我也见过类似用法,在宏任务执行完成后,微任务才开始执行,这里就是等到注册全部完成,再开始任务,如果在原构造函数直接执行,就会导致执行第一个构造函数的时候,没有事件,所以执行了个空气,但是后面才注册,注册了又没法开始.

所以是先把代码都放到js引擎,执行,之后发现没有微任务,所以跑回来执行宏任务settimeout

class LazyMan {
    constructor (name) {
        // any code ...
        setTimeout(() => {
            this.next() // 负责调度下一个方法执行
        },0)
    }
}

(原文这里还把宏任务,微任务都解释一遍,也是不错)

// 懒汉 类
class LazyManClass {
    constructor (name) {
        this.name = name
        this.queue = [] // 队列
        console.log(`Hi! This is ${name}`)
​
        // 延迟调度
        setTimeout(() => {
            this.next()
        },0)
    }
    // 调度方法
    next () {
        const fnc = this.queue.shift()
        fnc && fnc()
    }
    /**
     * 注册函数方法
     * @param {*} fn 要注册的函数 
     * @param {*} isFirst 是否注册在队列最前
     */
    register (fn, isFirst) {
        if (isFirst) {
            this.queue.unshift(fn)
        } else {
            this.queue.push(fn)
        }
    }
    // 吃
    eat (food) {
        const _eat = () => {
            console.log(`Eat ${food}~`)
            this.next()
        }
        this.register(_eat)
        return this
    }
    // 睡在最前面
    sleepFirst (s) {
        return this.sleep(s, true)
    }
    // 睡觉
    sleep (s, isFirst=false) {
        const timeout = s * 1000
        const _sleep = () => {
            console.log(`Wake up after ${s}`)
            setTimeout(() => {
                this.next()
            }, timeout)
        }
        this.register(_sleep, isFirst)
        return this
    }
}

// 懒汉 返回一个懒汉实例
function LazyMan (name) {
    return new LazyManClass(name)
}

interpret

  1. 首先是链式调用,提议在方法结尾return this,
  2. 使用队列,通过register注册方法进行函数注册,在链式调用过程中按照顺序推到队列中
  3. 利用事件循环,在构造函数中通过setTimeout将调度队列的时机放到之后的事件循环中进行.

后记 2022.3.19

在看Symbol.iterator时,看到了一个异步的迭代器Symbol.asyncIterator,又想到了这个题目,感觉可以通过这个实现一下

首先介绍一下symbol.iterator

symbol.iterator

在调用for (let i of iterable)时,首先是iterable调用 iterable[Symbol.iterator]() 函数,返回一个对象,这个对象里有一个方法next(),之后通过不断调用next()函数,就可以获得一个返回值,返回值一定要包含两个属性,value,done. done用true或者false表示遍历是否结束.具体图例如下

for(let i of iterable ) {  -> iterable[Symbol.iterator]()=>获得一个对象
                              iterator={next:function(){}}
    console.log(i)         -> let iter=iterator.next(); //{value:"value",done:false}
                           -> if(!iter.done){ 
                                i=iter.value;
                                //let 形成多个块级作用域	       
                                iter=iterator.next();
                              } =>进行下一个值
                           -> else {break;} 退出循环
}

symbol.asyncIterator

原理和上面的类似只不过遍历对象要用promise来包装,并且返回的值放在resolve函数中传递给then最后赋给遍历获得的参数.遍历时使用

for await(let i of asyncIterable){}

并且注意如果写在函数中,函数前面要加async(await标配)

一个简单的例子如下

<script>    
const asyncIterable = {

    [Symbol.asyncIterator]() {
      let i=0;
      return {
        next() {
          if (i < 3) {
            return new Promise((resolve)=>{
              setTimeout(()=>{
                resolve({value:i,done:false});
                i++;
              },2000);
            })
          }

          return Promise.resolve({ done: true });
        }
      };
    }
  };
(async function() {
  for await (let num of asyncIterable) {
    console.log(num);
  }
})();
</script>

结果间隔2s输出0,1,2; i在这里形成了闭包使用

LazyMan

想了一下,其实使用异步迭代器只不过是改变了上面this.next(),改成了asyncIterator,大体思路还是一致.记录要使用的函数,返回this,使用setTimeout触发.改成自动next()罢了

首先建立一个类,类中有一个queue,而这个queue有一个异步迭代器属性由我们自己来写,大体框架如下


  class LazyMan {
    constructor(name) {
      console.log("this is ", name);
      this.queue = {
        que: [],
        [Symbol.asyncIterator]: () => {
          let i = 0;
          let self = this;
          return {
            //这里不用箭头函数咋搞this啊
            next: () => {
              return 
              });
            },
          };
        },
      };
      let trig = this.trigger.bind(this);
      setTimeout(trig, 0);
    }
    sleep(time) {
      this.queue.que.push(() => {
        setTimeout(() => {
          console.log("sleep ", time);
        }, time);
      });
      return this;
    }

    eat(food) {
      this.queue.que.push(() => {
        //console.log("eat " + food);
      });
      return this;
    }
    async trigger() {
      for await (let num of this.queue) {
        console.log(num);
      }
    }
  }
  let person = new LazyMan("HH")
    .eat("cola")
    .sleep(2000)
    .eat("bobA")
    .sleep(3000)
    .eat("garbage");

在trigger函数里遍历,就可以获得自动执行next()的好处(好处好小...,写完就发现多写了好多行),这里没有直接使用this.queue作为数组是因为一开始出了个bug,改成这样,后来忘改回去了,应该问题不大

首先是在何处new promise.promise一旦建立就开始执行exector函数里的内容,如果在注册的时候new promise,就不能做成一个一个依次执行的效果,所以要在执行next()函数的时候new promise

那么,压到queue里面的就是一个个函数,这个函数包装到next()构造的promise中,等到这个函数执行完后才能resolve,并且resolve函数的参数应该是一个 {value:函数执行完的返回值,done: 是否到达queue的终点} . done:true时,value应为undefined

  [Symbol.asyncIterator] = () => {
    let i = 0;

    let self = this;
    return {
      next: () => {
        return new Promise((resolve, reject) => {
          if (typeof self.queue.que[i] === "function") {
            self.queue.que[i]();
            //resolve()
              i++;
            });
          } else {
            reject({
              value:
                typeof self.queue.que[i] === "function" && self.queue.que[i](),
              done: true,
            });
          }
        });
      },
    };
  };

首先要执行queue里的函数,并获得函数执行结果作为返回值.并且要在执行完,

resolve({value:`return of function`,done);

想到对于一般的sleep函数,一般是

new Promise((resolve,reject)=>{
	setTimeout(()=>{
		//do something
		resolve(res);
	},time)
})

到这里有两个问题

  1. resolve要在setTimeout的回调函数里执行,但是我们这个回调函数已经固定了,就是类中的eat和sleep,那么如何做到这个呢.
  2. resolve 执行时,value的值需要queue里面的函数执行的返回值,而这个要先执行这个函数才能resolve,这就形成了矛盾

第一个问题,改变了一下调用的方式,在加入queue时的函数的参数增加一个回调函数,表示在执行完成正常操作后才执行,这样就可以在setTimeout里面执行resolve了

this.queue.que.push((callback) => {
  setTimeout(() => {
  console.log("sleep ", time);
    if (typeof callback !== "function")
      throw new Error("callback not function,not you mistake");
    callabck();
  }, time);
  return "sleep " + time + " return";
});
 
//a example for callback
(resolve,reject)=>{
    this.queue.que[i](()=>{
        resolve(data);
        i++
    })
}

这样就可以在执行完后在setTimeout中使用resolve让promise变成fulfilled

第二个问题,考虑到js中对象是引用值,考虑如下

let res={value:null,done:i>=self.queue.que.length-1};
res.value = self.queue.que[i](() => {
     resolve(res);
     i++;
 });

这样一边执行函数,还能返回函数的结果

最终代码

class LazyMan {
    constructor(name) {
      console.log("this is ", name);
      this.queue = {
        que: [],
        record: [],
        //这里不用箭头函数咋搞this啊
        [Symbol.asyncIterator]: () => {
          let i = 0;

          let self = this;
          return {
            next: () => {
              return new Promise((resolve, reject) => {
                if (typeof self.queue.que[i] === "function") {
                  let res = {
                    value: null,
                    done: i >= self.queue.que.length - 1,
                  };
                  res.value = self.queue.que[i](() => {
                    // console.log("resolve ",i);
                    resolve(res);
                    self.queue.record[i] = "1";
                    i++;
                  });
                } else {
                  reject({
                    value:
                      typeof self.queue.que[i] === "function" &&
                      self.queue.que[i](),
                    done: true,
                  });
                }
              });
            },
          };
        },
      };
      let trig = this.trigger.bind(this);
      //bind this or the function inside won`t work
      //or apply triggger in arrow function way
      // setTimeout(()=>this.trigger(),0);
      setTimeout(trig, 0);
    }
    sleep(time) {
      this.queue.que.push((callback) => {
        setTimeout(() => {
          console.log("sleep ", time);
          if (typeof callback !== "function")
            throw new Error("callback not function,not you mistake");
          callback();
        }, time);
        return "sleep " + time + " return";
      });
      return this;
    }

    eat(food) {
      this.queue.que.push((callback) => {
        console.log("eat " + food);
        // typeof callback === "function" && callback();
        if (typeof callback !== "function")
          throw new Error("callback not function,not you mistake");
        callback();
        return "eat " + food + " return";
      });
      return this;
    }
    async trigger() {
      // console.log(this);
      for await (let num of this.queue) {
        console.log(num);
      }
    }
  }
  let person = new LazyMan("HH")
    .eat("cola")
    .sleep(2000)
    .eat("bobA")
    .sleep(3000)
    .eat("garbage");

最终结果

diy.html:31 this is  HH
diy.html:88 eat cola
diy.html:100 eat cola return
diy.html:76 sleep  2000
diy.html:100 sleep 2000 return
diy.html:88 eat bobA
diy.html:100 eat bobA return
diy.html:76 sleep  3000
diy.html:100 sleep 3000 return
diy.html:88 eat garbage

ps

有一个小问题,最后一个没有return结果

若改成

done: i > self.queue.que.length - 1,

则报错

已修改

上述错误是因为最后有效值的时候 done也应为false,理解错误.

以下摘自es6.ruanyifeng.com/#docs/itera…

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

修改后next如下

            next: () => {
              return new Promise((resolve, reject) => {
                if(i>=self.queue.que.length){
                  resolve({value:undefined,done:true});
                }
                if (typeof self.queue.que[i] === "function" ) {
                  let res = {
                    value: null,
                    done: i >= self.queue.que.length ,
                  };
                  res.value = self.queue.que[i](() => {
                    // console.log("resolve ",i);
                    resolve(res);
                    self.queue.record[i] = "1";
                    i++;
                  });
                } else {
                  reject({
                    value:
                      typeof self.queue.que[i] === "function" &&
                      self.queue.que[i](),
                    done: true,
                  });
                }
              });
            },

虽然可能不太正经,还忘记看题目不许使用promise,就权当练习玩了.