美文网首页
面试题:控制依赖任务执行

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

作者: 顽皮的雪狐七七 | 来源:发表于2022-03-04 19:30 被阅读0次

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

    题目

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

    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. 扩展运算符

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/qemmrrtx.html