前端对jQuery的jsTree插件的大量数据处理优化实践(单节点打开速度优化)

497 阅读3分钟

经过上一篇文章的优化之后,加载速度有了明显的提升,但是又有了新的问题

如果一个节点下有50000个子节点,打开节点时,需要20s的时间!并且打开时页面会卡顿;

打开节点卡顿原因:

查看源码打开节点的逻辑,在**open_node()方法中通过调用draw_children()**的方法绘制子节点,再通过选择器直接将子节点的display为none属性去掉,实现展示子节点的功能。

通过打印时间戳发现draw_children方法花费的时间不是很长,50000条数据大致在2s-3s的时间,但是单独一个修改样式的操作,将内联样式display为none去掉,让css中的display为block样式生效就花费了很长时间,一定是DOM节点一下添加这么多的节点导致的问题,于是想到了下面的解决方法:

解决大量的dom节点渲染卡顿问题:

通过一步步的摸索实践,最终实现的方式

1、通过修改源码 让他先绘制1000个节点,打开速度就是按照3000节点的速度打开,提高打开速度

2、打开节点之后然后通过**documentFragment()**创建节点,再append进去

3、一下append很多的节点还是会卡,于是想到了通过**requestAnimationFrame()**来分批次创建节点

具体代码实现:

jstree在open_node时调用draw_children来绘制子节点,我先让他绘制1000个子节点,在源码的**draw_children()方法中修改,修改后的draw_children()**方法:

draw_children : function (node) {
			var obj = this.get_node(node),
				i = false,
				j = false,
				k = false,
				d = document;
			if(!obj) { return false; }
			if(obj.id === $.jstree.root) { return this.redraw(true); }
			node = this.get_node(node, true);
			if(!node || !node.length) { return false; } // TODO: quick toggle

			node.children('.jstree-children').remove();
			node = node[0];

			if(obj.children.length && obj.state.loaded) {
				k = d.createElement('UL');
				k.setAttribute('role', 'group');
				k.className = 'jstree-children';
                // 重点是这里的逻辑 如果子节点长度小于1000 就按之前的长度 如果大于1000 就先渲染1000
				let length = obj.children.length>1000?1000:obj.children.length
				for(i = 0, j = length; i < j; i++) {
					ulDom = k;
					k.appendChild(this.redraw_node(obj.children[i], false, true));
				}

				node.appendChild(k);
			}

		},

第二步:在打开节点之后通过documentFragmentrequestAnimationFrame方法渲染剩余的节点,修改的源码open_node中的方法:

