JavaScript:等待递归树完成,其中每个递归级别是一个 API 调用

JavaScript: Await for a recursive tree to finish, where each recursive level is an API call

我正在尝试使用递归 API 调用构建 JSON 树,但我在数据结构的流控制方面遇到问题。如何在 BuildTree 函数堆栈结束之前阻止流程?

这是部分代码。提前致谢。

//FIRST CALL TO RETRIEVE THE ROOT
function callFW() {
    d3.json(url, function(data) { //?<----- api call
            Tree["uid"] = data.request.uid
            Tree["hid"] = data.firmware.meta_data.hid
            Tree["size"] = data.firmware.meta_data.size
            Tree["children"] = [];
            
            BuildTree(data.firmware.meta_data.included_files,Tree["children"])
            //WAIT FOR BUILDTREE
            console.log(Tree)
        } 
}

BuildTree 函数是这样的:

async function BuildTree(included_files, fatherNode){ 
        if( included_files.length > 0){
            promises = [];

            included_files.forEach(function(item) {
                url = endpoint+"file_object/"+item+"?summary=true";
                promises.push(axios.get(url));
            });

            Promise.all(promises).then(function (results) {
                results.forEach(function (response) {
                    
                    var node = {}
                    node["uid"]= response.data.request.uid
                    node["hid"]= response.data.file_object.meta_data.hid
                    node["size"] = response.data.file_object.meta_data.size
                    node["children"] = []

                    fatherNode.push(node)

                    BuildTree(response.data.file_object.meta_data.included_files, node["children"])
                    


                });
            });
        }
    }

您无缘无故地使用了 async 关键字,因为您没有在函数内部使用 await。你不妨使用它:)

async function BuildTree(included_files, fatherNode) {
    if (included_files.length > 0) {

        let promises = included_files.map( item => {
            let url = endpoint + "file_object/" + item + "?summary=true";
            return axios.get(url)
        });

        const results = await Promise.all(promises);

        for(let response of results){

            var node = {}
            node["uid"] = response.data.request.uid
            node["hid"] = response.data.file_object.meta_data.hid
            node["size"] = response.data.file_object.meta_data.size
            node["children"] = []

            fatherNode.push(node)

            await BuildTree(response.data.file_object.meta_data.included_files, node["children"]);
        };
    }
};

然后你可以简单地 await BuildTree :

function callFW() {
    d3.json(url, async function(data) {
            Tree["uid"] = data.request.uid
            Tree["hid"] = data.firmware.meta_data.hid
            Tree["size"] = data.firmware.meta_data.size
            Tree["children"] = [];
            
            await BuildTree(data.firmware.meta_data.included_files,Tree["children"]);

            console.log(Tree)
        })
}

我认为让功能更多一些会更简洁self-contained。例如,主函数可以替换

    Tree["children"] = [];
        
    await BuildTree(data.firmware.meta_data.included_files,Tree["children"]);

    Tree["children"] = await BuildTree (data.firmware.meta_data.included_files)

如果我们稍微重组 BuildTree。为此,我们可以编写 BuildTree 来递归构建树的 children 节点,如下所示:

const endpoint = 'http://my.service/'

const BuildTree = async (files) => Promise .all (
  files .map ((name) => 
    axios .get (`${endpoint}file_object/${name}?summary=true`) 
      .then (async response => ({
        uid: response.data.request.uid,
        hid: response.data.file_object.meta_data.hid,
        size: response.data.file_object.meta_data.size,
        children: await BuildTree (response.data.file_object.meta_data.included_files)
      }))
  )
)


BuildTree (['ca26bcfc'])
  .then  (console .log)
  .catch (console .warn)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script>/* Faking axios call and service behind it */ const axios = {get: async (url, uid = url.match(/\/([^?\/]+)\?summary/)[1]) => ({data: {request: {uid}, file_object: {meta_data: {hid: guid(), size: randSize(), included_files: [...Array ((Math .random () * 3 | 0) * (Math .random () * 3 | 0))] .map (guid)}}}})}, guid = () => 'xxxxxxxx' .replace(/x/g, () => ((Math .random () * 16 | 0) .toString (16))), randSize = () => (Math.random() * 1000 | 0) + 1000</script>

(请注意,有一个无趣的虚拟版本 axios 涉及,伪造服务调用,随机提供 hidsize 参数并随机返回零到四个 children. 所以每一次 运行 都会给你返回不同的随机数据,有时是一层深,有时是五层或十层甚至更多层,但你可能要 运行 几次才能看到嵌套。 )

这里重要的是如何管理递归调用。它只是 returns 一个项目数组的承诺,所以我们可以插入它来生成 children 就像我们在 main 函数的调用中所做的那样。这会导致更清晰的代码。

我还要做一件事来改进这个功能。它正在使用全局变量 endpoint。这使得它不纯,更难测试,也更脆弱。相反,我们可以传入它。我更喜欢这个版本:

const BuildTree = async (endpoint, files) => Promise .all (
  files .map ((name) => 
    axios .get (`${endpoint}file_object/${name}?summary=true`) 
      .then (async response => ({
        uid: response.data.request.uid,
        hid: response.data.file_object.meta_data.hid,
        size: response.data.file_object.meta_data.size,
        children: await BuildTree (endpoint, response.data.file_object.meta_data.included_files)
      }))
  )
)

BuildTree ('http://my.service/', ['ca26bcfc'])
  .then (console .log)
  .catch (err => console .log (`Error: ${err}`))

甚至:

const BuildTree = (endpoint) => async (files) => Promise .all (
  // ...
        children: await BuildTree (endpoint) (response.data.file_object.meta_data.included_files)
  // ...
BuildTree ('http://my.service/') (['ca26bcfc']) 
  // ...

我还推荐一个类似 clean-up 的 main 函数,它不带参数,但使用全局变量 Tree,再次使它更难测试。但是我对你的环境了解不够,无法提供实用的建议。