Phá vỡ chuỗi hứa hẹn và gọi một chức năng dựa trên bước trong chuỗi nơi nó bị hỏng (bị từ chối)


135

Cập nhật:

Để giúp những người xem trong tương lai của bài đăng này, tôi đã tạo bản demo câu trả lời này .

Câu hỏi:

Mục tiêu của tôi có vẻ khá đơn giản.

  step(1)
  .then(function() {
    return step(2);
  }, function() {
    stepError(1);
    return $q.reject();
  })
  .then(function() {

  }, function() {
    stepError(2);
  });

  function step(n) {
    var deferred = $q.defer();
    //fail on step 1
    (n === 1) ? deferred.reject() : deferred.resolve();
    return deferred.promise;
  }
  function stepError(n) {
    console.log(n); 
  }

Vấn đề ở đây là nếu tôi thất bại ở bước 1, cả stepError(1)AND stepError(2)đều bị sa thải. Nếu tôi làm không return $q.rejectthì stepError(2)sẽ không bị sa thải, nhưng step(2)sẽ, mà tôi hiểu được. Tôi đã hoàn thành mọi thứ trừ những gì tôi đang cố gắng làm.

Làm cách nào để viết lời hứa để tôi có thể gọi một hàm từ chối, mà không gọi tất cả các hàm trong chuỗi lỗi? Hoặc có một cách khác để thực hiện điều này?

Đây là một bản demo trực tiếp để bạn có một cái gì đó làm việc với.

Cập nhật:

Tôi loại đã giải quyết nó. Ở đây, tôi đang bắt lỗi ở cuối chuỗi và chuyển dữ liệu tới reject(data)để tôi sẽ biết vấn đề nào cần xử lý trong hàm lỗi. Điều này thực sự không đáp ứng yêu cầu của tôi vì tôi không muốn phụ thuộc vào dữ liệu. Sẽ là khập khiễng, nhưng trong trường hợp của tôi, việc chuyển một cuộc gọi lại lỗi cho hàm sẽ tốt hơn thay vì phụ thuộc vào dữ liệu được trả về để xác định việc cần làm.

Bản demo trực tiếp tại đây (click).

step(1)
  .then(function() {
    return step(2);
  })
  .then(function() {
    return step(3);
  })
  .then(false, 
    function(x) {
      stepError(x);
    }
  );
  function step(n) {
    console.log('Step '+n);
    var deferred = $q.defer();
    (n === 1) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
  }
  function stepError(n) {
    console.log('Error '+n); 
  }

1
Có một lib javascript async có thể giúp nếu điều này trở nên phức tạp hơn
lucuma

Promise.prototype.catch()các ví dụ về MDN hiển thị giải pháp cho các vấn đề chính xác tương tự.
toraritte

Câu trả lời:


199

Lý do mã của bạn không hoạt động như mong đợi là vì nó thực sự đang làm một cái gì đó khác với những gì bạn nghĩ.

Giả sử bạn có một cái gì đó như sau:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

Để hiểu rõ hơn những gì đang xảy ra, hãy giả vờ đây là mã đồng bộ với try/ catchkhối:

try {
    try {
        try {
            var a = stepOne();
        } catch(e1) {
            a = handleErrorOne(e1);
        }
        var b = stepTwo(a);
    } catch(e2) {
        b = handleErrorTwo(e2);
    }
    var c = stepThree(b);
} catch(e3) {
    c = handleErrorThree(e3);
}

Trình onRejectedxử lý (đối số thứ hai của then) về cơ bản là một cơ chế sửa lỗi (giống như một catchkhối). Nếu một lỗi được ném vào handleErrorOne, nó sẽ bị bắt bởi khối bắt tiếp theo (catch(e2) ), v.v.

Đây rõ ràng không phải là những gì bạn dự định.

Hãy nói rằng chúng tôi muốn toàn bộ chuỗi giải quyết thất bại cho dù có vấn đề gì xảy ra:

stepOne()
.then(function(a) {
    return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
    return stepThree(b).then(null, handleErrorThree);
});

Lưu ý: Chúng ta có thể rời khỏi vị handleErrorOnetrí của nó, bởi vì nó sẽ chỉ được gọi nếustepOne từ chối (đó là chức năng đầu tiên trong chuỗi, vì vậy chúng ta biết rằng nếu chuỗi bị từ chối tại thời điểm này, thì chỉ có thể là do lời hứa của chức năng đó) .

Thay đổi quan trọng là các trình xử lý lỗi cho các chức năng khác không phải là một phần của chuỗi hứa hẹn chính. Thay vào đó, mỗi bước có "chuỗi con" riêng với mộtonRejected chỉ được gọi nếu bước bị từ chối (nhưng chuỗi chính không thể đạt được trực tiếp).

