浅谈页面从url到页面加载完成(第二篇笔记)
前言
我是小白,励志要做技术男神的帅逼,目前住在南京,做了快3年前端工程师,我的第二篇笔记。
本笔记参考及引用到的文章链接(感谢技术大佬们的分享)
前置知识
- js堆栈概念
- 垃圾回收机制
- 执行上下文(执行环境)-> 代码的执行顺序是自上而下的
- VO -> 变量对象(variable object)、AO -> (活动对象(activation object))
- 作用域及作用域链
- !DOCTYPE标签:声明浏览器关于页面使用哪个 HTML 版本进行编写的指令. 在 HTML 4.01 中,<!DOCTYPE> 声明引用 DTD,因为 HTML 4.01 基于 SGML。DTD 规定了标记语言的规则,这样浏览器才能正确地呈现内容。HTML5 不基于 SGML,所以不需要引用 DTD。
- html需要等head中所有的js和css加载完成后才会开始绘制,但是html不需要等待放在body最后的js下载执行就会开始绘制。
- link 加载css是异步的 他不会阻塞加载
js堆栈概念
基本数据类型
- Undefined、Null、Boolean、String、Number、Symbol都是直接按值直接存在栈中,每种类型的数据占用的内存空间大小都是固定的,并且由系统自动分配自动释放
引用数据类型
- Object,Array,Function这样的数据存在堆内存中,但是数据指针是存放在栈内存中的,当我们访问引用数据时,先从栈内存中获取指针,通过指针在堆内存中找到数据
垃圾回收机制
JavaScript引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问的对象。
// user 具有对象的引用
// 对象user是个引用数据类型 存储的是一个地址 指向 存放 name:"john" 的堆
let user = {
name: "John"
};
// 如果 user 的值被覆盖地址没了,那它 指向 存放 name:"john" 的堆,就没有办法访问了,没有对它的引用。垃圾回收器将丢弃 John 数据并释放内存。
user = null
执行上下文(执行环境)
当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被事先提出来(变量提升),代码从上往下开始执行,就叫做执行上下文。
- 单线程,在主进程上运行(JavaScript语言的一大特点就是单线程)
- 同步执行,从上往下按顺序执行
- 全局上下文只有一个,浏览器关闭时会被弹出栈
- 函数的执行上下文没有数目限制
- 函数每被调用一次,都会产生一个新的执行上下文环境
-
它包含三个部分:
- 变量对象(VO) - 活动对象(AO)
- 作用域链(词法作用域)
- this指向
-
它的类型:
- 全局执行上下文(global EC)
- 函数执行上下文(callee)
- eval执行上下文
-
代码执行过程:
- 创建 全局上下文 (global EC)
- 全局执行上下文(global EC)逐行自上而下执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层
- 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起
- 函数执行完后,callee 被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行
// 1.进入全局上下文环境
var a = 10;
var fn;
var bar = function (x) {
var b = 20;
fn(x + b); // 3.进入fn上下文环境
}
fn = function(y){
var c = 20;
console.log(y + c);
}
bar(5); // 2.进入bar上下文环境



