web-dev-qa-db-ja.com

SetTimeout(fn、0)が便利な場合があるのはなぜですか。

最近、コードがJavaScriptを介して動的に<select>をロードしていたという、かなり厄介なバグに遭遇しました。この動的にロードされた<select>は事前に選択された値を持っていました。 IE6では、選択された<option>を修正するコードがすでにありました。これは、<select>selectedIndex値が、選択された<option>index属性と同期しなくなることがあるためです。

field.selectedIndex = element.index;

しかし、このコードは機能しませんでした。フィールドのselectedIndexが正しく設定されていても、間違ったインデックスが選択されることになります。ただし、alert()ステートメントを正しいタイミングで挿入した場合は、正しいオプションが選択されます。これはある種のタイミングの問題になるかもしれないと思って、私は以前にコードで見たことがある何かランダムに試してみました:

var wrapFn = (function() {
    var myField = field;
    var myElement = element;

    return function() {
        myField.selectedIndex = myElement.index;
    }
})();
setTimeout(wrapFn, 0);

そしてこれはうまくいった!

私は自分の問題に対する解決策を持っていますが、なぜこれが私の問題を解決するのか正確にはわからないのは不安です。誰かが公式の説明を持っていますか? setTimeout()を使用して "後で"自分の関数を呼び出すことで回避できるブラウザの問題は何ですか?

783
Daniel Lew

協同マル​​チタスキングをしているのでこれはうまくいきます。

ブラウザは多くのことをほぼ一度にしなければならず、そのうちの1つがJavaScriptの実行です。しかし、JavaScriptが非常によく使用されることの1つは、ブラウザに表示要素を作成するように依頼することです。これは多くの場合同期的に行われると想定されます(特にJavaScriptが並列に実行されない場合)が、これが当てはまるという保証はなく、JavaScriptには明確に定義された待機メカニズムがありません。

解決策は、レンダリングスレッドが追いつくことができるようにJavaScriptの実行を「一時停止」することです。そしてこれは、タイムアウトが 0 の場合のsetTimeout()の効果です。 「すぐに実行する」と言っているように見えますが、実際には、この新しいJavaScriptに参加する前に終了するのを待っていた、JavaScript以外のことを終了する機会がブラウザに与えられます。 。

(実際には、setTimeout()は実行キューの最後に新しいJavaScriptを再キューします。長い説明へのリンクはコメントを参照してください。)

IE6はたまたまこのエラーを起こしやすいですが、私はそれがMozillaの古いバージョンとFirefoxで起こるのを見ました。


より詳しい説明についてはPhilip Roberts talk "イベントループとは一体何なの?" を参照してください。

745
staticsan

序文:

重要な注記:最も支持され、受け入れられていますが、@ staticsanによる受け入れられた答えは実際にはNOT CORRECT!です-理由の説明についてはDavid Mulderのコメントを参照してください.

他の回答のいくつかは正しいですが、実際に解決される問題が何であるかを示していないので、その詳細なイラストを提示するためにこの回答を作成しました。

そのため、ブラウザの機能とsetTimeout()の使用方法の詳細なウォークスルーを投稿しています。長く見えますが、実際には非常にシンプルで簡単です-私はそれを非常に詳細にしました。

UPDATE:JSFiddleを作成して、以下の説明を実演しました: http://jsfiddle.net/C2YBE/31/ 。多くの@ThangChungがキックスタートを支援してくれたことに感謝します

UPDATE2:JSFiddle Webサイトが停止した場合、またはコードを削除した場合に備えて、最後にこの回答にコードを追加しました。


詳細

「何かをする」ボタンと結果divを備えたWebアプリを想像してください。

「何かをする」ボタンのonClickハンドラーは、次の2つのことを行う関数「LongCalc()」を呼び出します。

  1. 非常に長い計算を行います(たとえば3分かかります)

  2. 計算結果を結果divに出力します。

今、ユーザーはこれをテストし、「何かをする」ボタンをクリックします。ページは3分間何もしないように見えますが、落ち着きなくなり、もう一度ボタンをクリックし、1分間待って、何も起こりません。

問題は明らかです-何が起こっているかを示す「ステータス」DIVが必要です。それがどのように機能するかを見てみましょう。


