一、进程和线程
进程是资源分配的最小单位,线程是CPU调度的最小单位
(一)进程
一个进程就是一个程序的运行实例,每启动一个应用程序,操作系统都会为此程序创建一块内存,用来存放代码、数据数据、一个执行任务的主线程,我们把这样的一个运行环境叫进程。
一个进程关闭,操作系统则会回收为该进程分配的内存空间
(二)线程
线程是依附于进程的,而进程中使用多线程并行处理能提升运算效率。
进程与线程之间的关系: (进程是火车,线程是每节车厢)
进程中的某一线程执行出错,都会导致整个进程的崩溃线程之间共享进程中的公共数据。当一个进程关闭之后,操作系统会回收进程所占用的内存。进程之间的内容相互隔离(三)、浏览器多进程时代
一个浏览器主进程: 主要负责显示渲染进程生成的页面图层、用户交互、子管理进程,提供存储等功能一个GPU进程 :负责图形处理一个网络进程:负责网络资源的下载多个渲染进程(浏览器的核心部分,一般称为浏览器内核):默认情况下,每个tab页面一个进程,互不影响-- 特殊情况1:如多个空白tab会合并成一个进程;-- 特殊情况2:从一个标签页中打开了另一个新标签页,当新标签页和当前标签页属于同一站点的话,那么新标签页会复用当前标签页的渲染进程核心任务是将 HTML、CSS 和 JavaScript 转换为网页图层,通知浏览器主线程进行界面显示;渲染进程都是运行在沙箱模式下渲染进程中包含以下线程:(1).GUI渲染线程(2) Javascript引擎线程(3) 事件触发线程(归属于浏览器而不是JS引擎)(4)定时触发器线程(5)异步http请求线程(6)合成线程(7)IO线程:处理和其他进程进行通信GUI渲染线程与JS引擎线程是互斥的,不能一并执行多个插件进程:负责页面中的插件运行;也是运行在沙箱模式下各进程之间通过 IPC 来通信
二、浏览器渲染流程
第一步,解析:主线程开始解析HTML
浏览器收到HTML,HTML解析器开始解析HTML,生成DOM Tree,并保存在浏览器内存中-- 同时开启一个预解析线程,用来分析 HTML 文件中包含的Javascript、 CSS 、Img等资源,通知网络进程提前加载这些资源解析遇到CSS(style、行内、link),CSS解析器开始对CSS进行解析,生成CSSOM( 即styleSheets)样式计算:(css样式的继承、层叠等规则)转换样式中的属性值,如color: red; => color: rgb(255, 0, 0)计算出DOM每个节点的具体样式遇到 <script> ,渲染线程停止解析剩余的 HTML 文档,等待Javascript 资源加载,Javascript引擎执行脚本完成后,HTML再继续解析JavaScript 脚本是依赖样式表的,会先等CSS文件加载并解析完成再执行,因此Javascript对元素的样式是最终生效的
javascript 会阻塞HTML解析和页面渲染
css解析和HTML解析并行,不会阻塞HTML解析,但是会阻塞页面渲染(但是Javascript执行,会导致CSS的解析增加HTML解析的时间)
第二步,生成Layout Tree(布局树)
根据DOM和styleSheets生成LayoutTree布局树(渲染树),所有不可见的元素会被忽略,如head标签 , display:none的元素,script标签等
第三步,布局计算
渲染引擎计算出布局树中各元素的几何位置,并将计算结果保存在布局树中,布局阶段的输出就是我们常说的盒子模型,它会精确地捕获每个元素在屏幕内的确切位置与大小第四步,分层,生成图层树
渲染引擎根据布局树生成图层树,
第五步, 绘制
主线程根据图层树生成绘制列表,交给合成线程合成线程对图层进行分割,生成大小固定的图块合成线程按照视口附近的图块来优先交给GPU进程第六步,光栅化,生成位图
GPU进程根据不同图块生成位图,还给合成线程
第七步,合成
合成线程收到各图块位图之后,发出合成命令,交给浏览器主进程第八步,显示界面
浏览器主进程然后进行界面显示渲染流程中的特殊情况:
1. 重排(回流):
指修改了元素几何属性,如位置、尺寸、内容、结构等变化,引发元素几何位置变化,浏览器需要重新计算样式、构建布局树,开始之后的一系列子阶段,这个过程就叫重排。
重排需要更新完整的渲染流水线,所以开销也是最大的。
触发重排的情况:(Javascript操作DOM,引发不同渲染流水线重新工作)
添加或删除可见的DOM元素元素位置改变元素尺寸改变元素内容改变改变字体大小会引发回流页面渲染器初始化浏览器窗口大小发生改变当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括(1) offset(Top/Left/Width/Height)(2) scroll(Top/Left/Width/Height)(3) cilent(Top/Left/Width/Height)(4) width,height(5) 调用了getComputedStyle()或者IE的currentStyle2. 重绘:
指修改了元素的外观样式,不会引起几何位置变化,直接入绘制阶段,生成绘制列表,然后执行之后的一系列子阶段,这个过程就叫重绘。如背景颜色、边框颜色,文字颜色等
重绘省去了布局和分层阶段,所以执行效率会比重排操作要高一些。重排必然带来重绘,但是重绘未必带来重排
3. 直接合成:
指更改一个既不要布局也不要绘制的属性,直接分割图块阶段,然后交给浏览器主进程并不线上显示,这个过程叫做直接合成。如 transform:translate(100px, 100px)
相对于重绘和重排,直接合成能大大提升效率
减少重排(回流)、重绘, 方法:
多次dom 操作合成一次,批量操作,例如 createDocumentFragment,vue框架虚拟DOM和diff算法使用 class 操作样式,而不是频繁操作 style处理动画时,使用will-change和transform 做优化在css中使用will-change,渲染引擎会将该元素单独生成一个图层三、JavaScript执行机制
(一)JavaScript代码执行流程
第一步,代码编译:JavaScript 引擎对代码进行编译,并保存在内存中
编译结果为两部分:执行上下文、可执行代码
showName();//函数showName被执行 console.log(myname);//undefined var myname = '小白' function showName() { console.log('我是小白'); }
编译时的执行上下文如下:(变量环境部分)
{ showName: xxx, //showName 函数在堆内存的引用地址 myname: undefined }
可执行上下文如下:
showName(); console.log(myname);//undefined myname = '小白'执行上下文:是 JavaScript 执行一段代码时的运行环境每个执行上下文包含以下几个部分:变量环境词法环境外部环境,即当前执行上下文中变量的外部引用,用来指向外部的执行上下文,也称为 outerthis,this的指向在于当前函数的调用方式-直接调用指向全局对象window (严格模式下则是undefined)-通过对象调用,this指向该对象-通过apply、call、bind等方法调用则指向第一个参数对象-箭头函数中的this指向外层函数的this(解析箭头函数不会创建执行上下文)
let userInfo = { userName: "小白", age: 18, sayHello: function () { setTimeout(function () { console.log(`${this.userName},你好`) //undefined }, 100) } } userInfo.sayHello()
修改一个函数this指向的方法:
缓存外部的this, 如 var _this = this;使用箭头函数使用app、call、bind改变this指向第二步,执行可执行代码
问题:
var变量提升编译时变量声明提升,并初始化值为undefind,函数声明提升同时声明了多个相同名字的函数,后声明的会覆盖前面声明的函数函数声明的优先级高于变量提升,变量名和函数声明的名字相同时,采用函数名解决: 引入let、const、块级作用域
(二)函数执行(调用)过程
执行上下文栈:用来管理执行上下文,后进先出全局执行上下文:执行全局代码生成一个全局执行上下文,仅有一个,伴随页面的整个生存周期函数执行上下文:执行每个函数会生成一个函数执行上下文,可以有多个, 当函数执行结束,该函数的执行上下文会被销毁一段代码解析完成,即执行上下文创建完成,就立即执行可执行代码
var a = 2 function add(b,c){ return b+c } function addAll(b,c){ var d = 10 result = add(b,c) return a+result+d } addAll(3,6)
第一步,解析全局代码,创建全局执行上下文,压入调用栈,并全局的执行可执行代码
第二步,执行到addAll调用时,生成addAll函数的执行上下文,压入上下文,并执行addAll函数内部的可执行代码
第三步,执行到add 函数调用,生成add 函数的执行上下文,压入调用栈
执行上下文栈.png
执行add 函数内部的可执行代码,return 结果,然后add函数执行上下文销毁,弹出调用栈
第四部,执行addAll后续可执行代码,return 结果,addAll函数上下文销毁,弹出调用栈,最后只剩下全局执行上下文,伴随页面整个生命周期
问题: 栈溢出(递归函数)
(三)作用域、作用域链、闭包
1. 作用域:是指变量和函数可以被访问的范围
全局作用域:代码中任何地方都能被访问,即全局执行上下文中的变量和函数能在任何地方被访问,生命周期伴随着页面的生命周期。函数作用域:函数内部定义的变量或函数只能在函数内部被访问,函数执行结束之后,函数内部定义的变量会随着函数执行上下文一起销毁(闭包除外)块级作用域 { }var 、 let、const的区别:
var:-- 在javascript解析时, 声明和初始化提升,声明之前访问不报错,值为undefined;-- 存放在执行上下文中的变量环境中-- 可以多次声明同一个变量,后一个值会覆盖之前的值;-- 不支持块级作用域let :-- 用来声明一个变量,在解析时,声明会提升,但是初始化不会提升,声明之前访问报错;-- 存放在执行上下中的词法环境中-- 同一作用域内不能多次声明;-- 支持块级作用域const :-- 用来声明一个常量,不能再次修改--声明会提升,但是初始化不会提升,声明之前访问报错;-- 存放在执行上下中的词法环境中-- 同一作用域内不能多次声明;-- 支持块级作用域function foo(){ var a = 1 let b = 2 { let b = 3 var c = 4 let d = 5 console.log(a); //1 console.log(b); //3 } console.log(b) ;//2 console.log(c); //4 console.log(d); //报错:d is not defined } foo()
2. 作用域链:变量查找沿着各作用域一层层向外部引用指向的执行上下文查找,形成一个链条,即作用域链条
函数的作用域由词法作用域决定词法作用域:是指作用域是函数声明的位置来决定的,和函数怎么调用无关
3. 闭包:
当函数执行完毕时,函数体内的定义的变量会随着函数执行上下文立即销毁,但是当外部函数包含内部函数,且内部函数使用了外部函数中定义的变量,这些变量就不会销毁,仍然保存在内存,这些变量和内部函数就形成了闭包
闭包的形成条件:
外部函数里有内部函数内部函数中使用了外部函数中定义的变量function foo() { var myName = "小白"; var age = 18; function sayHello(){ console.log (`你好,我的名字是:${myName},今年${age}`) } return sayHello; } let hello = foo(); hello() // myName和age就是foo函数的闭包闭包形成原因:Javascript在代码编译阶段,遇到内部函数 时,JavaScript 引擎会对内部函数做一次快速的词法扫描,发现该内部函数引用了外部函数定义的变量,于是在堆空间创建换一个“closure”的对象,用来保存内部函数使用的变量,这个closure对象就是闭包闭包何时回收?引用闭包的函数是全局变量时,闭包则会一直保存在内存中,直到页面关闭引用闭包的内部函数局部变量时,内部函数执行结束后,内部函数就会立即销毁,下次JavaScript 引擎的执行垃圾回收时,判断不再使用,则销毁闭包,回收内存
问题:内存泄露( 该回收的内存未被及时回收 )
(四)Javascrip的垃圾回收机制
1. Javascript的内存机制
栈内存: 存储基本类型数据(调用栈,执行上下文栈)变量是引用类型时,存储的是引用类型的引用地址(编号)堆内存:存储引用类型数据代码空间:存储可执行代码2. Javascript的垃圾回收机制
数据被使用之后,不再需要了,就称为垃圾数据,垃圾数据要及时销毁,释放内存空间,否则会内存泄漏。
手动回收,如设置变量为null自动回收(1)栈内存回收
当Javascript代码执行时,记录当前执行状态的指针(称为 ESP),指向当前执行上下文的指针,当前函数代码之前完毕,指针下移指向下一个要执行的函数执行上下文,当前执行上下文弹出调用栈进行销毁,这个过程就是该函数栈内存回收的过程
function foo(){ var a = 1 var b = {name:"极客邦"} function showName(){ var c = 2 var d = {name:"极客时间"} } showName() } foo()
调用栈.png
(2)堆内存回收垃圾回收器:
主垃圾回收器: 负责回收生存时间长的垃圾数据(老生代垃圾数据)副垃圾回收器:负责回收生存时间短的垃圾数据(新生代垃圾数据)第一步,标记堆内存中活动对象和非活动对象
活动对象:还在使用的数据非活动对象:垃圾数据第二步,回收非活动数据所占据的内存在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象
第三步,做内存整理
(五)浏览器的事件循环机制
每个渲染进程都有一个主线程,处理以下事件:
渲染事件(如解析 DOM、计算布局、绘制)用户交互事件(如鼠标点击、滚动页面、放大缩小等)JavaScript 脚本执行事件网络请求完成、文件读写完成事件消息队列和循环机制保证了页面有条不紊地运行
1. 任务队列:是一种数据结构,用来放要执行的任务,先进先出
同步任务:直接进入主线程执行的任务,只有前一个任务执行完毕,才能执行后一个任务异步任务:以回调函数实现,先在其他的任务队列中排队,等待同步任务执行完成,该任务才会进入主线程执行,分为宏任务、微任务
宏任务队列:宏任务执行队列,回调函数里要执行的任务
微任务队列:JavaScript 执行一段脚本,V8 引擎会首先创建一个全局执行上下文,同时也会创建一个专为V8 引擎内部使用的微任务队列
(1)宏任务:宿主环境即浏览器分配的任务
宏任务 主要有以下几种:
setInterval、setTimeout-- setTimeout回调函数的真正执行时间>=设定时间,原因是受消息队列中其他任务执行时间的影响XMLHttpRequest(2)微任务:JavaScript 引擎发起的任务,执行时机为当前宏任务结束之前
Javascript脚本执行本身就也是一个宏任务,宏任务中又包含同步任务、微任务、宏任务
console.log(1); setTimeout(()=>{ console.log(3); Promise.resolve(4).then((data) => { console.log(data) }) setTimeout(() =>{ console.log(5) },0) }, 0) Promise.resolve(2).then((data) => { console.log(data) }) //执行结果:1, 2, 3,5
微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列微任务早于宏任务执行微任务的执行时长会影响到当前宏任务的时长
微任务主要有:
MotutaionObserverPromise(1) Promise的三种状态pending(待执行状态)、fulfilled(执行成功状态)、rejected(执行失败状态)(2)执行过状态不可逆,不会再变要么pending ->fulfilled要么pending -> rejected
(3)Promise实现原理:- 回调函数延迟绑定(微任务)- 回调函数返回值穿透,then回调函数中的返回值,可以穿透到最外层- 错误“冒泡”,通过链式调用then、catch,不论在哪一层出错,都会“冒泡”至catch
//封装一个函数,简单模拟promise function MyPomise(executor) { let _this = this; let _onResolve = null; this.then = function (onResolve) { _onResolve = onResolve; } this.resolve = function (value) { //此处用setTimeout模拟延迟绑定回调任务,也是微任务出现的原因 setTimeout(() => { _onResolve(value) }, 0) } executor(this.resolve, this.reject); } let demo = new MyPomise((resolve, reject) => { resolve(200) }) demo.then((data) => { console.log(data) })
(4)Promise.resolve(value):返回一个以给定值解析后的Promise对象
Promise.resolve(value)方法的参数分成四种情况:
-- 参数是一个 Promise对象的实例 ,直接返回这个 实例
-- 参数是一个thenable对象(即带有then方法),Promise.resolve()返回的是一个执行then方法之后的Promise对象,并且采用执行之后的状态
let thenable = { then: function(resolve, reject) { resolve(200) } } let p1 = Promise.resolve(thenable); //200,因为p1已经是fulfilled状态,因此直接then,可以获取到返回值 p1.then((data) => { console.log(data) })
-- 参数是一个普通值或对象,则直接返回新的 Promise 对象,状态为fulfilled(值为参数本身)
-- 参数为空,直接返回一个fulfilled状态的 Promise 对象,(值为undefined)
(5)链式调用时,then回调函数执行成功,返回的是一个fulfilled状态的promise,会进入后面的thenthen执行失败,返回的是一个rejected的promise,会进入后面的catchcatch回调函数执行成功,返回的也是一个fulfilled状态的promise,进入后面的thencatch执行失败,返回的是一个rejected的promise,进入后面的catch
async/awaitasync/await出现的原因:Promise 的编程模型采用链式回调方式,充满大量的then函数,语义化方面存在缺陷async/await的原理:使用了Promise在Promise基础配合生成器函数和协程,以同步代码编程的风格来实现异步回调async function foo() { console.log(1); let a = await 100; // await之后的代码相当于then函数里的代码 console.log(a); console.log(2); } console.log(0); foo(); console.log(3); //执行顺序:0,1,3,100,2
生成器函数:是一个带星号函数,是可以暂停执行和恢复执行的执行器:执行生成器函数的函数,则成为执行器携程: 是一种比线程更加轻量级的存在,
一个线程上可以存在多个协程,但是同时只能执行一个协程,因此要在协程之间进行切换如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程function* genDemo() { console.log("开始执行第一段"); yield 'generator 1';// 遇到yield 关键字,JavaScript 引擎会暂停该函数的执行,并将关键字后面的内容返回给外部,外部函数可以通过next()恢复继续执行 console.log("开始执行第二段"); yield 'generator 2; } console.log('main 0') let gen = genDemo(); //创建了一个gen协程,但是并没有执行 console.log(gen.next().value) ; //generator 1 console.log('main 1') console.log(gen.next().value); //generator 2 console.log('main 2')
2. 事件循环机制
参考文档https://www.jianshu.com/p/12b9f73c5a4f/,这个应该是讲得比较详细的
栈的概念理解(3种):
一段代码的运行环境,后进先出(执行上下文栈)存放数据的一种内存区域,栈空间、堆空间栈空间是有结构的,每个区块按照一定次序存放堆空间没有结构的,数据可以任意存放。栈空间的寻址速度要快于堆空间可执行代码执行方式,执行栈(调用栈),先进先出事件循环执行过程:
setTimeout(function () { console.log('timeout1'); },0) new Promise(function (resolve) { console.log('promise1'); resolve(100) }).then(() => { console.log('then1'); }) new Promise(function (resolve) { console.log('promise2'); resolve(200) }).then(() => { console.log('then2'); }) setTimeout(function () { console.log('timeout2'); },0) console.log('global1');第一步,开始执行Javascript脚本,进入宏任务队列,因为只有一个script(整体代码)任务,直接开始执行第二步,遇到setTimeout,setTimeout为一个宏任务,异步处理,等待时机成熟(100ms之后),timeout1加入宏任务队列第三步,遇到Promise,Promise本身是同步任务,promise1, resolve(100),立即放到执行栈( 按顺序执行 ),then才是回调异步函数,异步处理,处理完成后,then1加入微任务队列(异步任务执行加入微任务队中的时间是任务异步处理完成的时间顺序,不是在代码中的上下位置顺序)第四部,遇到第二个Promise,promise2, resolve(200),立即加入执行栈,then2加入微任务队列第五步,遇到第二个setTimeout,timeout2加入宏任务队列至此,调用栈为空,Javascript宏任务中的同步任务函数全部执行完毕
第六步,然后去微任务队列查看可执行的微任务,then1加入执行栈执行,执行完成,执行栈为空,再去微任务队列查看可执行的微任务,加入执行栈执行,反复循环,直到微任务队列为空第七步,查看宏任务队列可执行宏任务,timeout2执行完成时间早于timeout,因此先进入执行栈执行,反复循环,直到宏任务任务队列为空任务全部执行完毕,调用栈为空四、浏览器中的页面
页面的生命周期:
加载阶段更新阶段(交互阶段)销毁阶段(一)页面优化:
从页面的生命周期方向思考:
1. 加载阶段:如何让页面渲染快?
关键资源(核心资源):阻塞页面首次渲染的资源称为页面的关键资源,HTML、CSS、Javascript
减少关键资源个数,减少请求次数减小关键资源大小,提高资源加载速度传输关键资源需要多少个 RTT(Round Trip Time)--TCP协议传输资源时,是将资源分成一个个数据包(一般为14KB 左右),来回多次进行传输--RTT ,是指客户端开始发送数据开始,到收到服务器端接收确认信息所经历的时间具体优化方法:
(1)压缩HTML文件,移除 不必要注释(2)合并并压缩CSS 、JavaScript等文件 ,script 标签加上 async 或 defer属性(3)避免使用table布局(4)缓存(第二次请求命中缓存则直接读取缓存)
2. 更新阶段(交互阶段):通过Javascript操作DOM时,页面再次渲染速度如何更快?
目标是减少页面渲染过程的重排、重绘
具体优化方法:
(1)减少DOM操作,将多次操作DOM合并为一次,如插入元素节点(2)减少逐项更改样式,最好一次性更改style,或者将样式定义为class并一次性更新(3)前端框架Vue、React(虚拟DOM和Diff算法等)(3)避免多次读取offset等属性,使用变量做缓存(4)防抖、节流(5)做动画效果时,使用will-change和transform 做优化
(二)虚拟DOM及算法
多次1. 页面加载阶段:
首次加载时,先创建虚拟DOM树,再根据虚拟DOM树创建真实的DOM树,然后继续一系列渲染流水线工作2. 页面加载阶段:
如果数据发生了改变,再创建一棵新的虚拟DOM树两棵虚拟DOM树对比,计算出最少变化把所有变化记录一次性更新到真实DOM树上,然后继续一系列渲染流水线工作引入虚拟DOM树执行流程.png
五、浏览器中安全
同源策略:协议、域名、端口三者都相同则称为同源
1. XSS 攻击:跨站脚本攻击(Cross Site Scripting)
XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意 JavaScript 脚本,在用户浏览页面用户实施攻击的一种手段
(1)风险:
窃取用户Cookie信息-- 通过document.cookie获取用户Cookie 信息,发送到恶意服务器-- 恶意服务器拿到用户的 Cookie 信息之后,就可以模拟用户的登录,进行转账等操作监听用户行为-- 通过addEventListener来监听键盘事件,获取用户账号、密码、信用卡等信息, 发送到恶意服务器-- 恶意服务器拿拿到这些信息,又可以做很多违法的事情生成广告等影响用户体验(2)解决方法:
对输入脚本进行过滤或转码如:<script> --><script>响应头Set-Cookie加使用限制-- httpOnly,通知浏览器此 Cookie 只能通过浏览器 HTTP 协议传输,浏览器的 JS 引擎就会禁用 document.cookie;-- SameSite=Strict,限制此Cookie不能随着跳转链接跨站发送
2. CSRF攻击,跨站请求伪造(Cross Site Request Forgery)
目的是利用服务器的漏洞和用户的登录状态来实施攻击
发起CSRF攻击的方式:
通过<img src="恶意网站">,自动跳转到恶意网站通过诱导用户点击隐藏链接,指向恶意网站解决方法:-- SameSite=Strict,限制此Cookie不能随着跳转链接跨站发送-- 验证请求来源站点-- 使用Token验证服务器第一次返回时生成一个Token再次请求客户端带着对应的Token,进行验证
还没有评论,来说两句吧...