经过上一篇文章的优化之后,加载速度有了明显的提升,但是又有了新的问题:
如果一个节点下有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);
}
},
第二步:在打开节点之后通过documentFragment和requestAnimationFrame方法渲染剩余的节点,修改的源码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呢,因为这样也会卡住。
上面就是我的第二次性能优化,为了让一个节点就有大量数据时打开节点流畅不影响用户的交互