大家看下面代码注释 “主要的修改逻辑” 就是我修改的地方

		open_node : function (obj, callback, animation) {
			var t1, t2, d, t;
			if($.isArray(obj)) {
				obj = obj.slice();
				for(t1 = 0, t2 = obj.length; t1 < t2; t1++) {
					this.open_node(obj[t1], callback, animation);
				}
				return true;
			}
			obj = this.get_node(obj);
			if(!obj || obj.id === $.jstree.root) {
				return false;
			}
			animation = animation === undefined ? this.settings.core.animation : animation;
			if(!this.is_closed(obj)) {
				if(callback) {
					callback.call(this, obj, false);
				}
				return false;
			}
			if(!this.is_loaded(obj)) {
				if(this.is_loading(obj)) {
					return setTimeout($.proxy(function () {
						this.open_node(obj, callback, animation);
					}, this), 500);
				}
				this.load_node(obj, function (o, ok) {
					return ok ? this.open_node(o, callback, animation) : (callback ? callback.call(this, o, false) : false);
				});
			}
			else {
				d = this.get_node(obj, true);
				t = this;
				if(d.length) {
					if(animation && d.children(".jstree-children").length) {
						d.children(".jstree-children").stop(true, true);
					}
					let node = this.get_node(obj, true)[0];
					if(obj.children.length && !this._firstChild(d.children('.jstree-children')[0])) {
						// console.log(obj)
						this.draw_children(obj);
						//d = this.get_node(obj, true);
					}
					if(!animation) {
						this.trigger('before_open', { "node" : obj });
						d[0].className = d[0].className.replace('jstree-closed', 'jstree-open');
						d[0].setAttribute("aria-expanded", true);
					}
					else {
						this.trigger('before_open', { "node" : obj });			
						d.children(".jstree-children").css("display","none").end()
						d.removeClass("jstree-closed").addClass("jstree-open").attr("aria-expanded", true)
						.children(".jstree-children").stop(true,true)
						d.children(".jstree-children").css({"display":""})
						//主要修改的逻辑在这了  在1000条的子节点打开后 开始拼接剩余的子节点
							let arr = JSON.parse(JSON.stringify(obj.children))
							let height = arr.length*24;
							let that = this;
							if(arr.length>1000){
								arr.splice(0,1000);
								let count = Math.ceil(arr.length/50)

								// d.children(".jstree-children").css({"height":height+'px'})
								// 方法一
								// for(let j = 0;j<count;j++){
								// 	let docu = document.createDocumentFragment();
								// 	let newArr = arr.splice(0,4000);

								// 	let timer = setTimeout(() => {
								// 		for(let k = 0;k<newArr.length;k++){
								// 			console.log(docu,j,k)
								// 			docu.append(this.redraw_node(newArr[k], false, true))
								// 		}
								// 		d.children(".jstree-children").append(docu);
								// 		clearTimeout(timer)
								// 	}, 0);
								// }

								// window.requestAnimationFrame =window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
								// 方法二
								let j = 0;
								let cou = 0;
								window.aniArr=[];
                                // 通过 requestAnimationFrame 不断的调用该方法  每次添加50条
								function ani(){
									cou++;
									let docu = document.createDocumentFragment();
									let newArr = arr.splice(0,50);
									for(let k = 0;k<newArr.length;k++){
										// 这里的redraw_node是jstree里面的生成节点的方法
										docu.append(that.redraw_node(newArr[k], false, true))
									}
									d.children(".jstree-children").append(docu)
									if(j<count){
										j++;
										let animation = window.requestAnimationFrame(ani)
										aniArr.push(animation)
										if(aniArr.length>200){

											for (let ii = 0; ii < 100; ii++) {
												const ani = aniArr[ii];
												window.cancelAnimationFrame(ani)
											}
											aniArr.splice(0,100)
										}

									}else{

										for (let ii = 0; ii < aniArr.length; ii++) {
											const ani = aniArr[ii];
											window.cancelAnimationFrame(ani)
										}
									}
								}

								aniArr.push(window.requestAnimationFrame(ani))

							}
					}
				}
				obj.state.opened = true;
				if(callback) {
					callback.call(this, obj, true);
				}
				if(!d.length) {
					/**
					 * triggered when a node is about to be opened (if the node is supposed to be in the DOM, it will be, but it won't be visible yet)
					 * @event
					 * @name before_open.jstree
					 * @param {Object} node the opened node
					 */
					this.trigger('before_open', { "node" : obj });
				}
				/**
				 * triggered when a node is opened (if there is an animation it will not be completed yet)
				 * @event
				 * @name open_node.jstree
				 * @param {Object} node the opened node
				 */
				this.trigger('open_node', { "node" : obj });
				if(!animation || !d.length) {
					/**
					 * triggered when a node is opened and the animation is complete
					 * @event
					 * @name after_open.jstree
					 * @param {Object} node the opened node
					 */
					this.trigger("after_open", { "node" : obj });
				}
				return true;
			}
		},

科普:MDN关于 requestAnimationFrame 的解释

为什么使用requestAnimationFrame:因为在渲染的时候为了不影响用户继续操作UI界面的交互,如果一下子渲染30000条数据,我们要等渲染结束之后才能响应用户的操作事件,但是requestAnimationFrame我们每次渲染50条,等渲染完之后在此调用requestAnimationFrame(),每次渲染50条结束后都可以响应用户的操作,由于他的渲染非常快,所以基本上在渲染时不影响用户的交互。

为什么使用documentFragment: 因为我们生成单个节点之后就append到documentFragment中,当50个节点都生成之后再将documentFragment一次性添加到DOM节点当中,减少dom操作。可能有人会疑惑,为什么不将30000节点都放到documentFragment中之后再操作一次DOM呢,因为这样也会卡住。

上面就是我的第二次性能优化,为了让一个节点就有大量数据时打开节点流畅不影响用户的交互