غير متزامن - جافاسكريبت: كيفية التحكم في التدفق باستخدام استدعاء متكرر غير متزامن لتصفح الشجرة؟


أحتاج إلى التكرار عبر شجرة لأداء العمليات على عقد محددة باستخدام العمليات غير المتزامنة. كيف يمكنني التحكم في التدفق بحيث أتمكن من الوصول إلى العقد عند الانتهاء؟

إليك مثال على موقف:

data = {
  name: "deven",
  children: [
    { name: "andrew" },
    { name: "donovan" },
    { name: "james",
      children: [
        { name: "donatello" },
        { name: "dan" }
      ]
    },
    { name: "jimmy",
      children: [
        { name: "mike" },
        { name: "dank" }
      ]
    }
  ]
};

لدي دالة هدفها هو التنقل عبر الشجرة وتحويل أحرف جميع الأسماء التي تبدأ بـ 'd' إلى أحرف كبيرة. بعد ذلك، أريد تمرير الشجرة إلى دالة أخرى للقيام ببعض العمل الإضافي (ربما حذف جميع العقد التي تحتوي على اسم يبدأ بـ 'a')، ولكن فقط بعد الانتهاء من المعالجة الأولية:

function capitalize_d(node) {
    if(node.name === "d") {
        node.name = node.name.toUpperCase();
    }

    if(node.children != null) {
        for(var i = 0; i < node.children.length; i++) {
            capitalize_d(node.children[i]);
        }
    }
}

function remove_a(node) {
}

capitalize_d(data);

// Should only get called after all the d's have been capitalized.
remove_a(data);

الكود أعلاه يعمل بشكل جيد، لأن capitalize_d حجمي. إذا كان capitalize_d يستدعي نفسه بشكل غير متزامن، كيف يمكننا ضمان استدعاء remove_a بعد انتهائه؟ لاحظ استدعاء setTimeout في capitalize_d.

function capitalize_d(node) {
    setTimeout(function() {

        if(node.name === "d") {
            node.name = node.name.toUpperCase();
        }

        if(node.children != null) {
            for(var i = 0; i < node.children.length; i++) {
                capitalize_d(node.children[i]);
            }
        }

    }, 1);
}

function remove_a(node) {
}

capitalize_d(data);

// Should only get called after all the d's have been capitalized.
remove_a(data);

المشكلة هي أن لدينا معالجة لفروع مختلفة من الشجرة جميعها يتم تشغيلها في نفس الوقت، ومن المستحيل معرفة متى تنتهي المعالجة بالفعل.

كيف يمكنني حل هذا؟

الإجابة رقم 1

دعني ألخص ما فهمته من متطلباتك:

  • لديك شجرة بيانات حيث يمكن تعديل كل عقدة فردية بشكل غير متزامن بواسطة مجموعة من العمليات (capitalize_d و remove_a في مثالك)،
  • تريد التأكد من أن كل عقدة قد خضعت للعملية المعطاة قبل السماح بالانتقال إلى التالية.

لقد أمضيت أكثر من 10 سنوات في تصميم برامج مضمنة في الوقت الحقيقي، وصدقني، المتطلبات في هذا المجال أكثر قسوة ورعبًا من أي شيء سيعيشه معظم مبرمجي الشبكات طوال حياتهم. هذا يجعلني أحذرك من أنك تبدو متجهًا في الطريق الخطأ بشكل جاد هنا.

كما يمكنني أن أتخيل، مشكلتك هي تنظيم مجموعة من البيانات الفردية في نوع من البنية ذات المعنى. بعض العمليات تجمع قطعًا عشوائية من المعلومات (ما تسميه 'العقد' في مثالك)، وفي مرحلة ما تريد وضع كل تلك العقد في بنية بيانات متسقة وموحدة (شجرة هرمية في مثالك).

بمعنى آخر، لديك ثلاث مهام في متناول يدك:

  • عملية جمع بيانات ستقوم بجمع العقد بشكل غير متزامن
  • عملية إنتاج بيانات ستعرض شجرة بيانات متسقة ومُنقحة
  • عملية تحكم ستقوم بمزامنة جمع البيانات والإنتاج (ربما مباشرة مع واجهة المستخدم إذا كانت العمليتان أعلاه ذكية بما فيه الكفاية، لكن لا تعتمد على ذلك كثيرًا).

