Я видел вопрос о порядке выполнения Promise в Интернете раньше - напечатать вывод следующей программы:
new Promise(resolve => {
console.log(1);
resolve(3);
}).then(num => {
console.log(num)
});
console.log(2)
Результат этого вопроса 123, почему не 132? Потому что я всегда понимаю, что у Promise нет асинхронной функции, она просто помогает решить проблему асинхронного обратного вызова, который по сути такой же, как и обратный вызов, поэтому, если следовать этой идее, он должен быть сразу после разрешения. Но это не так. Вы использовали setTimeout?
Если вы добавите еще одно обещание к обещанию:
new Promise(resolve => {
console.log(1);
resolve(3);
Promise.resolve().then(()=> console.log(4))
}).then(num => {
console.log(num)
});
console.log(2)
Порядок выполнения 1243, а порядок второго промиса будет раньше первого, так что интуитивно это довольно странно, почему так?
Существует множество библиотек для реализации Promise, в том числе jQuery deferred, и многие из них предоставляют полифиллы, такие какes6-promise,lieд., их реализации основаны наPromise/A+Standard, который также принят ES6 Promise.
Для того, чтобы ответить на вопрос о порядке выполнения темы выше, вы должны понимать, как реализованы промисы, так что это зависит от того, как реализованы эти библиотеки, особенно, как реализована асинхронность промисов, которую я ошибочно считаю несуществующей, потому что консоль в последней строке.
Конечно, мы пытаемся не просто ответить на вопрос, а в основном понять внутренний механизм Promise. Читатели могут проанализировать ее самостоятельно, если у них есть время и интерес, а затем вернуться и сравнить анализ в этой статье. Или вы можете следовать приведенным ниже идеям и использовать мышь и клавиатуру для работы со мной.
Здесь используется библиотека lie.По сравнению с es6-promise код проще для понимания.Сначала npm устанавливаем его:
npm install lie
Пусть код работает на стороне браузера, подготовьте следующий html:
<!DOCType html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script src="node_modules/lie/dist/lie.js"></script>
<script src="index.js"></script>
</body>
</html>
Содержимое index.js:
console.log(Promise);
new Promise(resolve => {
console.log(1);
resolve(3);
Promise.resolve().then(()=> console.log(4))
}).then(num => {
console.log(num)
});
console.log(2);
Распечатайте обещание и подтвердите, что исходное было перезаписано.Сравнение выглядит следующим образом:
Поскольку мы не можем сломать нативный Promise, нам нужно использовать стороннюю библиотеку.
Давайте поставим точку останова в resolve(3) в строке 4, чтобы увидеть, как выполняется resolve, переходим слой за слоем, и окончательная функция будет такой:
Мы обнаружили, что эта функция, по-видимому, ничего не делает, она просто устанавливает состояние себя в FULFILLED (завершено) и устанавливает результат в значение, переданное из разрешения, здесь 3, если разрешение является обещанием. Он войдет в обработку цепочки промисов в строке 187 на приведенном выше рисунке, и мы не будем здесь рассматривать эту ситуацию. Здесь self ссылается на объект Promise:
У него в основном есть 3 атрибута: результат, очередь, состояние, из которых результат — это результат, переданный с помощью разрешения, состояние — это состояние промиса, в коде в строке 83 вы можете найти всего 3 состояния промиса:
var REJECTED = ['REJECTED'];
var FULFILLED = ['FULFILLED'];
var PENDING = ['PENDING'];
Rejected терпит неудачу, выполнено успешно, и pending все еще обрабатывается Как вы можете видеть в конструкторе Promise в строке 89, инициализированное состояние — это pending:
function Promise(resolver) {
if (typeof resolver !== 'function') {
throw new TypeError('resolver must be a function');
}
this.state = PENDING;
this.queue = [];
this.outcome = void 0;
if (resolver !== INTERNAL) {
safelyResolveThenable(this, resolver);
}
}
И как вы можете видеть в стеке вызовов справа, резолвер запускается конструктором промиса, то есть при создании нового промиса будет выполняться функция передачи параметров, как показано на следующем рисунке:
Передаваемая функция поддерживает два параметра, разрешает и отклоняет обратные вызовы:
let resolver = function(resolve, reject) {
if (success) resolve();
else reject();
};
new Promise(resolver);
Эти две функции определены внутри Promise, но вам нужно вызвать эту функцию в своей функции, чтобы сообщить ей, когда она удалась, а когда нет, чтобы она могла перейти к следующему шагу. Таким образом, эти два параметра функции передаются, они являются функциями обратного вызова Promise. Как Promise определяет и передает эти две функции? Сейчас он все еще находится в позиции точки останова, но давайте изменим позицию, отображаемую в стеке вызовов справа:
Функция thenable, выполняемая на приведенном выше рисунке, — это распознаватель, который мы передали ей, а затем передали onSuccess и onError, которые являются параметрами разрешения и отклонения, которые мы прописали в распознавателе. Если мы вызовем его функцию resolve или onSuccess, она вызовет handlers.resolve в строке 236, чтобы перейти к картинке, где мы впервые сломали точку, вот она снова:
Затем перейдите к установке состояния, результата и других свойств текущего объекта Promise. В строке 193 нет входа в цикл while, потому что очередь пуста. Это место еще будет упомянуто ниже.
Затем давайте сделаем точку останова и посмотрим:
Тогда что ты сделал? Как показано ниже:
Затем можно передать два параметра, а именно обратный вызов успеха и обратный вызов отказа. Мы передаем ему успешный обратный вызов, где линия нарисована выше. А так как в резолвере было установлено состояние выполнено, то он выполнит функцию unwrap, и передаст успешный обратный вызов и результат резолва в исход (также есть обещание параметра, которое в основном используется для возврата , образуя тогда цепочку).
Функция unwrap реализована так:
В строке 167 выполните обратный вызов успеха, переданный промису в then, и передайте результат результата.
Этот код заключен в немедленную функцию, которая является ключом к решению асинхронной проблемы Promise. И в каталоге node_modules мы также обнаружили, что здесь используетсяближайшая библиотека, он может достичьnextTickСинхронно выполняется функция , то есть она выполняется сразу после текущей логической единицы кода, что эквивалентно setTimeout 0, но не реализуется напрямую с setTimeout 0.
Давайте сосредоточимся на том, как он реализует функцию nextTick. График слива будет корректироваться в непосредственной (слив означает дренаж):
function immediate(task) {
// 这个判断先忽略
if (queue.push(task) === 1 && !draining) {
scheduleDrain();
}
}
Логика реализации находится в этом scheduleDrain, который реализован так:
var Mutation = global.MutationObserver || global.WebKitMutationObserver;
var scheduleDrain = null;
{
// 浏览器环境,IE11以上支持
if (Mutation) {
// ...
}
// Node.js环境
else if (!global.setImmediate && typeof global.MessageChannel !== 'undefined')
}
// 低浏览器版本解决方案
else if ('document' in global && 'onreadystatechange' in global.document.createElement('script')) {
}
// 最后实在没办法了,用最次的setTimeout
else {
scheduleDrain = function () {
setTimeout(nextTick, 0);
};
}
}
Он будет иметь оценку совместимости, сначала используя MutationObserver, а затем используя тег script, который поддерживается IE6, и, наконец, используйте setTimeout 0, если ничего не работает.
Мы в основном смотрим на то, как реализуется метод Mutation.MDNСуществует введение в использование этого MutationObserver, который можно использовать для отслеживания изменений в узлах DOM, таких как добавления и удаления, изменения атрибутов и т. д. Немедленное реализовано так:
if (Mutation) {
var called = 0;
var observer = new Mutation(nextTick);
var element = global.document.createTextNode('');
// 监听节点的data属性的变化
observer.observe(element, {
characterData: true
});
scheduleDrain = function () {
// 让data属性发生变化,在0/1之间不断切换,
// 进而触发observer执行nextTick函数
element.data = (called = ++called % 2);
};
}
Используйте обратный вызов nextTick для регистрации наблюдателя-наблюдателя, затем создайте элемент узла DOM, станьте объектом-наблюдателем наблюдателя и наблюдайте за его атрибутом данных. Когда необходимо выполнить функцию nextTick, scheduleDrain настраивается для изменения атрибута данных, что инициирует обратный вызов nextTick наблюдателя. Он выполняется асинхронно, сразу после выполнения текущего блока кода, но до setTimeout 0, то есть следующий код, 5 первой строки — последний вывод:
setTimeout(()=> console.log(5), 0);
new Promise(resolve => {
console.log(1);
resolve(3);
// Promise.resolve().then(()=> console.log(4))
}).then(num => {
console.log(num)
});
console.log(2);
На данный момент мы можем ответить, почему порядок вывода приведенного выше кода равен 123 вместо 132. Первая точка уверена, что сначала выводится 1, потому что после нового промиса переданный ему преобразователь выполняется синхронно, поэтому 1 печатается первым. После выполнения resolve(3) состояние текущего объекта Promiser будет изменено на завершенное состояние, а результат результата будет записан. Затем выпрыгните, чтобы выполнить then, и верните успешный вызов, переданный then, для немедленного выполнения в nextTick, а nextTick выполняется асинхронно с использованием Mutation, поэтому 3 будет выведено после 2.
Если вы напишете еще один промис внутри промиса, так как then внутреннего промиса выполняется до then внешнего промиса, то есть его nextTick регистрируется первым, поэтому 4 выводится раньше 3.
Это в основном объясняет порядок выполнения промисов. Но мы не сказали, как реализован его nextTick.Приведенный выше код помещает успешный обратный вызов в глобальную очередь массива при выполнении немедленного, а nextTick выполняет эти обратные вызовы по порядку, как показано в следующем коде:
function nextTick() {
draining = true;
var i, oldQueue;
var len = queue.length;
while (len) {
oldQueue = queue;
// 把queue清空
queue = [];
i = -1;
// 执行当前所有回调
while (++i < len) {
oldQueue[i]();
}
len = queue.length;
}
draining = false;
}
Сначала он установит для переменной дренажа дренаж значение true, а затем установит для нее значение false после завершения обработки Давайте рассмотрим только что выполненное суждение немедленно:
function immediate(task) {
if (queue.push(task) === 1 && !draining) {
scheduleDrain();
}
}
Поскольку JS является однопоточным, я не думаю, что это сужение переменных необходимо. Другое суждение заключается в том, что когда очередь пуста, в нее вставляется переменная. В это время в очереди есть только 1 элемент, и возвращаемое значение равно 1. Таким образом, если он был отправлен ранее, то нет необходимости запускать здесь nextTick, потому что при первом нажатии будут выполнены все элементы обратного вызова очереди, пока последующие операции помещаются в эту очередь. Таким образом, это суждение является оптимизацией.
Кроме того, основной код es6-promise такой же, за исключением того, что он изменяет немедленную функцию на asap (как можно скорее), а также преимущественно использует мутацию.
Есть еще одна проблема.Упомянутый выше код резольвера является синхронным, но мы часто используем Promise в асинхронных ситуациях.Резолв вызывается асинхронно, а не синхронно как выше, например:
let resolver = function(resolve) {
setTimeout(() => {
// 异步调用resolve
resolve();
}, 2000);
// resolver执行完了还没执行resolve
};
new Promise(resolver).then(num => console.log(num));
В это время преобразователь выполняется синхронно, но преобразователь еще не выполнен, поэтому, когда then выполняется, состояние промиса все еще находится в ожидании, и он перейдет к коду 134 (развертка строки 132 был только что выполнен):
Он создаст QueueItem и поместит его в свойство очереди текущего объекта Promise (обратите внимание, что очередь здесь и глобальная очередь в непосредственно упомянутом выше — это две разные переменные). Затем асинхронное выполнение завершается и вызывается resolve В это время очередь не пуста:
Обратный вызов успеха в очереди будет выполнен. Поскольку then может повторяться несколько раз, может быть несколько успешных обратных вызовов. Он также вызывает немедленное выполнение, которое выполняется в nextTick.
То есть, еслисинхронное разрешениеДа, обратный вызов успеха выполняется сразу после выполнения текущей единицы кода с помощью MutationObserver/Setimeout 0; и если этоАсинхронное разрешениеДа, сначала нужно поместить успешный обратный вызов в очередь текущего объекта Promise, а затем использовать тот же метод для вызова обратного вызова успеха в nextTick, когда выполнение resolve завершается асинхронно.
Мы не говорили о неудачных обратных вызовах, но в целом это похоже.