fedono / fe-questions

1 stars 0 forks source link

14. 实现一个 LazyMan & 升级实现每个都能返回自己的 then (即asyncQueue) #15

Open fedono opened 4 years ago

fedono commented 4 years ago

问题

实现一个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

解法

对于这个问题,首先创建一个任务队列,然后利用next()函数来控制任务的顺序执行:

核心是队列和事件执行 这里有两个点是要注意的,第一个是在_LazyMan 的时候需要执行next函数,但不能立即执行,而是需要放到 setTimeout 里面去,这样才不会导致在 sleepFirst 的时候,已经把fns中第一个函数执行完了,先执行同步任务,然后再执行异步任务,这样才会把sleepFirst 放到第一个fns 中

第二个是在 eat/sleep 函数中,都是在 setTimeout 中执行 next 函数的


function _LazyMan(name) {
    this.fns = [];
    let self = this;
    const fn = () => {
        setTimeout(() => {
            console.log('Hi! This is ' + name);
            self.next();
        });
    }

    this.fns.push(fn);
    // 如果这里直接使用 self.next(),就无法将 sleepFirst 放到第一位来执行了
    setTimeout(() => {
        self.next();
    }, 0)
}

// 使用 next 来执行下一次的函数
_LazyMan.prototype.next = function() {
    let fn = this.fns.shift();
    fn && fn();
}

_LazyMan.prototype.sleep = function(time) {
    let self = this;
    const fn = () => {
        setTimeout(() => {
            console.log('Wake up after ' + time);
            self.next();
        }, time * 1000);
    }
    this.fns.push(fn);
    return this;
}

_LazyMan.prototype.eat = function(food) {
    let self = this;
    const fn = () => {
        setTimeout(() => {
            console.log('Eat ' + food);
            self.next();
        }, 0);
    }
    this.fns.push(fn);
    return this;
}

// 看到 sleepFirst 就知道要建立队列的概念
_LazyMan.prototype.sleepFirst = function(time) {
    let self = this;
    const fn = () => {
        setTimeout(() => {
            console.log('Wake up after  ' + time);
            self.next();
        }, time * 1000);
    }
    this.fns.unshift(fn);
    return this;
}

function LazyMan(name) {
    return new _LazyMan(name);
}

LazyMan('jack').sleep(2).eat('orange').sleepFirst(3)

参考

fedono commented 1 year ago

贴个自己完成的

重点有三个

  1. 首先就是方法放在 prototype 上,要不然拿不到对应的方法
  2. 第二是需要返回 this,要不然无法向下衔接
  3. 第三则是每次运行完当前的方法,则直接执行下一个方法
  4. 第四则是需要使用 setTimeout 来完成队列的形式

function LazyMan(name) {
    return new _LazyMan(name)
}

function _LazyMan(name) {
    this.name = name;
    this.queues = []

    // 这里首先要执行的 hello 
    this.Hello()

    // 这个 setTimeout 很重要,主要是为了 push 完 .sleepFirst/.eat 这些方法到队列中
    setTimeout(() => {
        this.exec()
    })
}

_LazyMan.prototype.Hello = function() {
    this.handler(() => {
        console.log(`Hi This is ${this.name}!`)
    })

    return this;
}

_LazyMan.prototype.handler = function(fn, number = 0, before = false) {
    const cur = () => setTimeout(() => {
        fn()
        this.exec()
    }, number * 1000)

    if (before) {
        this.queues.unshift(cur)
    } else {
        this.queues.push(cur)
    }
}

_LazyMan.prototype.exec = function(){
    const fn = this.queues.shift()
    fn?.()
}

_LazyMan.prototype.sleepFirst = function(time) {
    this.handler(() => {}, time, true)

    return this;
}

_LazyMan.prototype.eat = function(food)  {
    this.handler(() => {
        console.log(`Eat ${food} !`)
    })

    return this;
}
fedono commented 1 year ago
class AutoQueue {
  constructor() {
    this.tasks = [];
    this._pendingPromise = false;
  }

  enqueue(action) {
    return new Promise((resolve, reject) => {
      this.tasks.push({ action, resolve, reject });
      this.dequeue();
    });
  }

