之前看了一些关于作用域的文章和书,可是都渐渐淡忘了,这里我在重新复习作用域之前,先去了解一下js引擎编译的大致过程,来帮助我加深对js的理解.
渲染引擎
浏览器的核心是两部分:渲染引擎和javascript解释器(引擎);不同的浏览器有不同的渲染引擎,他的主要作用是生成网页,通常分成四个阶段,因为图片看起来更加直观,所以把内容放在图片里.
javascript虚拟机(引擎)
-
js是解释型语言,有一定的优缺点:
-
早期,浏览器内部对js的处理过程:
-
即时编译
为了提高运行速度,现代浏览器改为采用'即时编译'(just in time compiler,缩写JIT),即字节码只在运行时编译,用到哪行,就编译哪一行,并且把编译结果缓存,通常,一个程序被经常用到的,只是其中一小部分的代码,有了缓存的编译结果,整个程序的运行速度就会显著提升.
有的浏览器索性省略了字节码的翻译步骤,直接编译成机器码,比如CHROME浏览器的v8引擎.
- 字节码不能直接运行,而是运行在一个虚拟机之上,一般也把虚拟机称为'javascript引擎'.因为js运行时未必有字节码,所以js虚拟机并不完全基于字节码,而是部分基于源码,即只要有可能,就通过JIT编译器直接把源码编译成机器码运行,省略字节码步骤,这一点与其他采用虚拟机的语言不尽相同,这样做的目的,是为了尽可能地优化代码,提高性能.
script标签的工作原理
正常的网页加载流程:
为了避免发生阻塞效应,较好的做法是将script标签都放在底部,而不是头部.这样即使遇到脚本失去响应,网页主体的渲染也已经完成了,用户至少可以看到内容,而不是面对一张空白的页面.
如果某些脚本代码非常重要,一定放在页面头部的话,最好将代码嵌入页面,而不是链接外部脚本文件,这样能缩短加载时间
将脚本放在网页尾部加载还有一个好处,在DOM结构生成之前就调用DOM,js会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时DOM肯定已经生成了.
defer属性
为了解决脚本文件下载阻塞网页渲染的问题,一个方法是加入defer属性
-
defer属性的作用是,告诉浏览器,等到DOM加载完成后,再执行指定脚本
-
有了defer属性,浏览器下载脚本文件的时候,不会阻塞页面渲染.下载的脚本文件在DOMContentLoaded事件触发前执行(即刚刚读取完</html>标签),而且可以保证执行顺序就是他们在页面上出现的顺序
-
对于内置而不是链接外部脚本的script标签,以及动态生成的script标签,defer属性不起作用
async
-
解决'阻塞效应'的另一个方法是加入async属性
-
async属性可以保证脚本下载的同时,浏览器继续渲染,
-
需要注意的是,一旦采用这个属性,就无法保证脚本的执行顺序,哪个脚本先下载结束,就先执行那个.使用async属性的脚本文件中,不应该使用document.write方法
重流和重绘(此部分为转载)
渲染树转换为网页布局,成为'布局流';布局显示到页面的这个过程,称为'绘制' 他们都具有阻塞效应,并且会耗费很多时间和计算资源
页面生成后,脚本操作和样式表操作,都会触发重流和重绘,用户的互动,比如设置了鼠标悬停效果,页面滚动,在输入框中输入文本,改变窗口大小.
重流和重绘并不一定一起发生,重流必然导致重绘,重绘不一定需要重流.比如改变元素的颜色,只会导致重绘,而不会导致重流.改变元素的布局,则会导致重流和重绘.
大多情况下,浏览器会智能判断,将重流和重绘只限制到相关的子树上面,最小化所耗费的代价,而不会全局生成网页
-
作为开发者,应该尽量设法降低重绘的次数和成本.比如,尽量不要变动高层的DOM元素,而以底层DOM元素的变动代替,再比如,重绘table布局和flex布局,开销都比较大.
var foo = document.getElementById(‘foobar’); foo.style.color = ‘blue’; foo.style.marginTop = ‘30px’;
上面的代码只会导致一次重绘,因为浏览器会累积DOM 变动,然后一次性执行.
下面的代码则会导致两次重绘:var foo = document.getElementById(‘foobar’); foo.style.color = ‘blue’; var margin = parseInt(foo.style.marginTop); foo.style.marginTop = (margin + 10) + ‘px’;
优化技巧:
- 读取DOM或者写入DOM,尽量写在一起,不要混杂;
- 缓存DOM信息
- 不要一项一项的改变样式,而是使用CSS class一次性改变样式
- 使用document fragment操作DOM
- 动画时使用absolute定位或fixed定位,这样可以减少对其他元素的影响
- 只在必要时才显示元素
- 使用window.requestAnimationFrame(),因为它可以把代码推迟到下一次重流时执行,而不是立即要求页面重流
- 使用虚拟DOM(virtual DOM)库
// 重绘代价高 function doubleHeight(element) { var currentHeight = element.clientHeight; element.style.height = (currentHeight * 2) + ‘px’; } all_my_elements.forEach(doubleHeight); // 重绘代价低 function doubleHeight(element) { var currentHeight = element.clientHeight; window.requestAnimationFrame(function () { element.style.height = (currentHeight * 2) + ‘px’; }); } all_my_elements.forEach(doubleHeight);
脚本的动态嵌入(此部分为转载)
['1.js', '2.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
document.head.appendChild(script);
});
这种方法的好处是,动态生成script标签不会阻塞页面渲染,也就不会造成浏览器假死,但是问题在于,这种方法无法保证脚本的执行顺序,哪个脚本文件先下载完成,就先执行哪个,
如果想避免这个问题,可以设置async属性为false
['1.js', '2.js'].forEach(function(src) {
var script = document.createElement('script');
script.src = src;
script.async = false;
document.head.appendChild(script);
});
在这段代码之后加载的脚本,要等待2.js执行完成后再执行
(function() {
var script,
scripts = document.getElementsByTagName('script')[0];
function load(url) {
script = document.createElement('script');
script.async = true;
script.src = url;
scripts.parentNode.insertBefore(script, scripts);
}
load('//apis.google.com/js/plusone.js');
load('//platform.twitter.com/widgets.js');
load('//s.thirdpartywidget.com/widget.js');
}());
此外,动态嵌入还有一个地方需要注意,动态嵌入必须等到CSS文件加载完成后,才会去下载外部脚本文件,静态加载就不存在这个问题,script标签指定的外部脚本文件,都是与css文件同时并发下载的.
单线程模型
-
js采用的是单线程模型
-
一次只能运行一个任务,其他任务需要等待前一个任务完成才能工作.
-
为什么不用多线程呢?
- 原因是不想浏览器变得复杂,因为多线程需要共享资源,且有可能修改彼此的运行结果
-
-
h5允许多线程,但是子线程完全受主线程控制,且不得操作DOM,所以这个新标准并没
- 有改变js单线程的本质.
-
存在的问题.
容易造成浏览器假死状态;
-
js设计者意识到,CPU完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务,等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去,这种机制就是js内部采用的Event Loop
Event Loop
-
Event Loop,指的是一种内部循环,用来排列和处理事件,以及执行函数.是一种程序结果,用于等待和发送信息和事件.
-
所有任务可以分成两种,一种是同步任务,一种是异步任务,
同步任务,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务,
异步任务,不进入主线程,而进入任务队列的任务.只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
下图上面浅蓝色底为同步任务,图下浅红色底为异步任务。
-
同步模式和异步模式
异步模式主线程可以运行更多的任务,提高了效率