KnockoutJS数组比较算法实例详解


Posted in Javascript onNovember 25, 2019

这篇文章主要介绍了KnockoutJS数组比较算法实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

前端开发中,数组是一种非常有用的数据结构。这篇博客会解释并分析KnockoutJS实现中使用的数据比较算法。

算法的目的

KnockoutJS使用MVVM的思想,view model中的数组元素会对应data model中的数组数据,当用户进行输入或者请求后台时,数组数据会发生变更, 从而带动UI的更新。例如购物车类的页面,用户可以通过点击一些按钮来添加/删除购物车中储存的物品。一个显示购物车中商品详情的列表会根据数组中物品元素的变化实时更新。

另一个例子可以是一个展示餐馆等候队列的展示页面,随着客人加入/退出队列,服务器端会不断推送数据到前端页面,实时更新当前最新的队列情况。因此我们需要一个算法,根据更新前的数组数据和更新后的数组数据,计算出一个DOM操作序列,从而使得绑定的DOM元素能根据data model里的数据变化自动更新DOM里的元素。

经典Edit Distance问题

开始正式分析之前,我们先回顾一个经典的算法问题。给定两个字符串,允许“添加”,“删除”或是“替换”字符,如何计算出将一个字符串转换成另一个字符串的操作次数。我们不难发现我们之前讨论的问题和这个经典的问题非常相似,但是又有以下一些不同点:

  • DOM元素并不能很好地支持“替换”的操作。通过浏览器的JavaScript api并不能很高效地将一个DOM元素变换成另一个DOM元素,所以必须通过“添加”和“删除”的组合操作来实现“替换”的等效操作。
  • DOM元素可以支持“移动”操作。尽管原版Edit Distance的并没有提到,但是在浏览器中利用已经存在的DOM元素是一个很合理的做法。
  • 原版问题只要求计算出最小的操作次数,我们的问题里需要计算出一个DOM操作序列。

众所周知,经典Edit Distance的算法使用动态规划实现,需要使用O(m*n)的时间和O(m*n)的空间复杂度(假设两个字符串的长度分别为m和n)。

KnockoutJS使用的edit distance算法

KnockoutJS的数组比较算法的第一步是一个变种的edit distance算法,基于具体问题的特殊性进行了一些调整。算法仍然使用动态规划,需要计算出一个2维的edit distance矩阵(叫做M),每个元素对应两个数组的子序列的最小edit distance + 1。比如说,假设两个数组分别叫arr1和arr2,矩阵的第i行第j列的值就是arr1[:i]和arr2[:j]的最小edit distance + 1。

不难发现,从任意的一个(i,j)配对出发,我们可以有如下的递归关系:

  • arr1[i-1] === arr2[j-1], 我们可以省去一次操作,M[i][j] = M[i-1][j-1]
  • arr1[i-1] !== arryou2[j-1], 这时我们有两种选项,取最小值
    • 删除arr1[i-1],继续比较, 此时M[i][j] = M[i-1][j] + 1
    • 在arr1[i-1]后添加一个等于arr2[j-1]的元素,继续比较,此时M[i][j] = M[i][j-1] + 1

根据这个递推关系可以知道如何初始化好第一行和第一列。算法本身使用循环自下而上实现,可以省去递归带来的额外堆栈开销。

计算edit distance具体代码如下:

// ... preprocess such that arr1 is always the shorter array
var myMin = Math.min,
  myMax = Math.max,
  editDistanceMatrix = [],
  smlIndex, smlIndexMax = smlArray.length,
  bigIndex, bigIndexMax = bigArray.length,
  compareRange = (bigIndexMax - smlIndexMax) || 1,
  maxDistance = smlIndexMax + bigIndexMax + 1,
  thisRow, lastRow,
  bigIndexMaxForRow, bigIndexMinForRow;

