├── README.md ├── image ├── img1.png └── img2.png └── src └── diff.js /README.md: -------------------------------------------------------------------------------- 1 | # miniprogram-diff - 小程序setData高性能diff算法 2 | > 关于渲染性能调优可以看我之前写的一篇文章:https://mp.weixin.qq.com/s/3Os3KV6sXXAhSQ0Lj5cxfw 3 | 4 | ## 前言 5 | 小程序的视图层和逻辑层是两个独立的线程模块,并不具备数据直接共享的通道,双方的通讯需要经过底层的JSBridge,数据到达视图层并不是实时的。 6 | 7 | ![小程序通讯架构](image/img1.png) 8 | - 频繁的执行setData,导致的后果:WebView JS线程一直在编译执行渲染,逻辑层到页面层的通讯耗时上升,导致渲染结果有延迟。 9 | - 数据传输实际是一次 **evaluateJavascript** 脚本过程。当数据量过大时会增加脚本的编译执行时间,占用 WebView JS 线程。 10 | 11 | ## 目的 12 | 解决小程序内因setData执行频繁和数据传输量大而引发的页面渲染延时和响应延迟问题。 13 | 14 | ## 优化方向 15 | - 降低setData执行频率 ---> 取消重复数据setData更新 16 | - 减少setData数据传输量 ---> 避免重复数据做setData操作 17 | 18 | **目标:实现更细粒度的精准修改** 19 | 20 | 策略:在数据更新之前先对更新前后数据做diff对比,找出差异部分patch,如果patch为空,结束更新,否则只对patch部分做更新。 21 | 22 | ![小程序通讯架构](image/img2.png) 23 | 24 | ## diff算法设计思路 25 | - diff前先将数据路径写法数据转换成格式化JSON 26 | - 使用深度优先遍历策略 27 | - 只对同层节点进行对比 28 | - 使用数据路径方式实现局部更新 29 | - 减少不必要的diff对比 30 | 31 | **具体diff算法设计请看源码** 32 | 33 | ## 使用 34 | 可以对setData做一个新的封装,Promise化使用 35 | ```javascript 36 | import diff from './src/diff'; 37 | 38 | // this指向的是Page对象 39 | this.update = (data) => { 40 | return new Promise((resolve, reject) => { 41 | if (Object.prototype.toString.call(data) !== '[object Object]') { 42 | reject('Error data type'); 43 | return; 44 | } 45 | const result = diff(data, this.data); 46 | if (!Object.keys(result).length) { 47 | resolve(null); 48 | return; 49 | } 50 | this.setData(result, () => { 51 | resolve(result); 52 | }); 53 | }); 54 | } 55 | ``` 56 | 封装后使用方法 57 | ```javascript 58 | this.update({ 59 | a: 1, 60 | b: { 61 | c: 2 62 | }, 63 | d: [1, 2, 3] 64 | }).then((res) => { 65 | // 渲染成功回调 66 | // do something 67 | }); 68 | ``` 69 | 70 | ## diff算法测试用例 71 | ```javascript 72 | import diff from './src/diff'; 73 | 74 | // 用例1 75 | const result1 = diff( 76 | { a: 10, 'c.d': [1, 2], 'c.e': { f: 'hello', g: { h: 11}}, i: false }, 77 | { a: 1, b: 2, c: { d: [1, 2, 3], e: { f: 'hello', g: { h: 3}}}, i: true } 78 | ); 79 | 80 | // diff结果 81 | result1: { 82 | a: 10, 83 | 'c.d': [1, 2], 84 | 'c.e.g.h': 11, 85 | i: false 86 | } 87 | 88 | 89 | // 用例2 90 | const result2 = diff( 91 | { a: 1, c: { d: [1, 'test1', false], e: { f: 'hello world', g: { h: 3}}}, i: ['a', 'b'] }, 92 | { a: 1, b: 2, c: { d: [1, 'test', false], e: { f: 'hello', g: { h: 3}}} } 93 | ); 94 | 95 | // diff结果 96 | result2: { 97 | 'c.d[1]': 'test1', 98 | 'c.e.f': 'hello world', 99 | i: ['a', 'b'] 100 | } 101 | 102 | 103 | // 用例3 104 | const result3 = diff( 105 | { a: [1, 2], b: { c: 1, d: 2 } }, 106 | { a: [1, 2, 3], b: { c: 1, d: 2, e: 3 } } 107 | ); 108 | 109 | // diff结果 110 | result3: { 111 | a: [1, 2], 112 | b: { 113 | c: 1, 114 | d: 2 115 | } 116 | } 117 | ``` 118 | -------------------------------------------------------------------------------- /image/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpcong/miniprogram-diff/9bcfde568ef25a37a7ef7dbdd2f1159eb2111fd4/image/img1.png -------------------------------------------------------------------------------- /image/img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lpcong/miniprogram-diff/9bcfde568ef25a37a7ef7dbdd2f1159eb2111fd4/image/img2.png -------------------------------------------------------------------------------- /src/diff.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: andypliang 3 | * @Description: 小程序setData diff算法 4 | */ 5 | const OBJECT_TYPE = '[object Object]'; 6 | const ARRAY_TYPE = '[object Array]'; 7 | const getType = (obj) => Object.prototype.toString.call(obj); 8 | const initPath = (data) => { 9 | if (getType(data) !== OBJECT_TYPE) return; 10 | for (let item in data) { 11 | if (/\w+\.\w+/g.test(item) && item.indexOf('[') === -1) { 12 | const arr = item.split('.'); 13 | let result = data, len = arr.length; 14 | for (let i = 0; i < len - 1; i++) { 15 | const arrItem = arr[i]; 16 | if (getType(result[arrItem]) !== OBJECT_TYPE) { 17 | result[arrItem] = {}; 18 | } 19 | result = result[arrItem]; 20 | } 21 | result[arr[len - 1]] = data[item]; 22 | delete data[item]; 23 | } 24 | } 25 | }; 26 | const initData = (cur, pre, root = false) => { 27 | if (cur === pre) return; 28 | const curType = getType(cur), preType = getType(pre); 29 | if (curType !== preType) return; 30 | if (curType === ARRAY_TYPE && cur.length >= pre.length) { 31 | for (let i = 0; i < pre.length; i++) { 32 | initData(cur[i], pre[i]); 33 | } 34 | } else if (curType === OBJECT_TYPE && Object.keys(cur).length >= Object.keys(pre).length) { 35 | for (let key in pre) { 36 | if (!root && cur[key] === undefined) { 37 | cur[key] = null; 38 | } else { 39 | initData(cur[key], pre[key]); 40 | } 41 | } 42 | } 43 | }; 44 | const doDiff = (cur, pre, target, path = '', root = false) => { 45 | if (cur === pre) return; 46 | const curRootType = getType(cur), preRootType = getType(pre); 47 | if (curRootType === ARRAY_TYPE && preRootType === curRootType && cur.length >= pre.length) { 48 | for (let i = 0; i < cur.length; i++) { 49 | doDiff(cur[i], pre[i], target, `${path}[${i}]`); 50 | } 51 | return; 52 | } 53 | if (curRootType === OBJECT_TYPE && preRootType === curRootType && (root || Object.keys(cur).length >= Object.keys(pre).length)) { 54 | const keys = Object.keys(cur); 55 | for (let key of keys) { 56 | const curVal = cur[key], preVal = pre[key]; 57 | const curType = getType(curVal), preType = getType(preVal); 58 | if (curVal === preVal) continue; 59 | if (curType === ARRAY_TYPE && preType === curType && curVal.length >= preVal.length) { 60 | for (let i = 0; i < curVal.length; i++) { 61 | doDiff(curVal[i], preVal[i], target, `${path ? path + '.' : ''}${key}[${i}]`); 62 | } 63 | continue; 64 | } 65 | if (curType === OBJECT_TYPE && preType === curType && Object.keys(curVal).length >= Object.keys(preVal).length) { 66 | for (let sKey in curVal) { 67 | doDiff(curVal[sKey], preVal[sKey], target, `${path ? path + '.' : ''}${key}.${sKey}`); 68 | } 69 | continue; 70 | } 71 | target[`${path ? path + '.' : ''}${key}`] = curVal; 72 | } 73 | return; 74 | } 75 | target[path] = cur; 76 | }; 77 | export default function diff(data, prevData) { 78 | const target = {}; 79 | initPath(data); 80 | initData(data, prevData, true); 81 | doDiff(data, prevData, target, '', true); 82 | return target; 83 | } --------------------------------------------------------------------------------