dom刷新问题再探讨

dom刷新问题再探讨

用js更新dom是web系统中经常出现的场景,但是有时候可能会遇到这样的情况,在更新dom之后还执行了一段运行时间可能比较长的js代码,这时你会发现,你更新的dom不会立刻在页面显现出来,而要等所有js都执行完之后才能出现。以最近支持的一个项目为例,点击页面一个按钮后执行一段复杂的计算逻辑,虽然现代浏览器执行js的效率比以往大大提升,但你要相信公司的业务总是存在着一些变态的计算(不排除开发人员的js写得不够有效率所致,但此处暂且不考虑这些因素),在以速度见长的chrome浏览器上尚且需要几秒时间,更别说令人头疼的IE。可想而知,点击按钮后让客户无所适从地等待浏览器缓过劲来是多么糟糕的体验,所以,开发人员想到需要在执行计算之前在页面产生一些提示,执行完了之后去掉提示,于是,设计的代码逻辑如下:

[javascript]  view plain  copy
  1. function work(){  
  2.      showTip(); // 这里对dom进行了更新,show出提示信息或弹出模态窗口之类  
  3.      calculate();  //计算逻辑  
  4.      hideTip();  // 把提示信息隐藏起来,同样有对dom的改动  
  5. }  

这段逻辑看上去完全没有问题,但是当你点击按钮后你会发现什么提示信息也没有出来,无论你用哪种浏览器,这是为什么?根据stackoverflow上的一段解释,“Mozilla (maybe IE as well) will cache/delay executing changes tothe DOM which affect display, so that it can calculate all the changes at onceinstead of repeatedly after each and every statement.”。 也就是说,浏览器会cache住(或者说延迟)影响dom展现的操作,直到当前所有的js都执行完,这时候它可以一次性地更新需要更新的dom。

为什么会是这样呢,深入研究浏览器内核可以发现,浏览器内核是多线程的,其中一个常驻线程叫javascript引擎线程,负责执行js代码,还有一个常驻线程叫GUI渲染线程,负责页面渲染,dom重画等操作。javascript引擎是基于事件驱动单线程执行的,js线程一直在等待着任务列表中的任务到来,而js线程与gui渲染线程是互斥的,当js线程执行时,渲染线程呈挂起状态,只有当js线程空闲时渲染线程才会执行。所以,我们可以理解为什么dom更新总是不能被立刻执行。就我们的代码来说,显示提示和隐藏提示的dom操作都被浏览器记下来了并放在gui渲染线程的任务队列中,但都没有立刻进行渲染,而是在当前函数完成后(js线程已处于空闲状态),进行最终的dom渲染,而我们的用户则基本感受不到这个过程,因为经过show和hide两个相反的操作,相当于dom完全没变。


浏览器的行为虽然是合理的,但却给我们带来了麻烦,怎么才能强制浏览器执行dom更新的操作以满足我们的业务需要,给用户友好的提示呢,有两种方法:

1.     采用alert语句进行提示,alert语句会block住js线程,将执行权让给gui渲染线程,执行alert之后浏览器会把这个语句之前的所有对dom的操作都进行体现。这个方法虽然简单有效,但不具有可操作性,首先alert是简单粗暴的一种提示方式,反倒降低了用户体验,其次不能适用在各种场景中,不可能在系统中无缘无故地弹出个alert框只是为了强制重画更新的dom。所以,该方法不值得推广。

采用setTimeout方法,这是普遍的解决方案。把计算逻辑和隐藏提示的方法放在setTimeout里可以解决这个问题,因为setTimeout启用了一个定时器,指定在经过一段时间后执行某段逻辑,从而使这段逻辑跳离了当前函数体,使当前函数可以快速地执行完,之后如果js引擎线程中没有排队的任务,则gui渲染线程得到执行,showTip相关的dom更新得到体现。当定时器到时后,js线程又得到了新的任务,从而使计算逻辑和隐藏提示的操作得到执行。所以,我们的代码逻辑可以改成如下形式:

