Speed up your JavaScrip 3[转]

优化JS递归
本文探讨了JavaScript中递归带来的性能问题及解决方案,包括使用memoization技术优化递归函数,以及如何将递归转换为迭代,以提高归并排序等算法的效率。

影响JavaScript性能的另外一个杀手就是递归,在上一节中提到采用memoization技术可以优化计算数值的递归函数,但 memoization不是万能的,不是所有的递归函数都可以用memoization技术优化,本文介绍了这些情况,并介绍了解决办法,就是将递归转换 为迭代,同时需要注意,本文末尾介绍的方案不是最终的方案,还需要和上一节优化循环的方案综合起来才能达到最佳效果。

【原文】Speed up your JavaScript, Part 3
【作者】Nicholas C. Zakas
【译文】http://cuimingda.com/2009/02/speed-up-your-javascript-part-3.html
【译者】明达

以下是对原文的翻译

递归是拖慢脚本运行速度的大敌之一。太多的递归会让浏览器变得越来越慢直到死掉或者莫名其妙的突然自动退出,所以我们一定要解决在 JavaScript中出现的这一系列性能问题。在这个系列文章的第二篇中,我曾经简短的介绍了如何通过memoization技术来替代函数中太多的递 归调用。memoization是一种可以缓存之前运算结果的技术,这样我们就不需要重新计算那些已经计算过的结果。对于通过递归来进行计算的函 数,memoization简直是太有用了。我现在使用的memoizer是由 Crockford写的,主要应用在那些返回整数的递归运算中。当然并不是所有的递归函数都返回整数,所以我们需要一个更加通用的memoizer()函 数来处理更多类型的递归函数。

 

Java代码
  1. function memoizer(fundamental, cache) {  
  2.   cache = cache || {};  
  3.   var shell = function(arg) {  
  4.       if  (! (arg in cache)) {  
  5.           cache[arg] = fundamental(shell, arg);  
  6.       }  
  7.       return  cache[arg];  
  8.   };  
  9.   return  shell;}  
function memoizer(fundamental, cache) {
  cache = cache || {};
  var shell = function(arg) {
      if (! (arg in cache)) {
          cache[arg] = fundamental(shell, arg);
      }
      return cache[arg];
  };
  return shell;}

 

这个版本的函数和Crockford写的版本有一点点不同。首先,参数的顺序被颠倒了,原有函数被设置为第一个参数,第二个参数是缓存对象,为可选 参数,因为并不是所有的递归函数都包含初始信息。在函数内部,我将缓存对象的类型从数组转换为对象,这样这个版本就可以适应那些不是返回整数的递归函数。 在shell函数里,我使用了in操作符来判断参数是否已经包含在缓存里。这种写法比测试类型不是undefined更加安全,因为undefined是 一个有效的返回值。我们还是用之前提到的斐波纳契数列来做说明:

Java代码
  1. var fibonacci = memoizer(function(recur, n) {  
  2.   return  recur(n -  1 ) + recur(n -  2 );  
  3. }, { "0"0"1"1 } );  
var fibonacci = memoizer(function(recur, n) {
  return recur(n - 1) + recur(n - 2);
}, { "0": 0, "1": 1} );

 

 

同样的,执行fibonacci(40)这个函数,只会对原有的函数调用40次,而不是夸张的331,160,280次。memoization对 于那些有着严格定义的结果集的递归算法来说,简直是棒极了。然而,确实还有很多递归算法不适合使用memoization方法来进行优化。

我在学校时的一位教授一直坚持认为,任何使用递归的情况,如果有需要,都可以使用迭代来代替。实际上,递归和迭代经常会被作为互相弥补的方法,尤其 是在另外一种出问题的情况下。将递归算法转换为迭代算法的技术,也是和开发语言无关的。这对JavaScript来说是很重要的,因为很多东西在执行环境 中是受到限制的(the importance in JavaScript is greater, though, because the resources of the execution environment are so restrictive.)。让我们回顾一个典型的递归算法,比如说归并排序,在JavaScript中实现这个算法需要下面的代码:

 

Java代码
  1. function merge(left, right) {  
  2.   var result = [];  
  3.   while  (left.length >  0  && right.length >  0 ) {  
  4.       if  (left[ 0 ] < right[ 0 ]) {  
  5.           result.push(left.shift());  
  6.       } else  {  
  7.           result.push(right.shift());  
  8.       }  
  9.   }  
  10.   return  result.concat(left).concat(right);  
  11. }  
  12.   
  13. //采用递归实现的归并排序算法   
  14. function mergeSort(items) {  
  15.   if  (items.length ==  1 ) {  
  16.       return  items;  
  17.   }  
  18.   var middle = Math.floor(items.length / 2 ),  
  19.   left = items.slice(0 , middle),  
  20.   right = items.slice(middle);  
  21.   return  merge(mergeSort(left), mergeSort(right));  
  22. }  
function merge(left, right) {
  var result = [];
  while (left.length > 0 && right.length > 0) {
      if (left[0] < right[0]) {
          result.push(left.shift());
      } else {
          result.push(right.shift());
      }
  }
  return result.concat(left).concat(right);
}

//采用递归实现的归并排序算法
function mergeSort(items) {
  if (items.length == 1) {
      return items;
  }
  var middle = Math.floor(items.length / 2),
  left = items.slice(0, middle),
  right = items.slice(middle);
  return merge(mergeSort(left), mergeSort(right));
}

 