for (smlIndex = 0; smlIndex <= smlIndexMax; smlIndex++) {
  lastRow = thisRow;
  editDistanceMatrix.push(thisRow = []);
  bigIndexMaxForRow = myMin(bigIndexMax, smlIndex + compareRange);
  bigIndexMinForRow = myMax(0, smlIndex - 1);
  for (bigIndex = bigIndexMinForRow; bigIndex <= bigIndexMaxForRow; bigIndex++) {
    if (!bigIndex)
      thisRow[bigIndex] = smlIndex + 1;
    else if (!smlIndex) // Top row - transform empty array into new array via additions
      thisRow[bigIndex] = bigIndex + 1;
    else if (smlArray[smlIndex - 1] === bigArray[bigIndex - 1])
      thisRow[bigIndex] = lastRow[bigIndex - 1];         // copy value (no edit)
    else {
      var northDistance = lastRow[bigIndex] || maxDistance;    // not in big (deletion)
      var westDistance = thisRow[bigIndex - 1] || maxDistance;  // not in small (addition)
      thisRow[bigIndex] = myMin(northDistance, westDistance) + 1;
    }
  }
}
// editDistanceMatrix now stores the result

算法利用了一个具体问题的特性,那就是头尾交叉的子序列配对不可能出现最优情况。比如说,对于数组abc和efg来说,配对abc和e不可能出现在最优解里。因此算法的第二层循环只需要遍历长数组长度和短数组长度的差值而不是长数组的长度。算法的时间复杂度被缩减到了O(m*(n-m))。因为JavaScript的数组基于object实现,未使用的index不会占用内存,因此空间复杂度也被缩减到了O(m*(n-m))。

仔细想想会发现在这个应用场景里,这是一个非常高效的算法。尽管理论最坏复杂度仍然是平方级,但是对于前端应用的场景来说,大部分时间面对的是高频小幅的数据变化。也就是说,在大部分情况下,n和m非常接近,因此这个算法在大部分情况下可以达到线性的时间和空间复杂度,相比平方级的复杂度是一个巨大的提升。

在得到edit distance matrix之后获取操作序列就非常简单了,只要从尾部按照之前赋值的规则倒退至第一行或者第一列即可。
计算操作序列具体代码如下:

// ... continue from the edit distance computation
var editScript = [], meMinusOne, notInSml = [], notInBig = [];
for (smlIndex = smlIndexMax, bigIndex = bigIndexMax; smlIndex || bigIndex;) {
  meMinusOne = editDistanceMatrix[smlIndex][bigIndex] - 1;
  if (bigIndex && meMinusOne === editDistanceMatrix[smlIndex][bigIndex-1]) {
    notInSml.push(editScript[editScript.length] = {   // added
      'status': statusNotInSml,
      'value': bigArray[--bigIndex],
      'index': bigIndex });
  } else if (smlIndex && meMinusOne === editDistanceMatrix[smlIndex - 1][bigIndex]) {
    notInBig.push(editScript[editScript.length] = {   // deleted
      'status': statusNotInBig,
      'value': smlArray[--smlIndex],
      'index': smlIndex });
  } else {
    --bigIndex;
    --smlIndex;
    if (!options['sparse']) {
      editScript.push({
        'status': "retained",
        'value': bigArray[bigIndex] });
    }
  }
}
// editScript has the (reversed) sequence of actions

元素移动优化

如之前提到的,利用已有的重复元素可以减少不必要的DOM操作,具体实现方法非常简单,就是遍历所有的“添加”,“删除”操作,检查是否有相同的元素同时被添加和删除了。这个过程最坏情况下需要O(m*n)的时间复杂度,破环了之前的优化,因此算法提供一个可选的参数,在连续10*m个配对都没有发现可移动元素的情况下直接退出算法,从而保证整个算法的时间复杂度接近线性。

检查可移动元素的具体代码如下:

// left is all the operations of "Add"
// right is all the operations of "Delete
if (left.length && right.length) {
  var failedCompares, l, r, leftItem, rightItem;
  for (failedCompares = l = 0; (!limitFailedCompares || failedCompares < limitFailedCompares) && (leftItem = left[l]); ++l) {
    for (r = 0; rightItem = right[r]; ++r) {
      if (leftItem['value'] === rightItem['value']) {
        leftItem['moved'] = rightItem['index'];
        rightItem['moved'] = leftItem['index'];
        right.splice(r, 1);     // This item is marked as moved; so remove it from right list
        failedCompares = r = 0;   // Reset failed compares count because we're checking for consecutive failures
        break;
      }
    }
    failedCompares += r;
  }
}
// Operations that can be optimized to "Move" will be marked with the "moved" property

完整的相关代码可以在这里找到

