异步加载、时间线、页面渲染、js执行机制

655 阅读12分钟

异步加载

专业的工具函数写法:命名空间方法(放在对象里)

var utils = {
	test: function(){...},
	demo: function(){...}
}
utils.test();

Element.prototype = {
    elemChildren: function(){...}
}
xxx.elemChildren();

这样做比较容易维护,知道这个函数出自于哪里

同步加载 / 阻塞加载

script标签默认是同步的,会阻塞DOM解析,所以最好写到最后面。DOM要全部解析完才能用JS操作。

实现异步的几种方法(浏览器并行加载)

link标签可以写在head里的原因就是,浏览器解析到link的时候会多开一个线程(异步),在解析css的时候是不影响DOM树的构建的。

1. defer

给js脚本添加defer属性,就可以异步加载(另开线程),IE8-IE4可用,但是他加载完之后不会立即执行,要等DOM树构建完毕之后才会执行。

<script type="text/javascript" src="tools.js" defer="defer"></script>

2. async

这种方法是W3C的标准方法,是html5新增的属性,IE9以上可用

只要加载完毕,立即执行。

<script type="text/javascript" src="tools.js" async="async"></script>

总结

  1. 异步加载的脚本,不要对文档进行操作!和DOM不要有关系!
  2. defer和async同时出现的话,除了IE,都会认async
  3. 异步加载通常用于加载工具类函数,或者不直接操作DOM / 完全和DOM无关的一些库,或者是按需加载的一些功能模块(动态加载,被触发了再加载)

3. 主动创建script标签

解析到.src就会开始下载,但不会立即执行

var s = document.createElement('script');
s.type = 'text/javascript';
s.src = 'tools.js';

添加到文档中才会开始执行

document.body.appendChild(s);

这样写不会阻塞DOM和CSS解析,但是会阻塞window.onload

那怎么样才能再onload之后加载呢?反正都是异步了

既然会阻塞onload,那么干脆就在onload里面执行

;(function(){
    function async_load(){
        var s = document.createElement('script');
        s.type = 'text/javascript';
        s.src = 'tools.js';
        document.body.appendChild(s);
    }
    
    if(window.attachEvent){
        window.attachEvent('onload', async_load);
    }else{
        windwo.addEventListener('load', async_load, false);
    }
})();

一般异步加载都会放在head里面

找到这个文档的第一个script标签,然后放到该标签前面,就可以上去了。

;(function(){
    function async_load(){
        var s = document.createElement('script'),
            oScript = document.getElementsByTagName('script')[0];
        s.type = 'text/javascript';
        s.src = 'tools.js';
        oScript.parentNode.insertBefore(s, oScript);
    }
    
    if(window.attachEvent){
        window.attachEvent('onload', async_load);
    }else{
        windwo.addEventListener('load', async_load, false);
    }
})();

同步脚本下载的时候,是不能同时构建DOM/CSS树的,会浪费资源,所以需要异步加载。

这种方法虽然外层标签是同步的,但是里面的代码进行预编译还有解析是不会有多少阻塞的,这个过程很快,只有下载才会阻塞。

这个script标签里的代码执行,s.src开始下载异步脚本,然后扔到上面执行,不会DOM构建产生多大影响,这样我们的目的就达成了。

其实放前面和放后面也多大关系,但是要挂在HTML页面里才会执行

用第三种方法的原因就是兼容性完美

异步获取方法执行:

IE8及以下script标签的没有onload事件的,所以要用onreadyStateChange

        function exec_util_with_loading_script(url, fn){
            var s = document.createElement('script'),
                oScript = document.getElementsByTagName('script')[0];
            s.type = 'text/javascript';

            // 判断脚本文件是否加载完
            if (s.readyState) { //IE
                s.onreadyStateChange = function(){
                    var state = s.readyState;

                    if (state === 'complate' || state === 'loaded') {
                        utils[fn]();
                    }
                }
            }else{ //非IE
                s.onload = function(){
                    utils[fn]();
                }
            }
            
            s.src = url;
            oScript.parentNode.insertBefore(s, oScript);
        } 

时间线

window.onload = function(){
    document.write('替换')
}

没有加window.onload之前,会在页面尾部追加内容,加window.onload之后,会把整个body里的内容都替换掉(包括script标签)

script标签也是DOM元素

会在尾部追加是因为执行到document.write的时候,DOM树还没有构建完成,DOM结构都还不知道什么的情况下,怎么清空?就只能在正常文档流下面追加

window.onload是在所有资源加载完成之后才执行的,就会把所有内容情况(不要用,浪费时间,在文档解析完之后就可以处理函数了,没有必要等资源全部加载完)