上图为执行上下文(执行环境)的生命周期
VO -> 变量对象(variable object)、AO -> (活动对象(activation object))
看完上面的执行上下文(执行环境)我们知道函数每被调用一次就会产生一个新的执行环境,而VO(变量对象)都会被分配一个这个新的执行上下文(执行环境)。 在函数上下文中,VO是不能直接访问的,此时由活动对象AO继续扮演VO的角色
- VO(变量对象)就是当前执行上下文(执行环境)中的所有形参,所有函数声明,所有变量声明(这里是依次创建的)
- AO(活动对象)当当前执行上下文(执行环境)中的函数被执行时VO(变量对象)会转变成AO(活动对象)让其可以被访问
// 全局执行上下文(执行环境)VO(变量对象): a fa fn
var a = 1;
function fa (x) {
// 局部执行上下文(执行环境) VO(变量对象): x b fa1
var b = 20;
function fa1 () {}
}
function fn (x) {
// 局部执行上下文(执行环境) VO(变量对象): x b
var b = 20;
a = 12
}
// 在此之前 所有 执行上下文(执行环境)都为 VO(变量对象),然而当下面被执行时
fn(1) // 执行上下文(执行环境)的生命周期 走到了 执行阶段 fn 的 VO(变量对象) 将变成 AO(活动对象) 访问,当其执行完毕执行环境就开始被回收了
console.log(a) // 12
作用域及作用域链
- 作用域是指程序中定义变量的区域, 也可以把当前环境的VO(变量对象 )就看做作用域。 每个执行上下文(执行环境)的作用域链由当前环境的VO(变量对象)及父级环境的作用域链构成。
- 作用域可分为 块级作用域 和 函数作用域
- 特性:
- 声明提前: 一个声明在函数体内都是可见的, 函数优先于变量
- 非匿名自执行函数,函数变量为 只读 状态,无法修改
let foo = function() { console.log(1) };
(function foo() {
foo = 10 // 由于foo在函数中只为可读,因此赋值无效
console.log(foo)
}())
// 结果打印: ƒ foo() { foo = 10 ; console.log(foo) }
- 作用域链
- 作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数。
- 由两部分组成:
- [[scope]]属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]和AO
- AO: 自身活动对象
如此 [[scopr]]包含[[scope]],便自上而下形成一条 链式作用域。
重点知识
- head中如果存在外部js脚本并加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
知识点
- html,css,js加载顺序
- js加载顺序(包括变量提升知识点)
- 渲染树(Render Tree)的构建过程
- 重排、重绘
- 从浏览器接收url到开启网络请求线程
html,css,js加载顺序
js放在head中会立即执行,阻塞后续的资源下载与执行。因为js有可能会修改dom,如果不阻塞后续的资源下载,dom的操作顺序不可控。
- 浏览器一边下载HTML网页,一边开始解析解析过程中,发现script标签暂停解析,网页渲染的控制权转交给JavaScript引擎,如果script标签引用了外部脚本,就下载该脚本,否则就直接执行,执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页
- js的执行依赖前面的样式。即只有前面的样式全部下载完成后才会执行js,但是此时外链css和外链js是并行下载的,所以css应放到js前。
- 外链的js脚本如果含有defer="true"属性,将会被并行下载js脚本(相当于浏览器继续往下解析HTML网页,同时并行下载script标签中的外部脚本),到页面全部加载完成后才会再按顺序执行下载的js脚本。
- 外链的js如果含有async="true"属性,将不会依赖于任何js和css的执行,此js下载完成后立刻执行,不保证按照书写的顺序执行。因为async="true"属性会告诉浏览器,js不会修改dom和样式,故不必依赖其它的js和css(相当于当解析过程中,发现带有async属性的script标签,浏览器继续往下解析HTML网页,同时并行下载带有async属性的script标签中的外部脚本,带有async属性的js脚本下载完成后,浏览器暂停解析HTML网页,开始执行下载的js脚本,直到脚本执行完毕,浏览器恢复解析HTML网页)。
- 如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定
总结:浏览器先读取html进行解析,解析过程中遇到外链css自上而下加载,遇到默认的script 标签 先进行阻塞加载,当加载完后进行继续渲染。
js加载顺序
- 先预处理后执行
- 预处理会跳过执行语句,只处理声明语句,同样也是按从上到下按顺序进行的。包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。 即使声明是在调用的下方进行的,但浏览器仍然先声明再调用(执行),这个现象叫做“提升”。所以,即便一个函数的声明在下方,在前面仍然可以正常执行这个函数。
- js作用域链变量访问规则:
- 当前作用域内存在要访问的变量时,则使用当前作用域中的变量。
- 当前作用域中不存在要访问的变量时,则会到上一层作用域中寻找,直到全局作用域。
- 完成预处理之后,JavaScript代码会从上到下按顺序执行逻辑操作和函数的调用。
上述结论
var a = 1;
function func(){
console.log(1, a);
var a = 2;
console.log(2, a);
}
function func1 (a, func2){
console.log(3, a , func2);
var a = 2;
console.log(4, a , func2);
function func2 () {
console.log('func1下的func2');
}
}
function func2 () {
console.log('全局func2');
}
func();
func1(3, 6);
console.log(5, a , func2);
// 上述代码,先进行预处理,想想 当前执行上下文(执行环境)中的所有形参,所有函数声明,所有变量声明(这里是依次创建的)
// 那预处理结果为(执行上下文(执行环境)生命周期的创建阶段):
创建阶段:注意以下都是创建阶段
全局执行上下文(执行环境):{
VO: {
func: function,
func1: function,
func2: function,
a:undefined
},
// 发现func函数被调用立马 新建了执行上下文
新建的执行上下文(执行环境)func: {
VO: {
// 1.没有形参,2. 没有函数声明
a:undefined // 3. 变量声明
},
},
// 发现func1函数被调用立马 新建了执行上下文
// 注意:所有形参,所有函数声明,所有变量声明(这里是依次创建的)
新建的执行上下文(执行环境)func1: {
VO: {
arguments: {
参数1 a: 3
参数2 func2: 6
},// 1.形参
func2: function,2. 函数声明
a:undefined 3. 变量声明
}
},
}
创建阶段结束
在看执行阶段之前 我们先看看 那些已经被预处理了
var a
function func(){
var a
console.log(1, a);
a = 2;
console.log(2, a);
}
function func1 (a, func2){
function func2 () {
console.log('func1下的func2');
}
var a;
console.log(3, a , func2); // 这个里面的是形参 并 非生成的a 变量
a = 2;
console.log(4, a , func2); // 这个里面的是生成的a 变量
}
function func2 () {
console.log('全局func2');
}
a = 1
func();
func1(3, 6);
console.log(5, a , func2);
执行阶段:注意以下都是执行阶段 新建的执行上下文(执行环境) VO 被激活 成为 AO
全局执行上下文(执行环境):{
VO: {
func: function,
func1: function,
func2: function,
a: 1 // 1. 赋值被执行了
},
新建的执行上下文(执行环境)func: {
AO: {
a:undefined => 2 // 2.执行 console.log(1, a); => a = 2; => console.log(2, a);
},
},
新建的执行上下文(执行环境)func1: {
AO: {
arguments: {
参数1 a: 3
参数2 func2: 6
},
func2: function,
a:undefined => 2 // 3. 执行 形参a console.log(3, a , func2); => a = 2; => 变量a console.log(4, a , func2);
}
},
}
那执行结果就清楚了,
第一个console.log(1, a); // 1 undefined
第二个console.log(2, a); // 2 2
第三个console.log(3, a, func2); // 3 3 ƒ func2 () { console.log('func1下的func2'); }
第四个console.log(4, a, func2); // 4 2 ƒ func2 () {console.log('func1下的func2'); }
第五个console.log(5, a, func2); // 5 1 ƒ func2 () {console.log('全局func2'); }
渲染树(Render Tree)的构建过程
渲染一个网页,浏览器需要完成的步骤:
- 处理 HTML 标记并构建 DOM 树。
- 处理 CSS 标记并构建 CSSOM 树。
- 将 DOM 与 CSSOM 合并成一个渲染树。
- 根据渲染树来布局(Layout),以计算每个节点的几何信息。
- 将各个节点绘制到屏幕上。
- 如果 DOM 或 CSSOM 被修改,您只能再执行一遍以上所有步骤,以确定哪些像素需要在屏幕上进行重新渲染。
重排、重绘
- 重排: DOM元素改变影响了元素的几何属性,如宽高
- 重绘: 元素的改变不影响几何属性,如颜色
上述4个知识点总结
浏览器先读取html进行解析并构建DOM树,解析过程中遇到外链css自上而下加载(解析CSS时并构建CSSOM树),遇到默认的script标签,则进行阻塞加载(但这时页面上渲染树还没有构建,要是操作渲染树就会报错),当加载完后进行将DOM与CSSOM合并成一个渲染树,然后根据渲染树来布局(Layout),以计算每个节点的几何信息后绘制到屏幕上,这时js进行DOM与CSSOM的操作就会引起DOM或CSSOM被修改,如果DOM或CSSOM被修改,只能再执行一遍以上所有步骤,以确定哪些像素需要在屏幕上进行重新渲染。
从浏览器接收url到服务器返回
首先我们要知道 浏览器 每开一个新的页面 就会单独开开劈一个进程空间(也就是说,浏览器是多进程的),这个进程空间是多线程的。这个进程包含的主要线程(都是单线程):
- GUI线程
- JS引擎线程
- 事件触发线程
- 定时器线程
- 网络请求线程(异步HTTP请求线程)
URL:http(协议头):主机域名或IP地址:端口号/目录路径/?参数=1
从客户端应用层(dns,http)的发送http请求(网络请求线程,被开了一个线程),到传输层(tcp,udp)通过三次握手建立tcp/ip连接,再到网络层(IP,ARP)的ip寻址,再到数据链路层(PPP)的封装成帧,最后到物理层(比特流)的利用物理介质传输。
服务器接收到请求 然后 一系列操作把该返回的东西都由 http报文 带回给 客户端(之后就是 上面的知识点的流程了)
简述: 域名解析 => 发起TCP3次握手 => 建立TCP连接 后 发起http请求 => 服务器端响应http请求,浏览器得到html代码 => 浏览器解析html代码,并请求html代码中的资源 => 浏览器对页面进行渲染呈现给用户
没考虑 其他请求
可总结出的页面优化
- css分块
首页的css独立,其余的css需要动态加载,因为html的绘制会被css阻塞,这样可以减少首次进入时的白屏时间。
- js代码放在body标签的最后
防止浏览器呈现“假死”状态
- 渲染优化
页面优化思路无非是从dom节点操作入手,尽量减少dom操作,尽可能在js端处理。
使元素脱离文档流, 对其应用多重改变, 把元素带回文档中。四种基本方法:
- 隐藏元素,改完了重新显示
- 使用文档片段(var fragment = document.createDocumentFragment(),Fragment的中文意思就是片段,文档片段是个轻量级的document对象,设计初衷就是用来完成这类任务----更新和移动节点)[推荐使用]
- 拷贝元素到一个脱离文档的节点上,修改完再替换回去
- 浮动元素, 尤其适用于动画, 所以叫脱离动画流[推荐使用]
结语
以上是我的第二篇笔记,本人前端小白一只,如果写的不好请各位大佬指正,俺想再学习到更深知识,希望和各位大佬交朋友,希望我的笔记对您提供舒适的观看体验。
转载自:https://juejin.cn/post/6844904138724098056