  async dequeue() {
    // 这个的主要目的,就是当前有一个正在执行时,下一个 action 就不要执行,等着前一个执行完
    if (this._pendingPromise) return false;

    let item = this.tasks.shift();

    if (!item) return false;

    try {
      this._pendingPromise = true;

      let payload = await item.action(this);

      this._pendingPromise = false;
      item.resolve(payload);
    } catch (e) {
      this._pendingPromise = false;
      item.reject(e);
    } finally {
      this.dequeue();
    }

    return true;
  }
}

// Helper function for 'fake' tasks
// Returned Promise is wrapped! (tasks should not run right after initialization)
let _ =
  ({ ms, ...foo } = {}) =>
  () =>
    new Promise((resolve) => setTimeout(resolve, ms, foo));
// ... create some fake tasks
let p1 = _({ ms: 50, url: '❪𝟭❫', data: { w: 1 } });
let p2 = _({ ms: 20, url: '❪𝟮❫', data: { x: 2 } });
let p3 = _({ ms: 70, url: '❪𝟯❫', data: { y: 3 } });
let p4 = _({ ms: 30, url: '❪𝟰❫', data: { z: 4 } });

const aQueue = new AutoQueue();
const start = performance.now();

aQueue
  .enqueue(p1)
  .then(({ url, data }) => console.log('%s DONE %fms', url, performance.now() - start)); //          = 50
aQueue
  .enqueue(p2)
  .then(({ url, data }) => console.log('%s DONE %fms', url, performance.now() - start)); // 50 + 20  = 70
aQueue
  .enqueue(p3)
  .then(({ url, data }) => console.log('%s DONE %fms', url, performance.now() - start)); // 70 + 70  = 140
aQueue
  .enqueue(p4)
  .then(({ url, data }) => console.log('%s DONE %fms', url, performance.now() - start)); // 140 + 30 = 170

来源 js-async-await-tasks-queue

fedono commented 1 year ago

贴一个实现的比较好的版本,不用 setTimeout,直接使用 Promise.resolve().then(fn) 的方式

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))

/*
interface Laziness {
  sleep: (time: number) => Laziness
  sleepFirst: (time: number) => Laziness
  eat: (food: string) => Laziness
}
*/

/**
 * @param {string} name
 * @param {(log: string) => void} logFn
 * @returns {Laziness}
 */
function LazyMan(name, logFn) {
  const cmds = [['greet', name]]

  const actions = {
    greet: name => logFn(`Hi, I'm ${name}.`),
    eat: food => logFn(`Eat ${food}.`),
    sleep: ms => sleep(ms * 1000).then(() => logFn(`Wake up after ${ms} second${ms > 1 ? 's' : ''}.`)),
  }

  Promise.resolve().then(exec)

  async function exec() {
    for (const [cmd, val] of cmds) {
      await actions[cmd](val)
    }
  }

  return {
    sleep(ms) {
      cmds.push(['sleep', ms])
      return this
    },
    sleepFirst(ms) {
      cmds.unshift(['sleep', ms])
      return this
    },
    eat(food) {
      cmds.push(['eat', food])
      return this
    },
  }
}

来自 JavaScript Coding Questions / 130. create LazyMan() / Discuss

fedono commented 1 year ago
  const timeout = (time) => new Promise(resolve => {
    setTimeout(resolve, time * 1000)
  })

  const scheduler = new Scheduler()
  const addTask = (time, order) => {
    scheduler.add(() => timeout(time))
      .then(() => console.log(order))
  }

//   每次最多执行两个
  addTask(1, 'jack1')
  addTask(2, 'jack2')
  addTask(3, 'jack3')
  addTask(4, 'jack4')

解法和 asyncQueue 差不多

class Scheduler {
    tasks = []
    count = 0
    add(promiseCreator) { 
        return new Promise((resolve, reject) => {
            // 这一步非常之关键,需要将 resolve/reject 都添加进去,这样每个 add 后面的 then,其实都可以获取当前运行的结果
            this.tasks.push([promiseCreator, resolve, reject])

            this.exec()
        })
     }

     async exec() {

        if (this.tasks.length === 0) {
            return
        }

        if (this.count >= 2) {
            return;
        }

        this.count++
        const [fn, resolve, reject] = this.tasks.shift()

        try {
            const res = await fn()
            resolve(res)
        } catch (error) {
            reject(error)            
        } finally {
            this.count--;

            this.exec()
        }
     }
  }