Lý do điều này hoạt động là cả hai onFulfilledonRejectedlà đối số tùy chọn cho thenphương thức. Nếu một lời hứa được thực hiện (nghĩa là được giải quyết) và lần tiếp theo thentrong chuỗi không có onFulfilledtrình xử lý, chuỗi sẽ tiếp tục cho đến khi có một trình xử lý như vậy.

Điều này có nghĩa là hai dòng sau là tương đương:

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

Nhưng dòng sau không tương đương với hai dòng trên:

stepOne().then(stepTwo).then(null, handleErrorOne)

Thư viện lời hứa của Angular $qdựa trên Qthư viện của kriskowal (có API phong phú hơn, nhưng chứa mọi thứ bạn có thể tìm thấy $q). Tài liệu API của Q trên GitHub có thể chứng minh hữu ích. Q thực hiện thông số Promise / A + , chi tiết về cách thứcthen và hành vi giải quyết lời hứa hoạt động chính xác.

BIÊN TẬP:

Ngoài ra, hãy nhớ rằng nếu bạn muốn thoát ra khỏi chuỗi trong trình xử lý lỗi của mình, bạn cần phải trả lại lời hứa bị từ chối hoặc ném Lỗi (sẽ tự động bị bắt và gói trong lời hứa bị từ chối). Nếu bạn không trả lại lời hứa, hãy thenkết thúc giá trị trả lại trong lời hứa giải quyết cho bạn.

Điều này có nghĩa là nếu bạn không trả lại bất cứ điều gì, bạn thực sự trả lại một lời hứa đã giải quyết cho giá trị undefined.


138
Phần này là vàng: if you don't return anything, you are effectively returning a resolved promise for the value undefined.Cảm ơn @pluma
Valerio

7
Đây thực sự là. Tôi đang chỉnh sửa nó để mang lại cho nó sự táo bạo xứng đáng
Cyril CHAPON

không từ chối thoát khỏi chức năng hiện tại? ví dụ: giải quyết sẽ không được gọi nếu từ chối được gọi là 1st `if (bad) {từ chối (trạng thái); } giải quyết (kết quả); `
SuperUberDuper

stepOne().then(stepTwo, handleErrorOne) `stepOne (). sau đó (null, xử lýErrorOne) .then (stepTwo)` Đây có phải là tương đương trully? Tôi nghĩ rằng trong trường hợp từ chối trong stepOnedòng mã thứ hai sẽ thực thi stepTwonhưng lần đầu tiên sẽ chỉ thực thi handleErrorOnevà dừng lại. Hay tôi đang thiếu một cái gì đó?
JeFf

5
Không thực sự cung cấp một giải pháp rõ ràng cho câu hỏi được hỏi, tuy nhiên lời giải thích tốt
HERken 18/03/2016

57

Đến bữa tiệc muộn nhưng giải pháp đơn giản này đã có hiệu quả với tôi:

function chainError(err) {
  return Promise.reject(err)
};

stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

Điều này cho phép bạn thoát ra khỏi chuỗi.


1
Đã giúp tôi nhưng FYI, bạn có thể trả lại sau đó để thoát ra khỏi vụ bắt như:.then(user => { if (user) return Promise.reject('The email address already exists.') })
Craig van Tonder

1
@CraigvanTonder bạn chỉ có thể thực hiện lời hứa và nó sẽ hoạt động giống như mã của bạn:.then(user => { if (user) throw 'The email address already exists.' })
Francisco Presencia

1
Đây là câu trả lời đúng duy nhất. Nếu không, bước 3 vẫn sẽ thực hiện ngay cả bước 1 có lỗi.
wdetac

1
Chỉ cần làm rõ, nếu một lỗi xảy ra trong stepOne (), thì cả chuỗiError sẽ được gọi đúng không? Nếu điều này mong muốn. Tôi có một đoạn trích thực hiện điều này, không chắc tôi có hiểu nhầm gì không - runkit.com/embed/9q2q3rjxdar9
user320550

10

Những gì bạn cần là một .then()chuỗi lặp lại với một trường hợp đặc biệt để bắt đầu và một trường hợp đặc biệt để kết thúc.

Bí quyết là để có được số bước của trường hợp thất bại để gợn qua để xử lý lỗi cuối cùng.

  • Bắt đầu: gọi step(1)vô điều kiện.
  • Lặp lại mẫu: chuỗi a .then()với các cuộc gọi lại sau:
    • thành công: bước gọi (n + 1)
    • thất bại: ném giá trị mà người trì hoãn trước đó đã bị từ chối hoặc lấy lại lỗi.
  • Kết thúc: chuỗi a .then()không có trình xử lý thành công và trình xử lý lỗi cuối cùng.

