补充:JS中其实是没有线程概念的,所谓的单线程也只是相对于多线程而言。JS的设计初衷就没有考虑这些,针对JS这种不具备并行任务处理的特性,我们称之为“单线程”。
JS单线程是指一个浏览器进程中只有一个JS的执行线程,同一时刻内只会有一段代码在执行。
举个通俗例子,假设JS支持多线程操作的话,JS可以操作DOM,那么一个线程在删除DOM,另外一个线程就在获取DOM数据,这样子明显不合理,这算是证明之一。
来看段代码👇
function foo() { console.log("first");
setTimeout(( function(){ console.log( 'second' );
}),5);
}
for (var i = 0; i < 1000000; i++) {
foo();
}复制代码
打印结果就是首先是很多个first,然后再是second。
异步机制是浏览器的两个或以上常驻线程共同完成的,举个例子,比如异步请求由两个常驻线程,JS执行线程和事件触发线程共同完成的。
先给出结论
CSS
不会阻塞DOM
解析,但会阻塞DOM
渲染。CSS
会阻塞JS执行,并不会阻塞JS文件下载CSS
控制的属性,浏览器是需要计算的,也就是依赖于CSS
。浏览器也无法感知脚本内容到底是什么,为避免样式获取,因而只好等前面所有的样式下载完后,再执行JS
。先给出结论👇
由于 JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。 因此为了防止渲染出现不可预期的结果,浏览器设置 「GUI 渲染线程与 JavaScript 引擎为互斥」的关系。 当 JavaScript 引擎执行时 GUI 线程会被挂起,GUI 更新会被保存在一个队列中等到引擎线程空闲时立即被执行。 当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。 因此如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
DOMContentLoaded
事件前执行,如果缺少 src
属性(即内嵌脚本),该属性不应被使用,因为这种情况下它不起作用带async的脚本一定会在load事件之前执行,可能会在DOMContentLoaded之前或之后执行。
如果 script 标签中包含 defer,那么这一块脚本将不会影响 HTML 文档的解析,而是等到HTML 解析完成后才会执行。而 DOMContentLoaded 只有在 defer 脚本执行结束后才会被触发。
我觉得这个题目说法上可能就是行不通,不能这么说,如果了解的话,都知道will-change只是一个优化的手段,使用JS改变transform也可以享受这个属性带来的变化,所以这个说法上有点不妥。
所以围绕这个问题展开话,更应该说建议推荐使用CSS动画,至于为什么呢,涉及的知识点大概就是重排重绘,合成,这方面的点,我在浏览器渲染流程中也提及了。
尽可能的避免重排和重绘,具体是哪些操作呢,如果非要去操作JS实现动画的话,有哪些优化的手段呢?
比如👇
createDocumentFragment
进行批量的 DOM 操作节流的意思是让函数有节制地执行,而不是毫无节制的触发一次就执行一次。什么叫有节制呢?就是在一段时间内,只执行一次。
规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
抓取一个关键的点:就是执行的时机。要做到控制执行的时机,我们可以通过「一个开关」,与定时器setTimeout结合完成。
function throttle(fn, delay) { let flag = true,
timer = null; return function (...args) { let context = this; if (!flag) return;
flag = false;
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(context, args);
flag = true;
}, delay);
};
};复制代码
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
核心思想:每次事件触发都会删除原有定时器,建立新的定时器。通俗意思就是反复触发函数,只认最后一次,从最后一次开始计时。
代码:
function debounce(fn, delay) { let timer = null
return function (...args) { let context = this
if(timer) clearTimeout(timer)
timer = setTimeout(function() {
fn.apply(context, args)
},delay)
}
}复制代码
_.debounce
和 _.throttle
方法,可以使用 Lodash 的自定义构建工具,生成一个 2KB 的压缩库。使用以下的简单命令即可:npm i -g lodash-cli
npm i -g lodash-clilodash-cli include=debounce,throttle复制代码
_.debounce
方法:// 错误$(window).on('scroll', function() {
_.debounce(doSomething, 300);
});// 正确$(window).on('scroll', _.debounce(doSomething, 200));复制代码
debounced_version.cancel()
,lodash 和 underscore.js 都有效。let debounced_version = _.debounce(doSomething, 200);
$(window).on(‘scroll’, debounced_version);// 如果需要的话debounced_version.cancel();复制代码防抖
节流
动画帧率可以作为衡量标准,一般来说画面在 60fps 的帧率下效果比较好。
换算一下就是,每一帧要在 16.7ms (16.7 = 1000/60) 内完成渲染。
我们来看看MDN对它的解释吧👇
window.requestAnimationFrame() 方法告诉浏览器您希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画。该方法使用一个回调函数作为参数,这个回调函数会在浏览器重绘之前调用。— MDN
当我们调用这个函数的时候,我们告诉它需要做两件事:
rAF(requestAnimationFrame) 最大的优势是「由系统来决定回调函数的执行时机」。
具体一点讲就是,系统每次绘制之前会主动调用 rAF 中的回调函数,如果系统绘制率是 60Hz,那么回调函数就每16.7ms 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。
换句话说就是,rAF 的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次(上一个知识点刚刚梳理完「函数节流」),这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
另外它可以自动调节频率。如果callback工作太多无法在一帧内完成会自动降低为30fps。虽然降低了,但总比掉帧好。
与setTimeout动画对比的话,有以下几点优势
规范中似乎是这么去定义的:
这样子分析的话,似乎很合理嘛,为什么要在重新渲染前去调用呢?因为rAF作为官方推荐的一种做流畅动画所应该使用的API,做动画不可避免的去操作DOM,而如果是在渲染后去修改DOM的话,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这样子似乎不合理。
rAF
在浏览器决定渲染之前给你最后一个机会去改变 DOM 属性,然后很快在接下来的绘制中帮你呈现出来,所以这是做流畅动画的不二选择。
至于宏任务,微任务,这可以说起来就要展开篇幅了,暂时不在这里梳理了。
跟 _.throttle(dosomething, 16)
等价。它是高保真的,如果追求更好的精确度的话,可以用浏览器原生的 API 。
可以使用 rAF API 替换 throttle 方法,考虑一下优缺点:
优点
缺点
根据经验,如果 JavaScript 方法需要绘制或者直接改变属性,我会选择 requestAnimationFrame
,只要涉及到重新计算元素位置,就可以使用它。
涉及到 AJAX 请求,添加/移除 class (可以触发 CSS 动画),我会选择 _.debounce
或者 _.throttle
,可以设置更低的执行频率(例子中的200ms 换成16ms)。