[javascript]  view plain  copy
  1. function work(){  
  2.      showTip();  // dom操作的任务已放到gui渲染线程中  
  3.      setTimeout(function(){  
  4.     calculate();    
  5.         hideTip();   
  6.      }, 100);  // 启用定时器100ms后执行任务,目的就是让js线程空闲以使渲染得到执行  
  7. }  

js阻塞dom刷新的问题在  Not blocking the UI in tightJavaScript loops一文中也有描述类似的场景,作者提出在一个for循环操作中,需要在每次执行操作时在某个dom节点上显示当前处理的是第几条记录,代码逻辑如下:

[javascript]  view plain  copy
  1. var lis = document.getElementsByTagName("li");  
  2. for (var i=0; i<lis.length; i++) { // yes this could be more efficient, don't care  
  3.   // do something here to lis[i]  
  4.   progressMonitor.innerHTML = "processing list item " + i; // fail  
  5. };  

与我刚才提到的情况相同,因为当前js线程还在执行,渲染线程挂起使得dom不能得到更新,只有等循环完了之后才能看到,而此时看到的是最终的结果。要实现实时的dom更新,作者也是采用setTimeout的方法,如:

[javascript]  view plain  copy
  1. var lis = document.getElementsByTagName("li");  
  2. var counter = 0;  
  3. function doWork() {  
  4.   // do something here to lis[i]  
  5.   counter += 1;  
  6.   progressMonitor.innerHTML = "processing list item " + counter;  
  7.   if (counter < lis.length) {  
  8.     setTimeout(doWork, 1);  
  9.   }  
  10. };  
  11. setTimeout(doWork, 1);  

用setTimeout替代了循环,在每次执行操作后启用setTimeout执行下次操作,模拟了循环语句,也使得渲染线程每次都能得到执行。但是这种方法带来的缺点就是性能的下降,虽然定时器设置的是1ms,但浏览器每启用一次定时器的时间应该远大于1ms,在数据量大的情况下,相比起简单的循环操作,这个耗费的时间还是相当可观的

用js更新dom是web系统中经常出现的场景,但是有时候可能会遇到这样的情况,在更新dom之后还执行了一段运行时间可能比较长的js代码,这时你会发现,你更新的dom不会立刻在页面显现出来,而要等所有js都执行完之后才能出现。以最近支持的一个项目为例,点击页面一个按钮后执行一段复杂的计算逻辑,虽然现代浏览器执行js的效率比以往大大提升,但你要相信公司的业务总是存在着一些变态的计算(不排除开发人员的js写得不够有效率所致,但此处暂且不考虑这些因素),在以速度见长的chrome浏览器上尚且需要几秒时间,更别说令人头疼的IE。可想而知,点击按钮后让客户无所适从地等待浏览器缓过劲来是多么糟糕的体验,所以,开发人员想到需要在执行计算之前在页面产生一些提示,执行完了之后去掉提示,于是,设计的代码逻辑如下:

[javascript]  view plain  copy
  1. function work(){  
  2.      showTip(); // 这里对dom进行了更新,show出提示信息或弹出模态窗口之类  
  3.      calculate();  //计算逻辑  
  4.      hideTip();  // 把提示信息隐藏起来,同样有对dom的改动  
  5. }  

这段逻辑看上去完全没有问题,但是当你点击按钮后你会发现什么提示信息也没有出来,无论你用哪种浏览器,这是为什么?根据stackoverflow上的一段解释,“Mozilla (maybe IE as well) will cache/delay executing changes tothe DOM which affect display, so that it can calculate all the changes at onceinstead of repeatedly after each and every statement.”。 也就是说,浏览器会cache住(或者说延迟)影响dom展现的操作,直到当前所有的js都执行完,这时候它可以一次性地更新需要更新的dom。