Bạn có thể viết toàn bộ nội dung bằng tay nhưng dễ dàng hơn để chứng minh mẫu bằng các hàm tổng quát có tên:

function nextStep(n) {
    return step(n + 1);
}

function step(n) {
    console.log('step ' + n);
    var deferred = $q.defer();
    (n === 3) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
}

function stepError(n) {
    throw(n);
}

function finalError(n) {
    console.log('finalError ' + n);
}
step(1)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(null, finalError);});

xem bản demo

Lưu ý làm thế nào trong step(), hoãn lại bị từ chối hoặc giải quyết n, do đó làm cho giá trị đó có sẵn cho các cuộc gọi lại trong chuỗi tiếp theo .then(). Khi stepErrorđược gọi, lỗi được lặp lại cho đến khi nó được xử lý finalError.


Câu trả lời đầy thông tin vì vậy nó đáng để giữ, nhưng đó không phải là vấn đề tôi phải đối mặt. Tôi đề cập đến giải pháp này trong bài viết của mình và nó không phải là thứ tôi đang tìm kiếm. Xem bản demo ở đầu bài của tôi.
m59

1
m59, đây là một câu trả lời cho câu hỏi được hỏi, "làm thế nào để tôi viết lời hứa để tôi có thể gọi một chức năng từ chối, mà không gọi tất cả các chức năng trong chuỗi lỗi?" và tiêu đề của câu hỏi, "Phá vỡ chuỗi hứa hẹn và gọi một chức năng dựa trên bước trong chuỗi bị hỏng (bị từ chối)"
Beetroot-Beetroot

Đúng như tôi đã nói, đó là thông tin và tôi thậm chí đã đưa giải pháp này vào bài viết của mình (với ít chi tiết hơn). Cách tiếp cận này nhằm mục đích sửa chữa mọi thứ để chuỗi có thể tiếp tục. Mặc dù nó có thể thực hiện những gì tôi đang tìm kiếm, nó không tự nhiên như cách tiếp cận trong câu trả lời được chấp nhận. Nói cách khác, nếu bạn muốn làm những gì được thể hiện bởi tiêu đề và câu hỏi được hỏi, hãy thực hiện phương pháp tiếp cận của Pluma.
m59

7

Khi từ chối, bạn nên chuyển một lỗi từ chối, sau đó bọc các trình xử lý lỗi bước trong một chức năng kiểm tra xem việc từ chối sẽ được xử lý hay "rút lại" cho đến khi kết thúc chuỗi:

// function mocking steps
function step(i) {
    i++;
    console.log('step', i);
    return q.resolve(i);
}

// function mocking a failing step
function failingStep(i) {
    i++;
    console.log('step '+ i + ' (will fail)');
    var e = new Error('Failed on step ' + i);
    e.step = i;
    return q.reject(e);
}

// error handler
function handleError(e){
    if (error.breakChain) {
        // handleError has already been called on this error
        // (see code bellow)
        log('errorHandler: skip handling');
        return q.reject(error);
    }
    // firs time this error is past to the handler
    console.error('errorHandler: caught error ' + error.message);
    // process the error 
    // ...
    //
    error.breakChain = true;
    return q.reject(error);
}

// run the steps, will fail on step 4
// and not run step 5 and 6
// note that handleError of step 5 will be called
// but since we use that error.breakChain boolean
// no processing will happen and the error will
// continue through the rejection path until done(,)

  step(0) // 1
  .catch(handleError)
  .then(step) // 2
  .catch(handleError)
  .then(step) // 3
  .catch(handleError)
  .then(failingStep)  // 4 fail
  .catch(handleError)
  .then(step) // 5
  .catch(handleError)
  .then(step) // 6
  .catch(handleError)
  .done(function(){
      log('success arguments', arguments);
  }, function (error) {
      log('Done, chain broke at step ' + error.step);
  });

Những gì bạn thấy trên bảng điều khiển:

step 1
step 2
step 3
step 4 (will fail)
errorHandler: caught error 'Failed on step 4'
errorHandler: skip handling
errorHandler: skip handling
Done, chain broke at step 4

Dưới đây là một số mã làm việc https://jsfiddle.net/8hzg5s7m/3/

Nếu bạn có cách xử lý cụ thể cho từng bước, trình bao bọc của bạn có thể giống như:

/*
 * simple wrapper to check if rejection
 * has already been handled
 * @param function real error handler
 */
