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 Zifa FormValid 0.1表单验证 代码打包下载
Jun 08 Javascript
jQuery.query.js 取参数的两点问题分析
Aug 06 Javascript
jquery文字上下滚动的实现方法
Mar 22 Javascript
jquery遍历select元素(实例讲解)
Dec 31 Javascript
让javascript加载速度倍增的方法(解决JS加载速度慢的问题)
Dec 12 Javascript
js实现简单锁屏功能实例
May 27 Javascript
JavaScript入门教程之引用类型
May 04 Javascript
jQuery EasyUI菜单与按钮详解
Jul 13 Javascript
AngularJS中下拉框的基本用法示例
Oct 11 Javascript
AngularJS实时获取并显示密码的方法
Feb 06 Javascript
微信小程序canvas截取任意形状的实现代码
Jan 13 Javascript
Echarts.js无法引入问题解决方案
Oct 30 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
一个域名查询的程序
2006/10/09 PHP
帖几个PHP的无限分类实现想法~
2007/01/02 PHP
PHP防注入安全代码
2008/04/09 PHP
php生成随机数或者字符串的代码
2008/09/05 PHP
PHP中shuffle数组值随便排序函数用法
2014/11/21 PHP
JS 实现Json查询的方法实例
2013/04/12 Javascript
Firefox和IE兼容性问题及解决方法总结
2013/10/08 Javascript
javascript实现颜色渐变的方法
2013/10/30 Javascript
如何在指定的地方插入html内容和文本内容
2013/12/23 Javascript
JavaScript的兼容性与调试技巧
2016/11/22 Javascript
JS jQuery使用正则表达式去空字符的简单实现代码
2017/05/20 jQuery
JS构造一个html文本内容成文件流形式发送到后台
2018/07/31 Javascript
详解Nuxt.js部署及踩过的坑
2018/08/07 Javascript
vue-cli项目代理proxyTable配置exclude的方法
2018/09/20 Javascript
vue实现双向绑定和依赖收集遇到的坑
2018/11/29 Javascript
javaScript把其它类型转换为Number类型
2019/10/13 Javascript
微信小程序之左右布局的实现代码
2019/12/13 Javascript
Javascript表单序列化原理及实现代码详解
2020/10/30 Javascript
[31:00]2014 DOTA2华西杯精英邀请赛5 24 NewBee VS iG
2014/05/25 DOTA
编写简单的Python程序来判断文本的语种
2015/04/07 Python
Python实现监控程序执行时间并将其写入日志的方法
2015/06/30 Python
Python 实现文件的全备份和差异备份详解
2016/12/27 Python
朴素贝叶斯分类算法原理与Python实现与使用方法案例
2018/06/26 Python
使用pyecharts生成Echarts网页的实例
2019/08/12 Python
python 3.7.4 安装 opencv的教程
2019/10/10 Python
关于Flask项目无法使用公网IP访问的解决方式
2019/11/19 Python
python读取excel进行遍历/xlrd模块操作
2020/07/12 Python
英国马莎百货印度官网:Marks & Spencer印度
2020/10/08 全球购物
军校大学生个人的自我评价
2014/02/17 职场文书
营销总监岗位职责范本
2014/02/26 职场文书
QQ空间主人寄语大全
2014/04/12 职场文书
教师四风自我剖析材料
2014/09/30 职场文书
2015年董事长秘书工作总结
2015/07/23 职场文书
幼儿园毕业典礼园长致辞
2015/07/29 职场文书
中秋节作文(五年级)之关于月亮
2019/09/11 职场文书
Python开发工具Pycharm的安装以及使用步骤总结
2021/06/24 Python