面试题:控制依赖任务执行

遇到一个有意思的面试题,在这里留下思路。

题目

给定一个任务队列,然后执行,如果有依赖,就将依赖的结果执行返回,最终得到队列中每个任务的结果并返回。

const input = [
    {
        id: 'task1',
        deps: [],
        runTask: () => 3,
    },
    {
        id: 'task2',
        deps: ['task1','task3'],
        runTask: (res1, res3) => 1 + res1 + res3,
    },
    {
        id: 'task3',
        deps: ['task1'],
        runTask: (res1) => 5 + res1,
    },
    {
        id: 'task4',
        deps: ['task1', 'task2'],
        runTask: (res1, res2) => 3 + res1 + res2,
    },
];

function runAllTask(list, cb) {
    // TODO
}

runAllTask(input, (err, res) => {
    console.log(err);
    console.log(res);
    /** 
        res应该为:
        { task1: 3, task2: 12, task3: 8, task4: 18 }
    */
});

解法一:遇到没有值的依赖,放入队尾等待执行

思考:

1. 循环队列,执行任务(每次执行任务,先将队头任务弹出执行)
2. 依赖检查
    - 待执行的任务检查依赖如果遇到没有依赖的直接返回结果,并将结果保存
    - 遇到有依赖的,遍历依赖先从结果中匹配看是否有已经算出的结果,如果没有匹配到执行优先级绕后
    - 遍历依赖完成判断结果数组是否和依赖数组长度一样(每一个依赖都拿到的结果),执行结果保存
3. 循环上面的情况,直到队列中没有任务即可
4. 错误处理,如果上面执行代码出问题,返回err

代码

function runAllTask(list, cb) {
    // 创建结果数组
    let obj = {}
    // 错误处理
    try {
        // 循环执行任务
        while(list.length > 0) {
            // 弹出要执行的任务
            let task = list.shift();
            // 依赖判断
            if (!task.deps.length) {
                obj[task.id] = task.runTask()
            } else {
                // 有依赖,创建依赖结果数组
                let arr = []
                task.deps.every(val => {
                    // 有结果直接放入,继续循环
                    if (obj[val] !== undefined) {
                        arr.push(obj[val])
                        return true
                    // 无结果说明依赖内容没执行,放入队尾等待执行,循环结束
                    } else {
                        list.push(task);
                        return false
                    }
                })
                // 判断依赖是否全部执行完毕
                if (arr.length === task.deps.length) {
                    obj[task.id] = task.runTask(...arr)
                }
            }
        }
        // 返回结果
        cb(null, obj);
    } catch(e) {
        cb(e, null);
    }
}

// 执行结果
// { task1: 3, task3: 8, task2: 12, task4: 18 }

解到到这里基本问题是解决了,但是目前的这个方案,存在一些漏洞:

  1. 如果遇到依赖不存在的情况循环无法终止
  2. 如果遇到依赖循环的情况,队列循环也无法终止

优化解法一:加入依赖错误检测

思路是如果当前任务在循环中执行了第二次仍失败,那么就可以命定为该任务队列中有错误,需要终止执行,抛出错误。

第一种:假设最后一个任务才是没有依赖的任务,那么正常情况下,最大需要执行length的阶乘,如果进入循环都判断不能超过这个次数也是可以解决,但是这样性能消耗太大,有很多不必要的循环次数。

第二种:在当前执行的任务中,要记录一下上一个成功执行的任务名称是什么,如果上一次成功执行的名称一样,那么就判定进入到了死循环。

function runAllTask(list, cb) {
    let obj = {}
    // 记录一下上一个运行的id,避免没有匹配的task出现
    let last_task = null;
    try {
        // 如果都运行完,就有了结果
        while(list.length > 0) {
            let task = list.shift();
            if (!task.deps.length) {
                obj[task.id] = task.runTask()
                // 记录一下当前的task名称
                last_task = task.id
            } else {
                let arr = []
                // 遍历依赖,如果obj中有结果直接获取,没有结果返回到队尾
                task.deps.every(val => {
                    // 如果运行成功的上一个和当前一样,表示一个任务执行了两次,避免陷入死循环
                    if (task.last_task === last_task) throw new Error('no match')
                    if (obj[val] !== undefined) {
                        arr.push(obj[val])
                        return true
                    } else {
                        //将上一次成功的任务id记录到当前任务中
                        task.last_task = last_task
                        list.push(task);
                        return false
                    }
                })
                // 如果当前长度和任务依赖长度一样,说明执行成功
                if (arr.length === task.deps.length) {
                    // 将参数打散
                    obj[task.id] = task.runTask(...arr)
                    // 记录成功的任务id
                    last_task = task.id
                }
            }
        }
        // 返回成功的结果
        cb(null, obj);
    } catch(e) {
        // 返回错误的结果
        cb(e, null);
    }
}

这个方案解决了一些依赖错误导致的无法终止的问题,不过存在一些漏洞:
有问题的任务,还是循环了两三次以上,不必要的性能浪费

解法二:递归调用

如果不移动到队尾,直接递归调用依赖返回结果,这样就完美解决了上面的问题。

function runAllTask(list, cb) {
    // 数组改键值对(为了定义递归函数的时候,不用每次遍历数组)
    let listObj = {}
    // 结果数组
    let obj = {}
    try {
        // 数组改键值对
        list.forEach(val => {
            listObj[val.id] = val
        })
        // 遍历执行任务
        list.forEach(val => {
            runOneTask(val.id)
        })
        // 成功返回obj
        cb(null, obj)
    } catch(e) {
        cb(e, null)
    }

    // 执行单个任务
    function runOneTask(name) {
        // 如果结果里面有,直接读取结果,使用缓存
        if(obj[name]) return obj[name]
        // 当前依赖数组结果
        let depResult = [];
        // 遍历依赖
        listObj[name].deps.forEach(dep => {
            // 每个依赖递归执行结果
            depResult.push(runOneTask(dep))
        })
        // 将结果执行加到结果对象中
        obj[name] = listObj[name].runTask(...depResult)
        // 返回结果
        return obj[name]
    }
}

总结

考点掌握:

  1. 队列
  2. 递归
  3. 数组改键值对
  4. 值存储(类似缓存)
  5. 错误处理 try-catch
  6. 扩展运算符
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 1. iOS开发中的加密方式 iOS加密相关算法框架:CommonCrypto。 对称加密: DES、3DES、A...
    Dezi阅读 1,224评论 0 7
  • 最近准备复习一下面试题,看到了J_Knight_在18年的出一套 iOS 高级面试题尝试着回答一下题目,由于水平有...
    lkkwxy阅读 5,469评论 0 24
  • 怎么去设计一个组件封装 组件封装的目的是为了重用,提高开发效率和代码质量 低耦合,单一职责,可复用性,可维护性 前...
    凡凡_4e04阅读 421评论 0 0
  • 目录 ES5 和 ES6 分别几种方式声明变量DOM 事件有哪些阶段?谈谈对事件代理的理解ES6 的 class ...
    WEB前端含光阅读 513评论 0 4
  • (掌握)Http和Https的区别? http协议和https协议的区别:传输信息安全性不同、连接方式不同、端口不...
    Grit_1024阅读 3,239评论 0 2