したがって、「ステータス」DIV(最初は空)を追加し、onclickハンドラー(関数LongCalc())を変更して4つのことを行います。

  1. ステータスDIVに「計算中...約3分かかる場合があります」というステータスを入力します

  2. 非常に長い計算を行います(たとえば3分かかります)

  3. 計算結果を結果divに出力します。

  4. ステータス「計算完了」をステータスDIVに設定します

そして、アプリを再テストするためにユーザーに喜んで提供します。

彼らは非常に怒ってあなたに戻ってきます。そして、ボタンをクリックしたときに、ステータスDIVが「Calculating ...」ステータスで更新されなかったことを説明してください!!!


あなたは頭をかいて、StackOverflowで尋ねて(またはドキュメントやgoogleを読んで)問題を理解します:

ブラウザーは、イベントから生じるすべての「TODO」タスク(UIタスクとJavaScriptコマンドの両方)を単一キューに配置します。残念ながら、新しい「計算中...」値を使用して「ステータス」DIVを再描画することは、キューの最後に移動する別個のTODOです。

ユーザーのテスト中のイベントの内訳、各イベント後のキューの内容は次のとおりです。

  • キュー:[Empty]
  • イベント:ボタンをクリックします。イベント後のキュー:[Execute OnClick handler(lines 1-4)]
  • イベント:OnClickハンドラーの最初の行を実行します(ステータスDIV値の変更など)。イベント後のキュー:[Execute OnClick handler(lines 2-4), re-draw Status DIV with new "Calculating" value]DOMの変更は瞬時に行われますが、対応するDOM要素を再描画するには、DOMの変更によってトリガーされ、キューの最後になった新しいイベントが必要です
  • 問題!!!問題!!!詳細は以下で説明します。
  • イベント:ハンドラーで2行目を実行します(計算)。後のキュー:[Execute OnClick handler(lines 3-4), re-draw Status DIV with "Calculating" value]
  • イベント:ハンドラーで3行目を実行します(結果DIVを取り込みます)。後のキュー:[Execute OnClick handler(line 4), re-draw Status DIV with "Calculating" value, re-draw result DIV with result]
  • イベント:ハンドラーの4行目を実行します(ステータスDIVに「DONE」を入力します)。キュー:[Execute OnClick handler, re-draw Status DIV with "Calculating" value, re-draw result DIV with result; re-draw Status DIV with "DONE" value]
  • イベント:returnハンドラーサブから暗黙のonclickを実行します。 「Execute OnClickハンドラ」をキューから取り出し、キューの次のアイテムの実行を開始します。
  • 注:既に計算が終了しているため、ユーザーには3分が既に経過しています。 再描画イベントはまだ発生していません!!!
  • イベント:「Calculating」値でステータスDIVを再描画します。再描画を行い、キューから削除します。
  • イベント:結果値で結果DIVを再描画します。再描画を行い、キューから削除します。
  • イベント:「完了」値でステータスDIVを再描画します。再描画を行い、キューから削除します。目の鋭い視聴者は、「ステータスDIVの「計算中」値が1ミリ秒未満で点滅することに気付くかもしれません-計算終了後

そのため、根本的な問題は、「Status」DIVの再描画イベントが最後にキューに置かれ、3分かかる「execute line 2」イベントの後、実際の再描画が行われないことです。計算が行われた後。


救いに来るのはsetTimeout()です。どのように役立ちますか? setTimeoutを介して長時間実行コードを呼び出すことにより、実際には2つのイベントを作成するためです:setTimeout実行自体、および(0タイムアウトにより)実行されるコードのキューエントリを分離します。

したがって、問題を解決するには、onClickハンドラーを2つのステートメントに変更します(新しい関数またはonClick内のブロックのみ)。

  1. ステータスDIVに「計算中...約3分かかる場合があります」というステータスを入力します

  2. 0タイムアウトでsetTimeout()を実行し、LongCalc() functionを呼び出します。

    LongCalc()関数は前回とほとんど同じですが、最初のステップとして「計算中...」ステータスDIV更新がありません。代わりに、すぐに計算を開始します。