时间线:浏览器开始加载页面的那一刻到整个页面完全加载完毕,这个过程中,按顺序发生的每一件事情是总流程。

  1. 生成document对象(#document)-> 从这里开始JS就开始起作用了,JS引擎设置的DOM功能体就生效了

  2. 解析文档(从HTML的第一行开始,阅读到最后一行),并且构建DOM树

    document.readyState = ‘loading’ 【文档加载中】

  3. 遇到link,就开新的线程,异步加载css外部文件,通过style样式,加载CSSOM(css树)。这个过程和构建DOM树是同时进行的。

  4. 没有设置异步加载的script的时候,阻塞文档解析,等待JS脚本加载并且执行完成后,才会继续解析文档。

  5. 异步加载script,异步加载JS脚本,如果是async就立即执行,不阻塞解析文档(不能使用document.write,会报错,实在要加,就用window.onload)

  6. 解析文档的时候遇到img,先解析这个节点(不管什么src),有写src再创建加载线程,异步加载图片资源,不阻塞解析文档

  7. 文档解析完成

    document.readyState = ‘interactive’ 【文档解析完成】

  8. defer script JS脚本开始按照顺序执行(加载是异步加载,执行要等着文档解析完成)

  9. 触发DOMContentLoaded事件(也就是说可以监控文档什么时候解析完成,不代表文档加载完成,只是DOM结构和渲染树出来了)

    程序:同步的脚本执行阶段 -> 事件驱动阶段

    浏览器把文档解析完成,js脚本加载执行完成,用户可以进行页面交互了。

  10. async script加载并执行完毕(async和defer谁先加载完成不好说,看位置和大小),img等资源加载完毕,window对象的onload事件才触发。

    document.readyState = ‘complete’ 【文档加载完成】

console.log(document.readyState);
document.onreadystatechange = function(){
    console.log(document.readyState);
}
document.addEventListener('DOMContentLoaded', function(){
    console.log('DOMContentLoaded');
}, false)

输出:loading -> interactive(文档解析完成) -> DOMContentLoaded(文档解析完成后立即处罚法) -> complete

只要文档状态变化了就会触发这个事件处理函数,document.readyState可以通过onreadystatechange来做

这个监听的过程是由JS引擎来做的,而不是用户做的,所以不能说是基于事件驱动阶段的

事件驱动阶段一定是开发者或用户有意图设立事件和事件处理函数处理相应的程序,但是readyState是JS引擎主动监听的

async虽然是加载完之后立马执行,但不知道排哪去了,时间线的过程相当的快,尽量不要写异步

做异步也要和DOM没有关联,不要依赖其他文件,不要有需要立即触发的事件,异步有很多不确定因素

一般网络加载才用异步


关于script标签写在上面还是下面的问题

script标签也是标签,也是DOM结构,要整个DOM读完一遍了之后才会渲染,放上面和放下面都会阻塞进程的。 但是现代浏览器,为了更好的用户体验,渲染引擎会尽快的尝试渲染页面。先解析的部分,先构建DOMTree、CSSTree、renderTree。script写上面会浪费first paint(初次渲染)的时间,这样就有可能会留白。 一边解析,一般构建三个树

放上面要写DOMContentLoaded,因为script里面的代码执行的时候,有些DOM元素还没有构建,无法取到需要的元素

DOMContentLoaded不管script放哪,都会等dom解析完再触发

在jQuery中,这三种写法都代表文档解析完成,相当于DOMContentLoaded (document).ready(function(){...});(function(){...}); $(document).on('ready', function(){})


封装文档解析完毕函数

function domReady(fn){
	if (document.addEventListener) {
		document.addEventListener('DOMContentLoaded', function(){
			// 确认有监听事件之后立即删除,没有必要一直监听,会等程序走完再删除
			document.removeEventListener('DOMContentLoaded', arguments.claaee, false);
			fn();
		}, false);
	}else if (document.attachEvent) {
		// DOMContentLoaded没有句柄形式,所以要用readyState判断状态
		document.attachEvent('onreadystatechange', function(){
			// 用complete不用interactive的原因是,IE早些版本的interactive不稳定,有些时候还没有解析完,就interactive了,触发的很快
			if (readyState === 'complete') {
				document.attachEvent('onreadystatechange', arguments.claaee);
				fn();
			}
		});	
	}

	// readyState还没有complete的时候就走这
	// doScroll是IE低版本(67)的API,这个方法在解析完之前执行会一直报错 && 窗口不在iframe里,在iframe里很难监控doScroll
	if (document.documentElement.doScroll && typeof(window.frameElement) === 'undefined') {
		try{
			// 如果文档没有解析完成,报错
			document.documentElement.doScroll('left');
		}catch(e){
			// 如果报错,domReady延迟20毫秒再执行一次
			return setTimeout(arguments.callee, 20);
		}
		fn();
	}	
}

页面渲染:

浏览器组成部分:

  1. 用户界面:用户看到浏览器的样子
  2. 浏览器引擎:让浏览器运行的程序接口集合,主要是查询和操作渲染引擎
  3. 渲染引擎:解析HTML/CSS,将解析结果渲染到页面的程序
  4. 网络:进行网络请求的程序
  5. UI后端: 绘制组合选择框及对话框等基本组件的程序(alert
  6. JS解释器:解释执行js代码的程序(JS引擎
  7. 数据存储: 浏览器存储相关的程序cookie、storage

渲染

渲染:用一个特定的软件将模型(一个程序)转化为用户能看到的图像的过程

渲染引擎:内部具备一套绘图像的方法集合,渲染引擎可以让特定的方法执行,把HTML/CSS代码解析成图像显示在浏览器窗口中

渲染模式有怪异模式和标准模式

声明HTML文档

DTD:文档类型定义(document type definition) 种类:严格版本(strict)、过渡版本(transitional)、框架版本(frameset)

strict DTD: 文档结构与表现形式实现了更高的分离,页面的外观用CSS控制(比如说center标签,就不用了

transitional DTD:包含了HTML4.01版本的全部标记,从HTML过渡到XHTML(比较规范的写法)

frameset DTD:使用以框架的形式将网页分为多个文档


JS执行机制

js运行原理:js引擎是单线程的,同时可以执行异步操作(事件驱动)

进程:计算机程序关于某数据集合上的一次运行活动,是系统进行资源分配的调度的基本调度(程序的运行活动,有的软件功能比较多,就会有多个进程)

多进程:启动多个进程

单线程:进程中一个相对独立的,可调度的执行单元(同一时间,只能做一件事)

同一进程中的不同线程可以共享进程中的资源

多线程:启动一个进程,在一个进程内部启动多个线程

浏览器是多进程的:

  1. browser进程(新开页面、设置页面等)

  2. 第三方插件进程

  3. GPU进程(GPU硬件加速,负责3D渲染、建模以及运动)

  4. 浏览器渲染引擎进程(浏览器内核,多线程)

    • js引擎线程(单线程):解释执行js代码
    • GUI线程:渲染用户界面,和js引擎线程互斥,先解释执行代码,再绘制,如果js卡死了,就不会再绘制页面。
    • http网络请求线程(AJAX)
    • 定时器触发线程
    • 浏览器事件处理线程(onclick, addEventListener)

    后面三个线程是处理异步事件的,等交互事件发生之后,把事件放到事件队列中,统称为webAPIs(事件驱动)


为什么js引擎一定是单线程呢?

js的诞生就是为了和用户互动,必然就要操作DOM元素,多线程会产生DOM冲突,两个程序都操作同一个dom的话,dom听谁的?

单线程如果要处理的数据量大的话怎么办?

解决方法:

  1. SSR(服务端渲染技术):让后端做计算,前端做渲染就可以了。
  2. webworker(H5 API):申请在js引擎下面开启一个子线程,是浏览器开辟的,不是js引擎开辟的。专门用来计算,不能访问DOM。

计算量不大但也比较多怎么办? -> 异步(同时处理) 单线程是通过事件驱动(webAPIs)的方式模拟异步的,webAPIs是和js引擎线程独立分开的,不用的环境基于事件驱动的环境可能对不一样。


机制

执行栈 / 调用栈(Call Stack主线程)就是个有底的兜,所有任务代码都会进入到执行栈中,先进后出。

同步任务就会按照顺序执行。

异步任务:

异步代码只有webAPIs的三种方式,通常以回调函数的形式出现(setInterval),但不是所有的回调函数都是异步代码,也就是同步回调(比如sort、filter)。

Callback / Event / Task Quene (事件 / 异步 / 回调 / 任务队列)

执行栈调用webAPIs,将异步代码注册相应的回调,注册完了之后,当事件触发,将异步函数推入Callback Queue,再通过事件循环(Event Loop),推入Call Stack

例:

//同步代码执行,执行完之后移除,打印Hi
console.log('Hi');
//调用相应的线程,注册回调,将回调挂起,并没有执行,要等事件被触发(Bye打印完),触发之后进入Event, Queue(事件队列中),然后根据轮转的机制,主线程抽取事件队列中的任务,打印值
setTimeout(function(){
    console.log('cb1');
}, 0)
console.log('Bye');

结果是Hi Bye cb1

过程:

函数执行扔到相应的执行栈中
函数执行的时候,一看是异步代码,就调用相应的异步线程(定时器触发线程),注册对应的回调函数(名称为timer[假设的名称]),然后就抛到一边了,等待事件被触发。

同步代码实现异步操作的原因就在这里,执行setTimerout的时候,并没有阻塞当前的代码,而是把setTimerout扔到对应的web APIs中。

定时器函数执行完了之后会移除,webAPIs里的timer等待事件被触发。

同步代码执行完毕

H5规定最小延迟4毫秒,写0也没用

30毫秒以上才会有连贯的效果

事件循环会看任务队列(Call Stack)中有没有事件还没有执行完,然后看事件队列(Callback Queue)有没有任务,有事件任务且任务队列中没有其他任务,就会把事件任务推入到任务队列中。

事件轮循是一个一直监听的过程,一会看看任务队列,一会看看事件队列,反复查看(哨兵)

setTimeout设置的延迟时间会再加上因为同步代码执行堵塞的时间,也就是说事件会更多一些(时间偏差)