function createHandler(realHandler) {
    return function(error) {
        if (error.breakChain) {
            return q.reject(error);
        }
        realHandler(error);
        error.breakChain = true;
        return q.reject(error);    
    }
}

sau đó là chuỗi của bạn

step1()
.catch(createHandler(handleError1Fn))
.then(step2)
.catch(createHandler(handleError2Fn))
.then(step3)
.catch(createHandler(handleError3Fn))
.done(function(){
    log('success');
}, function (error) {
    log('Done, chain broke at step ' + error.step);
});

2

Nếu tôi hiểu chính xác, bạn chỉ muốn lỗi cho bước thất bại hiển thị, phải không?

Điều đó sẽ đơn giản như thay đổi trường hợp thất bại của lời hứa đầu tiên về điều này:

step(1).then(function (response) {
    step(2);
}, function (response) {
    stepError(1);
    return response;
}).then( ... )

Bằng cách quay lại $q.reject()trong trường hợp thất bại của bước đầu tiên, bạn sẽ từ chối lời hứa đó, điều này khiến errorCallback được gọi trong lần 2 then(...).


Những gì trên thế giới ... đó chính xác là những gì tôi đã làm! Xem trong bài viết của tôi rằng tôi đã thử điều đó, nhưng chuỗi sẽ khởi động lại và chạy step(2). Bây giờ tôi chỉ thử lại lần nữa, nó không xảy ra. Tôi thấy bối rối.
m59

1
Tôi đã thấy rằng bạn đề cập đến điều đó. Điều đó thật kỳ quái. Hàm đó return step(2);chỉ nên được gọi khi step(1)giải quyết thành công.
Zajn

Cào đó - nó chắc chắn đang xảy ra. Giống như tôi đã nói trong bài viết của mình, nếu bạn không sử dụng return $q.reject(), chuỗi sẽ tiếp tục. Trong trường hợp này return responselàm rối tung nó lên. Xem điều này: jsbin.com/EpaZIsIp/6/edit
m59

Hừm, được thôi. Nó dường như hoạt động trong jsbin mà bạn đã đăng khi tôi thay đổi điều đó, nhưng tôi chắc chắn đã bỏ lỡ điều gì đó.
Zajn

Vâng tôi chắc chắn thấy rằng không làm việc bây giờ. Quay lại bảng vẽ cho tôi!
Zajn

2
var s = 1;
start()
.then(function(){
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/20/edit

Hoặc tự động cho bất kỳ số bước:

var promise = start();
var s = 1;
var l = 3;
while(l--) {
    promise = promise.then(function() {
        return step(s++);
    });
}
promise.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/21/edit


Nhưng nếu tôi sẽ gọi deferred.reject(n)thì tôi nhận được cảnh báo rằng lời hứa đã bị từ chối với một đối tượng không phải là người nước ngoài
9 giờ sáng

2

Hãy thử ro sử dụng như libs:

https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
    if (!data.someCheck()) {
        tellSomeone();

        // All other '.then' calls will be skiped
        return pb.BREAK;
    }
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
    console.error(error);
});

2

Nếu bạn muốn giải quyết vấn đề này bằng cách sử dụng async / await:

(async function(){    
    try {        
        const response1, response2, response3
        response1 = await promise1()

        if(response1){
            response2 = await promise2()
        }
        if(response2){
            response3 = await promise3()
        }
        return [response1, response2, response3]
    } catch (error) {
        return []
    }

})()

1

Đính kèm các trình xử lý lỗi dưới dạng các thành phần chuỗi riêng biệt trực tiếp để thực hiện các bước:

        // Handle errors for step(1)
step(1).then(null, function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).then(null, function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).then(null, function() { stepError(3); return $q.reject(); });
});

hoặc sử dụng catch():

       // Handle errors for step(1)
step(1).catch(function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).catch(function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).catch(function() { stepError(3); return $q.reject(); });
});

Lưu ý: Về cơ bản, đây là mô hình tương tự như pluma gợi ý trong câu trả lời của anh ấy nhưng sử dụng cách đặt tên của OP.


1

Đã tìm thấy các Promise.prototype.catch()ví dụ trên MDN dưới đây rất hữu ích.

(Câu trả lời được chấp nhận đề cập then(null, onErrorHandler)về cơ bản giống như catch(onErrorHandler).)

Sử dụng và xâu chuỗi phương thức bắt

var p1 = new Promise(function(resolve, reject) {
  resolve('Success');
});