では、イベントシーケンスとキューは現在どのようになっていますか?

  • キュー:[Empty]
  • イベント:ボタンをクリックします。イベント後のキュー:[Execute OnClick handler(status update, setTimeout() call)]
  • イベント:OnClickハンドラーの最初の行を実行します(ステータスDIV値の変更など)。イベント後のキュー:[Execute OnClick handler(which is a setTimeout call), re-draw Status DIV with new "Calculating" value]
  • イベント:ハンドラーの2行目を実行します(setTimeout呼び出し)。後のキュー:[re-draw Status DIV with "Calculating" value]。キューには、0秒以上新しいものはありません。
  • イベント:0秒後にタイムアウトのアラームがオフになります。後のキュー:[re-draw Status DIV with "Calculating" value, execute LongCalc (lines 1-3)]
  • イベント:「Calculating」値でステータスDIVを再描画します。後のキュー:[execute LongCalc (lines 1-3)]。この再描画イベントは、アラームが鳴る前に実際に発生する可能性があり、これも同様に機能することに注意してください。
  • ...

やった!計算が開始される前に、ステータスDIVが「計算中...」に更新されました!!!



以下は、これらの例を示すJSFiddleのサンプルコードです。 http://jsfiddle.net/C2YBE/31/

HTMLコード:

<table border=1>
    <tr><td><button id='do'>Do long calc - bad status!</button></td>
        <td><div id='status'>Not Calculating yet.</div></td>
    </tr>
    <tr><td><button id='do_ok'>Do long calc - good status!</button></td>
        <td><div id='status_ok'>Not Calculating yet.</div></td>
    </tr>
</table>

JavaScriptコード:(onDomReadyで実行され、jQuery 1.9が必要な場合があります)

function long_running(status_div) {

    var result = 0;
    // Use 1000/700/300 limits in Chrome, 
    //    300/100/100 in IE8, 
    //    1000/500/200 in FireFox
    // I have no idea why identical runtimes fail on diff browsers.
    for (var i = 0; i < 1000; i++) {
        for (var j = 0; j < 700; j++) {
            for (var k = 0; k < 300; k++) {
                result = result + i + j + k;
            }
        }
    }
    $(status_div).text('calculation done');
}

// Assign events to buttons
$('#do').on('click', function () {
    $('#status').text('calculating....');
    long_running('#status');
});

$('#do_ok').on('click', function () {
    $('#status_ok').text('calculating....');
    // This works on IE8. Works in Chrome
    // Does NOT work in FireFox 25 with timeout =0 or =1
    // DOES work in FF if you change timeout from 0 to 500
    window.setTimeout(function (){ long_running('#status_ok') }, 0);
});
627
DVK

John Resigによる JavaScriptタイマーの仕組み に関する記事を見てください。タイムアウトを設定すると、エンジンが現在の呼び出しスタックを実行するまで実際に非同期コードをキューに入れます。

86
Andy

ほとんどのブラウザはメインスレッドと呼ばれるプロセスを持っています。それはいくつかのJavaScriptタスク、UIの更新を実行する責任があります。

一部のJavaScript実行タスクおよびUI更新タスクは、ブラウザのメッセージキューに入れられてから、ブラウザのメインスレッドにディスパッチされて実行されます。

メインスレッドがビジーの間にUIの更新が生成されると、タスクはメッセージキューに追加されます。

setTimeout(fn, 0);は、このfnを実行されるキューの最後に追加します。指定された時間が経過したら、タスクがメッセージキューに追加されるようにスケジュールします。

21
arley

0に設定されていても、setTimeout()はDOM要素がロードされるまでしばらく時間がかかります。

これをチェックしてください: setTimeout

20
Jose Basilio

ここには相反する支持された答えがあります、そして証明なしでは誰が信じるべきかを知る方法がありません。これが@DVKが正しいことと@SalvadorDaliが間違っていることの証明です。後者の主張:

「そして、これが理由です。0ミリ秒の時間遅延でsetTimeoutを持つことは不可能です。最小値はブラウザによって決定され、それは0ミリ秒ではありません。歴史的にブラウザはこの最小値を10ミリ秒に設定します最近のブラウザでは4ミリ秒に設定されています。」

4msの最小タイムアウトは、起こっていることとは無関係です。実際に起こることは、setTimeoutがコールバック関数を実行キューの最後にプッシュするということです。 setTimeout(callback、0)の後に実行に数秒かかるブロッキングコードがある場合、ブロッキングコードが終了するまでコールバックは数秒間実行されません。このコードを試してください:

function testSettimeout0 () {
    var startTime = new Date().getTime()
    console.log('setting timeout 0 callback at ' +sinceStart())
    setTimeout(function(){
        console.log('in timeout callback at ' +sinceStart())
    }, 0)
    console.log('starting blocking loop at ' +sinceStart())
    while (sinceStart() < 3000) {
        continue
    }
    console.log('blocking loop ended at ' +sinceStart())
    return // functions below
    function sinceStart () {
        return new Date().getTime() - startTime
    } // sinceStart
} // testSettimeout0

出力は以下のとおりです。

setting timeout 0 callback at 0
starting blocking loop at 5
blocking loop ended at 3000
in timeout callback at 3033
18
Vladimir Kornea

その理由の1つは、コードの実行を別の後続のイベントループに遅らせることです。ある種のブラウザイベント(マウスクリックなど)に応答するとき、時々操作のみを実行する必要がある場合 現在のイベントが処理されます。 setTimeout()機能は最も簡単な方法です。

編集 2015年になったので、requestAnimationFrame()もあることに注意してください。これはまったく同じではありませんが、言及する価値があるsetTimeout(fn, 0)に非常に近いです。

13
Pointy

これは古い質問と古い答えです。私はこの問題に新たな見方を加え、なぜこれが起こるのか、なぜこれが役に立つのかではないことに答えることを望みました。

だからあなたは2つの機能があります:

var f1 = function () {    
   setTimeout(function(){
      console.log("f1", "First function call...");
   }, 0);
};

var f2 = function () {
    console.log("f2", "Second call...");
};

そして、2番目のものが最初に実行されたことを確認するためにf1(); f2();という順番でそれらを呼び出します。

そしてこれがその理由です:setTimeoutを0ミリ秒の時間遅延で持つことは不可能です。 最小値はブラウザによって決定されます それは0ミリ秒ではないです。 歴史的に browsersはこの最小値を10ミリ秒に設定していますが、 HTML5仕様 および最近のブラウザでは4ミリ秒に設定されています。

ネストレベルが5より大きく、タイムアウトが4より小さい場合は、タイムアウトを4に増やします。

Mozillaからも。

最近のブラウザで0ミリ秒のタイムアウトを実装するには、 here のようにwindow.postMessage()を使用します。

P.S以下の 記事 を読んだ後に情報が取られます。

9
Salvador Dali

それは0の期間渡されているので、私はそれが実行の流れからsetTimeoutに渡されたコードを取り除くためであると思います。そのため、時間がかかる可能性がある関数であっても、それ以降のコードの実行が妨げられることはありません。

8
user113716

これら2つの評価の高い答えはどちらも間違っています。 並行性モデルとイベントループに関するMDNの説明を確認してください そして、何が起こっているのか(MDNリソースが本当の宝石であること)が明らかになるはずです。そして 単純に setTimeoutを使用すると、この小さな問題を「解決する」ことに加えて、予期しない問題がコードに追加される可能性があります。

実際に ここで行っているのは、「ブラウザがまだ完全に準備ができていない可能性があるため」、または「各行がキューの後ろに追加されるイベント」ということではありません。

DVKが提供する jsfiddle は確かに問題を示していますが、それに対する彼の説明は正しくありません。

彼のコードで起こっていることは、彼が最初に#doボタンのclickイベントにイベントハンドラをアタッチしていることです。

その後、実際にボタンをクリックすると、イベントハンドラ関数を参照するmessageが作成され、それがmessage queueに追加されます。 event loopがこのメッセージに到達すると、jsfiddleのclickイベントハンドラへの関数呼び出しを使用して、スタック上にframeを作成します。

そして、これが面白いところです。私たちはJavascriptが非同期であると考えることに慣れているので、この小さな事実を見逃しがちです: 次のフレームを実行する前に、すべてのフレームを実行しなければなりません 。並行性はありません、人々。

これは何を意味するのでしょうか?つまり、関数がメッセージキューから呼び出されると、生成されたスタックが空になるまでキューがブロックされます。あるいは、より一般的には、関数が戻るまでブロックします。そしてそれは、DOMレンダリング操作、スクロール、そしてwhatnotを含む everything をブロックします。確認が必要な場合は、フィドル内で長時間実行されている操作の継続時間を長くする(たとえば、外側のループをさらに10回実行する)と、実行中はページをスクロールできません。それが十分に長く動くならば、あなたのブラウザはあなたがプロセスを殺すことを望むかどうかあなたに尋ねるでしょう、なぜならそれはページを無反応にしているからです。フレームは実行されており、イベントループとメッセージキューは終了するまで動かなくなります。