为什么会是这样呢,深入研究浏览器内核可以发现,浏览器内核是多线程的,其中一个常驻线程叫javascript引擎线程,负责执行js代码,还有一个常驻线程叫GUI渲染线程,负责页面渲染,dom重画等操作。javascript引擎是基于事件驱动单线程执行的,js线程一直在等待着任务列表中的任务到来,而js线程与gui渲染线程是互斥的,当js线程执行时,渲染线程呈挂起状态,只有当js线程空闲时渲染线程才会执行。所以,我们可以理解为什么dom更新总是不能被立刻执行。就我们的代码来说,显示提示和隐藏提示的dom操作都被浏览器记下来了并放在gui渲染线程的任务队列中,但都没有立刻进行渲染,而是在当前函数完成后(js线程已处于空闲状态),进行最终的dom渲染,而我们的用户则基本感受不到这个过程,因为经过show和hide两个相反的操作,相当于dom完全没变。


浏览器的行为虽然是合理的,但却给我们带来了麻烦,怎么才能强制浏览器执行dom更新的操作以满足我们的业务需要,给用户友好的提示呢,有两种方法:

1.     采用alert语句进行提示,alert语句会block住js线程,将执行权让给gui渲染线程,执行alert之后浏览器会把这个语句之前的所有对dom的操作都进行体现。这个方法虽然简单有效,但不具有可操作性,首先alert是简单粗暴的一种提示方式,反倒降低了用户体验,其次不能适用在各种场景中,不可能在系统中无缘无故地弹出个alert框只是为了强制重画更新的dom。所以,该方法不值得推广。

采用setTimeout方法,这是普遍的解决方案。把计算逻辑和隐藏提示的方法放在setTimeout里可以解决这个问题,因为setTimeout启用了一个定时器,指定在经过一段时间后执行某段逻辑,从而使这段逻辑跳离了当前函数体,使当前函数可以快速地执行完,之后如果js引擎线程中没有排队的任务,则gui渲染线程得到执行,showTip相关的dom更新得到体现。当定时器到时后,js线程又得到了新的任务,从而使计算逻辑和隐藏提示的操作得到执行。所以,我们的代码逻辑可以改成如下形式:

[javascript]  view plain  copy
  1. function work(){  
  2.      showTip();  // dom操作的任务已放到gui渲染线程中  
  3.      setTimeout(function(){  
  4.     calculate();    
  5.         hideTip();   
  6.      }, 100);  // 启用定时器100ms后执行任务,目的就是让js线程空闲以使渲染得到执行  
  7. }  

js阻塞dom刷新的问题在  Not blocking the UI in tightJavaScript loops一文中也有描述类似的场景,作者提出在一个for循环操作中,需要在每次执行操作时在某个dom节点上显示当前处理的是第几条记录,代码逻辑如下:

[javascript]  view plain  copy
  1. var lis = document.getElementsByTagName("li");  
  2. for (var i=0; i<lis.length; i++) { // yes this could be more efficient, don't care  
  3.   // do something here to lis[i]  
  4.   progressMonitor.innerHTML = "processing list item " + i; // fail  
  5. };  

与我刚才提到的情况相同,因为当前js线程还在执行,渲染线程挂起使得dom不能得到更新,只有等循环完了之后才能看到,而此时看到的是最终的结果。要实现实时的dom更新,作者也是采用setTimeout的方法,如:

[javascript]  view plain  copy
  1. var lis = document.getElementsByTagName("li");  
  2. var counter = 0;  
  3. function doWork() {  
  4.   // do something here to lis[i]  
  5.   counter += 1;  
  6.   progressMonitor.innerHTML = "processing list item " + counter;  
  7.   if (counter < lis.length) {  
  8.     setTimeout(doWork, 1);  
  9.   }  
  10. };  
  11. setTimeout(doWork, 1);  

用setTimeout替代了循环,在每次执行操作后启用setTimeout执行下次操作,模拟了循环语句,也使得渲染线程每次都能得到执行。但是这种方法带来的缺点就是性能的下降,虽然定时器设置的是1ms,但浏览器每启用一次定时器的时间应该远大于1ms,在数据量大的情况下,相比起简单的循环操作,这个耗费的时间还是相当可观的

猜你喜欢

转载自blog.csdn.net/qq_39542027/article/details/78893873
今日推荐