knockout/src/binding/editDetection/compareArrays.js

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript面向对象之二 命名空间
Feb 08 Javascript
从数据结构分析看:用for each...in 比 for...in 要快些
Apr 17 Javascript
canvas实现弧形可拖动进度条效果
May 11 Javascript
基于LayUI分页和LayUI laypage分页的使用示例
Aug 02 Javascript
Bootstrap Table快速完美搭建后台管理系统
Sep 20 Javascript
Vue官方文档梳理之全局配置
Nov 22 Javascript
Vue 实现手动刷新组件的方法
Feb 19 Javascript
jQuery层叠选择器用法实例分析
Jun 28 jQuery
jquery 遍历hash操作示例【基于ajax交互】
Oct 12 jQuery
JS实现可控制的进度条
Mar 25 Javascript
详解React 元素渲染
Jul 07 Javascript
javaScript Array api梳理
Mar 31 Javascript
js实现简单的日历显示效果函数示例
Nov 25 #Javascript
VUE.CLI4.0配置多页面入口的实现
Nov 25 #Javascript
用Golang运行JavaScript的实现示例
Nov 25 #Javascript
JS插入排序简单理解与实现方法分析
Nov 25 #Javascript
纯 JS 实现放大缩小拖拽功能(完整代码)
Nov 25 #Javascript
python实现迭代法求方程组的根过程解析
Nov 25 #Javascript
JS桶排序的简单理解与实现方法示例
Nov 25 #Javascript
You might like
php4的session功能评述(二)
2006/10/09 PHP
windows下升级PHP到5.3.3的过程及注意事项
2010/10/12 PHP
PHP使用ODBC连接数据库的方法
2015/07/18 PHP
php实现编辑和保存文件的方法
2015/07/20 PHP
gearman中worker常驻后台,导致MySQL server has gone away的解决方法
2020/02/27 PHP
javascript 当前日期转化为中文的实现代码
2010/05/13 Javascript
理解Javascript_06_理解对象的创建过程
2010/10/15 Javascript
Javascript级联下拉菜单以及AJAX数据验证核心代码
2013/05/10 Javascript
Jquery Uploadify多文件上传带进度条且传递自己的参数
2013/08/28 Javascript
javascript中拼接HTML字符串的最快、最好的方法
2014/06/07 Javascript
浅析基于WEB前端页面的页面内容搜索的实现思路
2014/06/10 Javascript
javascript将浮点数转换成整数的三个方法
2014/06/23 Javascript
jQuery版AJAX简易封装代码
2016/09/14 Javascript
angular.js4使用 RxJS 处理多个 Http 请求
2017/09/23 Javascript
Angular5中调用第三方js插件的方法
2018/02/26 Javascript
python 正则式使用心得
2009/05/07 Python
python 解析html之BeautifulSoup
2009/07/07 Python
Python做文本按行去重的实现方法
2016/10/19 Python
浅谈pandas中DataFrame关于显示值省略的解决方法
2018/04/08 Python
Python实现的合并两个有序数组算法示例
2019/03/04 Python
python 求10个数的平均数实例
2019/12/16 Python
python随机生成大小写字母数字混合密码(仅20行代码)
2020/02/01 Python
Python递归及尾递归优化操作实例分析
2020/02/01 Python
python打印文件的前几行或最后几行教程
2020/02/13 Python
python爬虫把url链接编码成gbk2312格式过程解析
2020/06/08 Python
Django windows使用Apache实现部署流程解析
2020/10/12 Python
用CSS3写的模仿iPhone中的返回按钮
2015/04/04 HTML / CSS
纯html5+css3下拉导航菜单实现代码
2013/03/18 HTML / CSS
欧洲最大的球衣网上商店:Kitbag
2017/11/11 全球购物
英国时尚泳装品牌:Maru Swimwear
2019/10/06 全球购物
Strathberry苏贝瑞中国官网:西班牙高级工匠手工打造
2020/10/19 全球购物
中科软测试工程师面试题
2012/06/16 面试题
大学生职业规划书的范本
2014/02/18 职场文书
我们的节日中秋活动方案
2014/08/19 职场文书
食堂管理制度范本
2015/08/04 职场文书
Python异常类型以及处理方法汇总
2021/06/05 Python