では、なぜテキストのこの副作用が更新されないのでしょうか。 DOM内の要素の値を have に変更したとき - 変更した直後にその値をconsole.log()に変更でき、に変更されたことを確認できますt correct) - ブラウザはスタックが使い果たされる(onハンドラ関数が戻る)のを待ってメッセージが終了するのを待っているので、最終的にはランタイムによって追加されたメッセージを我々の突然変異操作、そしてその突然変異をUIに反映させるために。

これは、コードが実行を終了するのを実際に待っているためです。イベントベースの非同期Javascriptで通常行うように、「だれかがこれを取得してからこの関数を呼び出して結果を返す」とは言っていません。 clickイベントハンドラ関数に入り、DOM要素を更新し、別の関数を呼び出します。もう一方の関数は長時間動作してから戻り、その後同じDOM要素を更新します。そして then から戻ります。初期関数。事実上スタックを空にします。そして then ブラウザはキュー内の次のメッセージにたどり着くことができます。これは内部の "on-DOM-mutation"タイプのイベントをトリガーすることによって生成されたメッセージかもしれません。

ブラウザのUIは、現在実行中のフレームが完了する(関数が戻る)までUIを更新できません(または更新しないことを選択します)。個人的には、これは制限ではなく設計によるものだと思います。

なぜsetTimeoutがうまくいくのでしょうか。これは、長時間実行される関数への呼び出しをそれ自身のフレームから効果的に削除し、後でwindowコンテキストで実行されるようにスケジュールし、それ自体で すぐに戻る にしてメッセージキューを許可するためです。他のメッセージを処理します。そして考えは、DOM内のテキストを変更したときにJavascriptで私たちによって引き起こされたUI "on update"メッセージは、長期実行機能のためにキューに入れられたメッセージよりも先になっているためです。長い間。

A)長時間実行される関数 は、実行時に allをブロックします。そしてb)UIの更新が実際にメッセージキューの先頭にあることを保証するものではありません。私の2018年6月のChromeブラウザでは、値0はフィドルが示す問題を「修正」していません - 10は修正します。 UI更新メッセージはそれより前にキューに入れられるべきだというのは論理的に思えるので、私は実際には少し動揺しています。しかし、おそらくV8エンジンには干渉する可能性のある最適化がいくつかあるか、または私の理解が足りない可能性があります。

さて、それでsetTimeoutを使うことの問題は何ですか、そしてこの特定のケースのためのより良い解決策は何ですか?

まず、このようなイベントハンドラでsetTimeoutを使用して別の問題を軽減しようとすると、他のコードで混乱しやすくなります。これが私の作品の実例です。

同僚は、イベントループについての情報を誤って理解しているため、テンプレートのレンダリングコードにレンダリングにsetTimeout 0を使用することでJavascriptを「スレッド化」しようとしました。彼はもう尋ねる必要はありませんが、おそらくレンダリング速度を測定するためのタイマーを挿入し(これは関数の戻りの即時性になるでしょう)、このアプローチを使用するとその関数からのすばやい応答に役立つことがわかりました。

最初の問題は明白です。あなたはjavascriptを通すことができないので、難読化を追加している間、ここでは何も勝ちません。次に、テンプレートのレンダリングは、そのテンプレートがレンダリングされたと予想される可能性のあるイベントリスナのスタックから効果的に切り離されましたが、実際にはそうではありませんでした。その関数の実際の振る舞いは今や非決定的なものでした - 無意識のうちに - それを実行する、またはそれに依存する関数。あなたは知識のある推測をすることはできますが、その振る舞いを正しくコーディングすることはできません。

そのロジックに依存する新しいイベントハンドラを書くときの "修正"はuse setTimeout 0でした。しかし、これは修正方法ではなく、理解するのも難しく、このようなコードが原因で発生したエラーをデバッグするのは楽しいことではありません。時には問題がない場合もあれば、それが一貫して失敗した場合もあります。また、プラットフォームの現在のパフォーマンスやその時のその他の状況によっては、時折問題が発生して中断することもあります。これが、私が個人的にこのハックを使用しないことをお勧めする理由です(それが is ハックであり、我々は皆それがそれであることを知っているべきです)。

