Старый вопрос 2009 года. Теперь в 2015 году возможно новое решение с генераторами, определенными в ECMAscript 2015 aka ES6. Он был одобрен в июне, но ранее он был реализован в Firefox и Chrome Теперь функция сна может быть сделана не занятой, неблокируемой и вложенной в циклы и подфункции без зависания браузера. Требуется только чистый JavaScript, без библиотек и фреймворков.
В программе ниже показано, как можно сделать sleep()
и runSleepyTask()
. Функция sleep()
является только оператором yield
. Это так просто, что на самом деле проще написать оператор yield
непосредственно вместо вызова sleep()
, но тогда не будет сна-слова :-) yield возвращает значение времени методу next()
внутри wakeup()
и ждет. Фактический «сон» выполняется в wakeup()
с использованием старого доброго setTimeout()
. При обратном вызове метод next()
вызывает выполнение оператора yield
для продолжения, и «волшебство» yield заключается в том, что все локальные переменные и весь стек вызовов вокруг него все еще не повреждены.
Функции, которые используют sleep () или yield, должны быть определены как генераторы. Это легко сделать, добавив звездочку к ключевому слову function*
. Выполнить генератор немного сложнее. При вызове с ключевым словом new
генератор возвращает объект, имеющий метод next()
, но тело генератора не выполняется (ключевое слово new
является необязательным и не имеет значения). Метод next()
запускает выполнение тела генератора, пока не встретит yield
. Функция обёртки runSleepyTask()
запускает пинг-понг: next()
ожидает yield
, а yield
ожидает next()
.
Еще один способ вызвать генератор - с ключевым словом yield*
, здесь он работает как простой вызов функции, но также включает возможность возврата к next()
.
Это все продемонстрировано на примере drawTree()
. Он рисует дерево с листьями на вращающейся трехмерной сцене. Дерево нарисовано в виде ствола с 3-мя частями вверху в разных направлениях. Каждая часть затем рисуется как другое, но меньшее дерево, вызывая drawTree()
рекурсивно после короткого сна. Очень маленькое дерево нарисовано только как лист.
У каждого листа своя жизнь в отдельной задаче, начинающейся с runSleepyTask()
. Он рождается, растет, сидит, исчезает, падает и умирает в growLeaf()
. Скорость контролируется с помощью sleep()
. Это демонстрирует, как легко сделать многозадачность.
function* sleep(milliseconds) {yield milliseconds};
function runSleepyTask(task) {
(function wakeup() {
var result = task.next();
if (!result.done) setTimeout(wakeup, result.value);
})()
}
//////////////// written by Ole Middelboe /////////////////////////////
pen3D =setup3D();
var taskObject = new drawTree(pen3D.center, 5);
runSleepyTask(taskObject);
function* drawTree(root3D, size) {
if (size < 2) runSleepyTask(new growLeaf(root3D))
else {
pen3D.drawTrunk(root3D, size);
for (var p of [1, 3, 5]) {
var part3D = new pen3D.Thing;
root3D.add(part3D);
part3D.move(size).turn(p).tilt(1-p/20);
yield* sleep(50);
yield* drawTree(part3D, (0.7+p/40)*size);
}
}
}
function* growLeaf(stem3D) {
var leaf3D = pen3D.drawLeaf(stem3D);
for (var s=0;s++<15;) {yield* sleep(100); leaf3D.scale.multiplyScalar(1.1)}
yield* sleep( 1000 + 9000*Math.random() );
for (var c=0;c++<30;) {yield* sleep(200); leaf3D.skin.color.setRGB(c/30, 1-c/40, 0)}
for (var m=0;m++<90;) {yield* sleep( 50); leaf3D.turn(0.4).tilt(0.3).move(2)}
leaf3D.visible = false;
}
///////////////////////////////////////////////////////////////////////
function setup3D() {
var scene, camera, renderer, diretionalLight, pen3D;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(75,
window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 15, 20);
renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
directionalLight = new THREE.DirectionalLight(0xffffaa, 0.7);
directionalLight.position.set(-1, 2, 1);
scene.add(directionalLight);
scene.add(new THREE.AmbientLight(0x9999ff));
(function render() {
requestAnimationFrame(render);
// renderer.setSize( window.innerWidth, window.innerHeight );
scene.rotateY(10/60/60);
renderer.render(scene, camera);
})();
window.addEventListener(
'resize',
function(){
renderer.setSize( window.innerWidth, window.innerHeight );
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
},
false
);
pen3D = {
drawTrunk: function(root, size) {
// root.skin = skin(0.5, 0.3, 0.2);
root.add(new THREE.Mesh(new THREE.CylinderGeometry(size/12, size/10, size, 16),
root.skin).translateY(size/2));
root.add(new THREE.Mesh(new THREE.SphereGeometry(size/12, 16),
root.skin).translateY(size));
return root;
},
drawLeaf: function(stem) {
stem.skin.color.setRGB(0, 1, 0);
stem.add(new THREE.Mesh(new THREE.CylinderGeometry(0, 0.02, 0.6),
stem.skin) .rotateX(0.3).translateY(0.3));
stem.add(new THREE.Mesh(new THREE.CircleGeometry(0.2),
stem.skin) .rotateX(0.3).translateY(0.4));
return stem;
},
Thing: function() {
THREE.Object3D.call(this);
this.skin = new THREE.MeshLambertMaterial({
color: new THREE.Color(0.5, 0.3, 0.2),
vertexColors: THREE.FaceColors,
side: THREE.DoubleSide
})
}
};
pen3D.Thing.prototype = Object.create(THREE.Object3D.prototype);
pen3D.Thing.prototype.tilt = pen3D.Thing.prototype.rotateX;
pen3D.Thing.prototype.turn = pen3D.Thing.prototype.rotateY;
pen3D.Thing.prototype.move = pen3D.Thing.prototype.translateY;
pen3D.center = new pen3D.Thing;
scene.add(pen3D.center);
return pen3D;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r71/three.min.js"></script>
3D-содержимое скрыто внутри setup3D () и включено только для того, чтобы сделать его менее скучным, чем console.log (). Кстати, ангелы измеряются в радианах.
Проверено на работу в Firefox и Chrome. Не реализовано в Internet Explore и iOS (iPad). Попробуйте запустить его самостоятельно.
После очередного прохода ответов я обнаружил, что Габриэль Ратенер сделал аналогичный ответ год назад: https://stackoverflow.com/a/24401317/5032384