调用mergeSort()函数处理一个数组,就可以返回经过排序的数组。注意每次调用mergeSort()函数,都会有两次递归调用。这个算法 不可以使用memoization来进行优化,因为每个结果都只计算并使用一次,就算缓冲了结果也没有什么用。如果你使用mergeSort()函数来处 理一个包含100个元素的数组,总共会有199次调用。1000个元素的数组将会执行1999次调用。在这种情况下,我们的解决方案是将递归算法转换为迭 代算法,也就是说要引入一些循环(关于算法,可以参考这篇《List Processing: Sort Again, Naturally 》):

 

Java代码
  1. // 采用迭代实现的归并排序算法   
  2. function mergeSort(items) {  
  3.   if  (items.length ==  1 ) {  
  4.       return  items;  
  5.   }  
  6.   var work = [];  
  7.   for  (var i =  0 ,  
  8.   len = items.length; i < len; i++) {  
  9.       work.push([items[i]]);  
  10.   }  
  11.   work.push([]); //in case of odd number of items   
  12.   for  (var lim = len; lim >  1 ; lim = (lim +  1 ) /  2 ) {  
  13.       for  (var j =  0 ,  
  14.       k = 0 ; k < lim; j++, k +=  2 ) {  
  15.           work[j] = merge(work[k], work[k + 1 ]);  
  16.       }  
  17.       work[j] = []; //in case of odd number of items   
  18.   }  
  19.   return  work[ 0 ];  
  20. }  
// 采用迭代实现的归并排序算法
function mergeSort(items) {
  if (items.length == 1) {
      return items;
  }
  var work = [];
  for (var i = 0,
  len = items.length; i < len; i++) {
      work.push([items[i]]);
  }
  work.push([]); //in case of odd number of items
  for (var lim = len; lim > 1; lim = (lim + 1) / 2) {
      for (var j = 0,
      k = 0; k < lim; j++, k += 2) {
          work[j] = merge(work[k], work[k + 1]);
      }
      work[j] = []; //in case of odd number of items
  }
  return work[0];
}

 

这个归并排序算法实现使用了一系列循环来代替递归进行排序。由于归并排序首先要将数组拆分成若干只有一个元素的数组,这个方法更加明确的执行了这个 操作,而不是通过递归函数隐晦的完成。work数组被初始化为包含一堆只有一个元素数组的数组。在循环中每次会合并两个数组,并将合并后的结果放回 work数组中。当函数执行完成后,排序的结果会通过work数组中的第一个元素返回。在这个归并排序的实现中,没有使用任何递归,同样也实现了这个算 法。然而,这样做却引入了大量的循环,循环的次数基于要排序的数组中元素的个数,所以我们可能需要使用在 上篇讨论过的技术 来进行修订,处理这些额外开销。

总结一下基本原则,不管是什么时候使用递归的时候都应该小心谨慎。memoization和迭代是代替递归的两种解决方案,最直接的结果当然就是避免那个 提示脚本失控的对话框

 

 

### 使用键盘控制3D模型上下旋 #### Three.js 实现方案 为了实现在Three.js中通过键盘控制3D模型的上下旋功能,可以利用`THREE.Raycaster`和事件监听器来捕捉用户的按键输入并相应地调整对象的角度。 下面是一个具体的实现方式: 定义全局变量用于存储当前角度以及目标物体: ```javascript let rotationSpeed = Math.PI / 180; // 定义每次旋的速度 let object; ``` 加载GLTF模型之后设置好要操作的对象: ```javascript function loadModel() { const loader = new THREE.GLTFLoader(); loader.load(&#39;path/to/your/model.gltf&#39;, function (gltf) { object = gltf.scene; scene.add(object); animate(); // 开始动画循环 }); } ``` 添加键盘事件处理程序以响应用户按下方向键时的动作[^2]: ```javascript document.addEventListener(&#39;keydown&#39;, onDocumentKeyDown, false); function onDocumentKeyDown(event){ switch(event.key){ case &#39;ArrowUp&#39;: // 上箭头增加Y轴正向旋 object.rotation.x += rotationSpeed; break; case &#39;ArrowDown&#39;: // 下箭头减少Y轴负向旋 object.rotation.x -= rotationSpeed; break; default: console.log(`未识别的按键 ${event.key}`); } } ``` 此代码片段展示了如何捕获向上和向下箭头键,并据此修改所选3D模型绕X轴的旋角度。每当检测到相应的按键被按下时,就会更新该对象的姿态属性(rotation),从而达到视觉上的旋效果。 对于更复杂的交互逻辑,还可以考虑引入额外的状态管理工具或框架(如React),以便更好地管理和同步UI状态与三维场景之间的关系。 #### Unity 实现方案 在Unity环境中,可以通过编写C#脚本来实现类似的键盘控制机制。这里提供了一个简单的例子说明怎样让游戏对象沿指定轴线根据玩家输入而动。 创建一个新的MonoBehaviour类命名为`RotateObject.cs`并将它附加给想要操控的游戏物件上: ```csharp using UnityEngine; public class RotateObject : MonoBehaviour { public float speed = 90.0f; // 设置每秒过的度数 void Update () { if(Input.GetKey(KeyCode.UpArrow)){ transform.Rotate(Vector3.right * Time.deltaTime * speed); // 绕右侧(X轴)顺时针旋 } if(Input.GetKey(KeyCode.DownArrow)){ transform.Rotate(-Vector3.right * Time.deltaTime * speed); // 绕左侧逆时针旋 } } } ``` 上述脚本会在每一帧调用Update方法检查是否有对应的按键处于按下的状态;如果有,则按照设定好的速度使物体围绕其局部坐标系内的X轴发生自旋运动[^1]。 无论是采用Three.js还是Unity平台开发的应用程序,都可以借助这些基本原理去构建更加丰富的互动体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值