しかし、 can の代わりに何ができますか?参照されているMDNの記事で示唆されているように、キューに入れられている他のメッセージが実行中にインターリーブされて実行されるように作業を複数のメッセージに分割するか、実行できるWebワーカーを使用します。あなたのページと連携して、その計算が終わったら結果を返します。

ああ、そしてあなたが考えているのなら、「まあ、それを非同期にするために長期実行関数にただコールバックを入れることはできないのですか?」そしていいえ。コールバックは非同期にはしません、それはあなたのコールバックを明示的に呼び出す前にそれはまだ長い時間のかかるコードを実行しなければならないでしょう。

これが他に行うことは、関数呼び出しをスタックの一番下にプッシュして、関数を再帰的に呼び出す場合にスタックオーバーフローを防ぐことです。これはwhileループの効果がありますが、JavaScriptエンジンに他の非同期タイマーを起動させることができます。

3
Jason Suárez

他のコードが完了する前に実行ループとDOMのレンダリングに関する答えは正しいです。 JavaScriptでは、タイムアウトが0秒であるため、コードが擬似マルチスレッド化されることはありませんが、そうではありません。

クロック制限のため、多くのモバイルブラウザでは20ミリ秒より短いタイムアウトを登録できないため、JavaScriptでのクロスブラウザ/クロスプラットフォームのゼロ秒タイムアウトのBEST値は実際には0(ゼロ)ではなく約20ミリ秒です。 AMDチップ上。

また、DOM操作を伴わない長期実行プロセスは、Webワーカーに送信する必要があります。それらは、JavaScriptの真のマルチスレッド実行を提供するからです。

2
ChrisN

SetTimeoutが便利なその他の場合

ブラウザが「フリーズ」したり「ページのスクリプトがビジーです」と表示されないように、長時間のループや計算を小さなコンポーネントに分割したいとします。

クリックしたときにフォーム送信ボタンを無効にしたいが、onClickハンドラでボタンを無効にした場合、フォームは送信されません。時間をゼロに設定したsetTimeoutを使用すると、イベントを終了し、フォームを送信し始め、ボタンを無効にすることができます。

1
fabspro

問題は、既存の要素に対してJavascript操作を実行しようとしていたことです。要素はまだロードされていなかったため、setTimeout()は次の方法で要素をロードする時間を増やします。

  1. setTimeout()により、イベントはansynchronousになります。したがって、すべての同期コードの後に​​実行され、要素の読み込み時間が長くなります。 setTimeout()のコールバックのような非同期コールバックは、イベントキューに配置され、同期コードのスタックが空になった後、イベントループによってスタックに配置されます。
  2. 関数setTimeout()の2番目の引数としてのmsの値0は、多くの場合わずかに高くなります(ブラウザによって4〜10ミリ秒)。 setTimeout()コールバックの実行に必要なこのわずかに長い時間は、イベントループの「ティック」(スタックが空の場合にティックがスタックにコールバックをプッシュする)の量が原因です。パフォーマンスとバッテリー寿命の理由により、イベントループのティックの量は特定の量に制限されますless than 1000 times per second。
1

setTimeoutを0に設定すると、遅延プロミスを設定するパターンでも非常に便利で、すぐに返すことができます。

myObject.prototype.myMethodDeferred = function() {
    var deferredObject = $.Deferred();
    var that = this;  // Because setTimeout won't work right with this
    setTimeout(function() { 
        return myMethodActualWork.call(that, deferredObject);
    }, 0);
    return deferredObject.promise();
}
1
Stephan G

SetTimeoutを呼び出すことで、ユーザーがしていることすべてに反応するためのページ時間を与えることができます。これは、ページのロード中に実行される機能に特に役立ちます。

1
Jeremy

Javascriptはシングルスレッドのアプリケーションであるため、このイベントループを達成するために関数を同時に実行することはできません。そのため、setTimeout(fn、0)がコールスタックが空のときに実行されるタスククエストに入れられるのは、まさにそのとおりです。私はこの説明がかなり退屈であることを知っています、それで私はあなたがこのビデオを通して経験することがあなたが物事がブラウザのフードの下でどのように働くかをあなたに助けるでしょう。このビデオをチェックしてください: - https://www.youtube.com/watch?time_continue=392&v=8aGhZQkoFbQ

0
Jani Devang