نصيحتي: لا تحاول القيام بالاكتساب والإنتاج في نفس الوقت.

مجرد لإعطائك فكرة عن الكابوس الذي تتجه نحوه:

  • اعتمادًا على كيفية تشغيل العمليات، هناك احتمال ألا تتم معالجة الشجرة بالكامل من قبل عملية معينة. لنفترض أن البرنامج المسيطر نسى استدعاء capitalize_d على بعض العقد، فـ remove_a ببساطة لن يحصل على الضوء الأخضر أبدًا

  • على العكس، إذا أطلقت النار على الشجرة بشكل عشوائي، فمن المحتمل جدًا أن تتم معالجة بعض العقد عدة مرات، ما لم تتابع تغطية العمليات لمنع تطبيق نفس التحويل مرتين على عقدة معينة

  • إذا كنت تريد أن يبدأ معالجة remove_a على الإطلاق، قد تضطر إلى منع البرنامج المسيطر من إرسال أي طلبات capitalize_d أخرى، وإلا قد يبقى الضوء عالقًا على اللون الأحمر إلى الأبد. في النهاية، ستضطر إلى التحكم في تدفق طلباتك بطريقة أو بأخرى (أو الأسوأ: لن تفعل أي شيء، ومن المحتمل أن يتجمد نظامك تمامًا إذا خرج تدفق العملية عن النقطة المثالية التي صادفت الوصول إليها بالصدفة).

  • إذا كانت العملية تغير بنية الشجرة (كما يفعل remove_a بوضوح)، يجب عليك منع الوصولات المتزامنة. على الأقل، ينبغي عليك قفل الشجرة الجزئية بدءًا من العقدة التي يعمل عليها remove_a، وإلا فإنك ستسمح بمعالجة شجرة جزئية من المحتمل أن تتغير وتُدمر بشكل غير متزامن.

حسنًا، هذا ممكن. لقد رأيت رجالًا شبابًا جيدين يجنون أموالًا كبيرة من خلال القيام بتغييرات على هذا الموضوع. عادةً ما يقضون بضعة أمسيات كل أسبوع في تناول البيتزا أمام حواسيبهم أيضًا، لكن مهلاً، هذه هي الطريقة التي يمكنك من خلالها التمييز بين القراصنة الجادين وجمهور آكلي الكيش، أليس كذلك؟...

أفترض أن قيامك بنشر هذا السؤال هنا يعني أنك لا تريد حقًا القيام بذلك. الآن إذا كان رئيسك يريد، حسنًا، لأقتبس من أحد الروبوتات الشهيرة، لا أستطيع أن أكذب عليك بشأن فرصك، لكن... لديك تعاطفي.

الآن بجدية يا جماعة.. هذه هي الطريقة التي سأتعامل بها مع المشكلة.

1) التقط لقطة لبياناتك في وقت معين

يمكنك تصفية البيانات الخام باستخدام أكبر عدد ممكن من المعايير (مثل أن تكون آخر عملية جمع بيانات قديمة جدًا، أو الإدخال غير الصحيح، أو أي شيء يسمح لك ببناء أصغر شجرة ممكنة).

2) قم ببناء الشجرة باستخدام اللقطة، ثم قم بتطبيق أي عمليات capitalize_d و remove_a و camelize_z تسلسليًا على هذه اللقطة المحددة.

بالتوازي، ستستمر عملية جمع البيانات في جمع العقد الجديدة أو تحديث القائمة الحالية، جاهزة لأخذ اللقطة التالية.

بالإضافة إلى ذلك، يمكنك نقل بعض معالجة البيانات إلى الأمام. من الواضح أن capitalize_d لا يستفيد من هيكل الشجرة، لذا يمكنك تطبيق capitalize_d على كل عقدة في اللقطة قبل بناء الشجرة حتى. قد تتمكن حتى من تطبيق بعض التحولات في وقت أبكر، أي على كل عينة تم جمعها. يمكن أن يوفر لك هذا الكثير من وقت المعالجة وتعقيد الكود.

لإنهاء الأمر ببعض الثرثرة النظرية,

  • نهجك هو اعتبار شجرة البيانات ككائن مشترك يجب أن تدعم الوصول المتزامن من عمليات جمع البيانات وإنتاج البيانات.
  • نهجي هو جعل عملية الحصول على البيانات تخدم (بشكل غير متزامن) مجموعات بيانات متسقة لعملية إنتاج البيانات، والتي يمكنها بعد ذلك التعامل تتابعياً مع مجموعة البيانات المذكورة.