p1.then(function(value) {
  console.log(value); // "Success!"
  throw 'oh, no!';
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

// The following behaves the same as above
p1.then(function(value) {
  console.log(value); // "Success!"
  return Promise.reject('oh, no!');
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

Gotchas khi ném lỗi

// Throwing an error will call the catch method most of the time
var p1 = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

p1.catch(function(e) {
  console.log(e); // "Uh-oh!"
});

// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw 'Uncaught Exception!';
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // This is never called
});

// Errors thrown after resolve is called will be silenced
var p3 = new Promise(function(resolve, reject) {
  resolve();
  throw 'Silenced Exception!';
});

p3.catch(function(e) {
   console.log(e); // This is never called
});

Nếu nó được giải quyết

//Create a promise which would not call onReject
var p1 = Promise.resolve("calling next");

var p2 = p1.catch(function (reason) {
    //This is never called
    console.log("catch p1!");
    console.log(reason);
});

p2.then(function (value) {
    console.log("next promise's onFulfilled"); /* next promise's onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise's onRejected");
    console.log(reason);
});

1

Giải pháp tốt nhất là tái cấu trúc chuỗi hứa hẹn của bạn để sử dụng ES6 đang chờ. Sau đó, bạn có thể chỉ cần quay lại từ hàm để bỏ qua phần còn lại của hành vi.

Tôi đã đánh đầu vào mô hình này trong hơn một năm và sử dụng await là thiên đường.


Khi sử dụng IE async / await thuần túy không được hỗ trợ.
ndee

0

Sử dụng mô-đun SequentialPromise

Ý định

Cung cấp một mô-đun có trách nhiệm thực hiện các yêu cầu một cách tuần tự, trong khi theo dõi chỉ số hiện tại của từng hoạt động theo cách thứ tự. Xác định hoạt động trong một Mẫu lệnh để linh hoạt.

Những người tham gia

  • Bối cảnh : Đối tượng có phương thức thành viên thực hiện một thao tác.
  • SequentialPromise : Xác định mộtexecute phương thức để xâu chuỗi & theo dõi từng thao tác. SequentialPromise trả về Chuỗi Promise từ tất cả các hoạt động được thực hiện.
  • Invoker : Tạo một cá thể SequentialPromise, cung cấp cho nó bối cảnh & hành động và gọi executephương thức của nó trong khi chuyển qua một danh sách tùy chọn thứ tự cho mỗi thao tác.

Kết quả

Sử dụng SequentialPromise khi cần có hành vi thứ tự của độ phân giải Promise. SequentialPromise sẽ theo dõi chỉ số mà Promise bị từ chối.

Thực hiện

clear();

var http = {
    get(url) {
        var delay = Math.floor( Math.random() * 10 ), even = !(delay % 2);
        var xhr = new Promise(exe);

        console.log(`REQUEST`, url, delay);
        xhr.then( (data) => console.log(`SUCCESS: `, data) ).catch( (data) => console.log(`FAILURE: `, data) );

        function exe(resolve, reject) {
            var action = { 'true': reject, 'false': resolve }[ even ];
            setTimeout( () => action({ url, delay }), (1000 * delay) );
        }

        return xhr;
    }
};

var SequentialPromise = new (function SequentialPromise() {
    var PRIVATE = this;

    return class SequentialPromise {

        constructor(context, action) {
            this.index = 0;
            this.requests = [ ];
            this.context = context;
            this.action = action;

            return this;
        }

        log() {}

        execute(url, ...more) {
            var { context, action, requests } = this;
            var chain = context[action](url);

            requests.push(chain);
            chain.then( (data) => this.index += 1 );

            if (more.length) return chain.then( () => this.execute(...more) );
            return chain;
        }

    };
})();

var sequence = new SequentialPromise(http, 'get');
var urls = [
    'url/name/space/0',
    'url/name/space/1',
    'url/name/space/2',
    'url/name/space/3',
    'url/name/space/4',
    'url/name/space/5',
    'url/name/space/6',
    'url/name/space/7',
    'url/name/space/8',
    'url/name/space/9'
];
var chain = sequence.execute(...urls);
var promises = sequence.requests;

chain.catch( () => console.warn(`EXECUTION STOPPED at ${sequence.index} for ${urls[sequence.index]}`) );

// console.log('>', chain, promises);

Ý chính

SequentialPromise


0

Nếu tại bất kỳ thời điểm nào bạn quay lại, Promise.reject('something')bạn sẽ bị ném vào khối bắt theo lời hứa.

promiseOne
  .then((result) => {
    if (!result) {
      return Promise.reject('No result');
    }
    return;
  })
  .catch((err) => {
    console.log(err);
  });

Nếu lời hứa đầu tiên không trả lại bất kỳ kết quả nào, bạn sẽ chỉ nhận được 'Không có kết quả' trong bảng điều khiển.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.