يمكن تشغيل عملية إنتاج البيانات عند الطلب (لنقل عند ضغط المستخدم النهائي على زر "أرني شيئًا"), وفي هذه الحالة ستكون الاستجابة ضعيفة إلى حد ما: سيظل المستخدم يراقب الساعة الرملية أو أي عجلة دوران أنيقة في Web2.0 طوال الوقت اللازم لبناء ومعالجة الشجرة (لنفترض 7-8 ثوانٍ).

أو يمكنك تنشيط عملية إنتاج البيانات بشكل دوري (تغذيتها بلقطة جديدة كل 10 ثوانٍ، وهي فترة آمنة تتجاوز متوسط زمن معالجة مجموعة البيانات). عندها، سيقوم زر "أرني شيئًا" بعرض آخر مجموعة من البيانات المكتملة. إجابة فورية، لكنها تحتوي على بيانات قد تكون أقدم بعشر ثوانٍ من العينات المستلمة الأخيرة.

نادراً ما رأيت حالات لم يُعتبر فيها هذا مقبولاً، خاصة عندما تنتج مجموعة من البيانات المعقدة يحتاج المشغل لبضع عشرات من الثواني لهضمها.

نظريًا، ستفقد طريقتي بعض التفاعلية، نظرًا لأن البيانات المعالجة ستكون قديمة بعض الشيء، لكن أسلوب الوصول المتزامن من المحتمل أن يؤدي إلى برنامج أبطأ حتى (وبالتأكيد أكبر بخمسة إلى عشرة أضعاف وأكثر عرضة للأخطاء).

الإجابة رقم 2

أعلم أن هذا المنشور قديم، لكنه ظهر في نتائج البحث، والرد الوحيد لا يقدم مثالاً عملياً، لذا إليكم نسخة معدلة من شيء قمت به مؤخراً...

function processTree(rootNode, onComplete) {

    // Count of outstanding requests.
    // Upon a return of any request,
    // if this count is zero, we know we're done.
    var outstandingRequests = 0;

    // A list of processed nodes,
    // which is used to handle artifacts
    // of non-tree graphs (cycles, etc).
    // Technically, since we're processing a "tree",
    // this logic isn't needed, and could be
    // completely removed.
    //
    // ... but this also gives us something to inspect
    // in the sample test code. :)
    var processedNodes = [];

    function markRequestStart() {
        outstandingRequests++;
    }

    function markRequestComplete() {
        outstandingRequests--;
        // We're done, let's execute the overall callback
        if (outstandingRequests < 1) { onComplete(processedNodes); }
    }

    function processNode(node) {
        // Kickoff request for this node
        markRequestStart();
        // (We use a regular HTTP GET request as a
        // stand-in for any asynchronous action)
        jQuery.get("/?uid="+node.uid, function(data) {
            processedNodes[node.uid] = data;
        }).fail(function() {
            console.log("Request failed!");
        }).always(function() {
            // When the request returns:
            // 1) Mark it as complete in the ref count
            // 2) Execute the overall callback if the ref count hits zero
            markRequestComplete();
        });

        // Recursively process all child nodes (kicking off requests for each)
        node.children.forEach(function (childNode) {
            // Only process nodes not already processed
            // (only happens for non-tree graphs,
            // which could include cycles or multi-parent nodes)
            if (processedNodes.indexOf(childNode.uid) < 0) {
                processNode(childNode);
            }
        });

    }

    processNode(rootNode);
}

وإليك مثال على الاستخدام باستخدام QUnit:

QUnit.test( "async-example", function( assert ) {
    var done = assert.async();

    var root = {
        uid: "Root",
        children: [{
            uid: "Node A",
            children: [{
                uid: "Node A.A",
                children: []
            }]
        },{
            uid: "Node B",
            children: []
        }]
    };

    processTree(root, function(processedNodes) {
        assert.equal(Object.keys(processedNodes).length, 4);
        assert.ok(processedNodes['Root']);
        assert.ok(processedNodes['Node A']);
        assert.ok(processedNodes['Node A.A']);
        assert.ok(processedNodes['Node B']);
        done();
    });
});