const,let,var的区别
- 作用域上面var不存在块级作用域,let和const存在块级作用域。
- 重复声明 var可以重复声明,let和const不可以重复声明。
- 声明提升 var声明提升作用域顶部并且初始化为undefined,let和const声明提升作用域顶部但是不初始化,并且存在暂时性死区,在变量声明之前不能使用该变量否则报错。
- var在全局作用域会被挂载到全局对象上,而let和const不会
- let和const类似但是const必须初始化,而且不能重新赋值。但是对于引用类型数据可以修改里面的值但是不能整个对象重新赋值
Js中变量类型
Js中变量类型分为基本数据类型和引用类型
- 简单类型:Undefined、Null、Boolean、Number、String,Symbol(ES6新增)和BigInt
- 引用类型:Object
引用类型和简单类型的值都保存在栈上面只不过引用类型的值其实是真实数据的地址,指向堆上面的真实数据。
JS中变量只有值传递,所以对函数传参时,如果是引用类型,函数就可以改变外部函数的实际值
判断两个类型的方法 如果是简单类型可以使用typeof操作符但是除了null, 如果是引用类型可以使用instanceof操作符。查看其原型指向的构造方法是否一致。 数组的话有Array.isArray()方法。 null的话直接使用===判断即可。 有一种万能判断方法就是调用Object.prototype.toString.call()方法,传入变量,返回一个字符串,然后这个字符串就包含这个变量类型
包装类型
String、Number、Boolean这三个基本类型可以像对象一样调用方法,主要是由于这三个基本数据类型存在对应的包装类型
当基本数据类型当做对象调用方法的时候,JavaScript会换成对应的包装类型,相当于new了一个对象,内容和基本类型的数据一致,调用方法的时候就会调用这个临时对象的方法,返回结果,然后销毁这个临时对象。所以一些基本数据类型也拥有调用方法的能力
类型转换
JavaScript中类型转换分为显式转换和隐式转换
显示转换就是直接调用对应的构造函数,比如Number()、String()、Boolean()等。
隐式转换就是自动转换,比如在一些判断语句中,会自动转化为布尔值,在一些算术运算中中除了+法可能是字符串拼接其他的都是隐式转换成数字进行运算。
当出现一个字符串拼接其他类型时会将其他类型自动转化为字符串进行拼接
Object转化字符串的时候会优先调用toString()方法,如果返回的值无法转化为字符串就调用valueOf()方法,如果还是不能就报错 而Object转化为数字的时候会优先调用valueOf()方法,如果返回的值无法转化为数字就调用toString()方法,如果还是不能就报错 对象转化为布尔值都为true
运算符
NaN任何运算都为NaN 对象比较的话就先比较valueOf()的值,再比较toString()的值, 严格比较的话就是比较地址是否相同 && 如果左边为false则不会计算右边 返回第一个为false的值 || 如果左边为true则不会计算右边,返回第一个为true的值
如何理解原型和原型链
所有的对象都有一个内部原型属性指向其构造函数的原型对象,而构造函数的原型对象也有一个内部原型属性指向构造函数Object的原型对象,而Object的原型对象也有内部原型属性指向null。这样就形成了原型链。 当访问一个对象的属性时,如果对象本身没有这个属性,那么就会到它的原型对象上去查找,如果原型对象上也没有,就会到原型对象的原型对象上去查找,直到找到为止。 原型对象有constructor属性指向构造函数,这样就在构造函数,原型对象,实例对象之间建立了三角关系,而构造函数也是一个特殊的对象,也有内部原型指向Function的原型对象。Function的内部原型属性指向的是自己的原型对象,自己的原型对象就会指向Object的原型对象最后指向null
有一个例外就是Object.create()方法创建的对象,可以自己指定原型对象
原型对象主要用来实现继承的,可以实现面向对象的编程
如何实现继承: 构造函数的方法,直接将构造函数的原型对象指向Object.create(父原型对象)的实例对象,然后这个原型对象的constructor属性指向子构造函数,这样就可以实现继承。
继承属性的话,直接调用父级构造函数的call方法,将this指向子构造函数的实例对象,这样就可以继承父级构造函数的属性。
而es6之后又class关键字,可以更方便的实现继承。 直接使用extend就可以继承父级构造函数的方法。集成属性直接使用super就行会直接调用父级构造函数
执行栈和执行上下文
当代码运行的时候会创建自己的执行上下文,执行上下文分为全局执行上下文和函数执行上下文。
全局执行上下文就是代码运行的第一个上下文,它是整个代码执行的起点,当运行函数的时候又会创建函数执行上下文,执行上下文以栈的形式被储存,最新创建的执行上下文会被压入栈顶,当函数执行完毕后,执行上下文会从栈顶弹出。 执行上下文士动态创建和销毁的,只有当代码要运行的时候才会创建执行上下文,代码运行完毕会销毁执行上下文
执行上下文分为两个阶段,创建阶段和执行阶段。创建阶段主要是创建变量对象 、作用域链、this,执行阶段主要是执行代码。
变量对象包括函数形参赋值,arguments赋值,字面量函数赋值还有var声明初始化为undefined
在es6出现块级作用域后,函数字面量声明方式如果出现在块级作用域内会出现双重提升的效果并且赋值为undefined,在块级作用域会被提升一次,在函数作用域里也会被提升一次,赋值后在函数外部和块级作用域都会被赋值
作用域与作用域链
es5之前只有全局作用域和函数作用域,es6之后又增加了块级作用域。
作用域主要是为了隔离变量,避免变量的命名冲突
作用域的确定是静态的,他在代码执行之前就被确定好了,如果一个函数内部访问一个作用域外的变量,他就会沿着函数创建的时候的外层作用域里寻找该变量,直到找到为止就形成一个作用域链
作用域链一定是创建的时候确定好的不会动态改变,外层作用域也是根据创建时候的外层作用域而不是函数执行的时候的外层作用域
this的指向
this的指向是由函数调用方式决定的,如果是对象调用函数,该函数的this指向该对象,如果是普通函数调用this指向全局对象,严格模型下指向undefined,如果是构造函数以new的方式调用,this执行实例对象,如果是call或者apply调用,this指向第一个参数,如果是bind调用,this指向绑定函数的this call 的参数不是数组而apply第二个事数组 bind的话会不会立即执行,而是返回一个绑定了this的函数
闭包
闭包就是一个内部函数引用了函数外部的变量,也就是函数使用了自由变量就会发生闭包。 当这个内部函数被返回时,使用这个函数也可以访问到外部函数的变量,可以用来封装数据,实现函数柯里化。
DOM事件的传播机制
DOM传播机制出现主要是因为当你点击了目标元素,其实也是点击目标元素的父元素,父元素的父元素,一直到document根元素所以需要事件传播机制来触发外部的事件 DOM事件传播机制分为两种首先会进行事件捕获,然后会进行事件冒泡,事件捕获就是从最外层元素一直找到目标元素 事件捕获后会触发目标元素的方法并且执行事件冒泡,从目标元素到最外层元素依次冒泡执行对应的事件方法
有一种编程技巧叫做事件委托,利用冒泡机制,如果需要给每个子元素添加事件,可以直接给父元素添加一个事件,当触发子元素的时候会冒泡父元素的事件,拿到target对应的子元素再进行操作,这样可以减少大量事件绑定
阻止事件默认行为方法
- 如果不是使用addEventListener注册事件直接return false
- event.preventDefault()
- 如果是IE浏览器兼容可以使用event.returnValue = false 可以通过event.cancelable来判断是否可以取消默认行为
递归
浮点数的精度问题
在js中所有的Number都是以64位双精度浮点数存储的,遵循IEE754标准,浮点数的运算会先转化为二进制 // 有的小数转化为二进制会超过js所能表示的最大精度 从而会产生截断 // 因此小数就会变成不是很精确 最大精度为+—2^53-1,也就是15位有效数字,所以在进行浮点数运算的时候要注意 在Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER之间的值都可以保证精度
垃圾回收和内存泄漏
JS引擎中垃圾回收是自动的会自动回收没有被用到的变量的内存,当一些变量没有被使用并且没有被回收就会造成内存泄漏 比如说闭包使用了外部变量,外部变量没有被回收,闭包也不会被回收,造成内存泄漏 垃圾回收有两种方式,标记清除和引用计数
标记清除就是当变量进入环境时,就标记这个变量为“进入环境”,当变量离开环境时,就标记这个变量为“离开环境”,然后垃圾回收器会扫描内存中所有的变量,然后清除掉那些标记为“离开环境”的变量,这样就保证了内存的有效利用。
引用计数就是每个值都有一个引用计数,每当有一个变量引用这个值时,引用计数就加1,当引用变量的变量又引用这个值时,引用计数就加1,当引用变量的变量的引用计数为0时,这个值就会被回收。 现在主要使用的还是标记清除,因为他可以解决循环引用的问题
解决内存泄漏的方法就是及时清除引用,闭包中及时清理函数的引用,使用事件解除绑定,定时器清除等等
深浅拷贝
浅拷贝就是在克隆对象时,原对象和拷贝后的对象里依旧公用同一块内存空间
深拷贝就是克隆对象时,原对象和拷贝后的对象不公用同一块内存空间
浅拷贝的主要方式是赋值、Object.assign、还有es6新出的扩展运算符,包括数组的一些slice,contact等方法
深拷贝主要方式是使用Json的静态方法parse和stringify,然后就是lodash的cloneDeep方法,还有手动实现深拷贝的方法,比如递归遍历每个属性,然后进行赋值
事件循环
对于浏览器来说,每一个标签页相当于一个进程,里面会有多个线程共同工作,当JS引擎线程工作时遇到一些自己处理不了的任务就会将工作丢给其他线程处理,当处理完毕后再回到JS线程继续执行。
JS线程里的同步代码执行完毕后,检查微队列上是否有任务,利用Promise.then方法里的任务就会被放到微队列里,如果微队列里有任务就会清空微队列里的任务,再执行一个宏队列里的任务。像一些计时器的操作比如setTimeOut,setInterval,当计时器结束后会被放到宏队列里
执行一个宏队列,再清空微队列,再执行下一个宏队列,再清空微队列这样就保证了事件循环
浏览器总结
浏览器的渲染流程
浏览器的渲染流程分为
- 解析HTML
- 样式计算
- 布局
- 分层
- 生成绘制指令
- 分块
- 光栅化
- 绘制
解析HTML
浏览器开启一个网络线程,向服务器请求HTMl文档
二进制->字符串->分词->分析出节点->构建DOM树
同时浏览器会开启一个预解析线程,平行扫描页面中HTML文档本身的资源引用,提前下载资源如link,script,img等标签
构建dom的时候如果遇到style标签会解析css样式并生成CSSOM树
如果遇到script标签,会暂停解析HTML,并等到js下载完成后并且执行后再执行dom解析 优化方式:async和defer
补充:结构位置的优化
- style标签放到head中,避免页面闪烁
- script标签放在body底部,避免阻塞渲染或者使用defer和async
样式闪烁:如果将style标签放到body底部,浏览器已经解析出绝大部分dom树,为了不让用户等待太久,浏览器会做出妥协,先渲染已经解析了DOM内容(使用默认样式和已经加载的部分样式)
样式计算
渲染主线程会遍历得到的DOM树,依次为每个节点计算样式(确认声明值、层叠冲突、使用继承,使用默认样式)并且将相对单位变为绝对单位 得到一颗带有样式的DOM树
布局
又会遍历每个DOM结点的计算样式,计算出一颗带有位置信息的布局树 这个布局树只显示页面中可见的节点,所以不是和dom树一一对应的
分层
为了确认哪里元素放到哪一层,主线程会遍历布局树,创建一颗层次树 将来某个层中元素改变后只改变该层的元素不会影响到其他层 影响分层的属性:z-index,transform,opacity等都会影响分层
生成绘制指令
主线程会根据每个图层产生绘制指令集,用于描述如何将这一层画出来
生成绘制指令层后,渲染主线程的任务就结束了,渲染主线程将每个图层的绘制信息交给合成线程,
分块
合成线程将每个图层进行分块,划分为更小的区域合成线程会开启多个线程一起进行分块任务
光栅化
光栅化就是确认每个像素点的rgb信息,合成线程将块信息交给GPU进程,以极高的速度完成光栅化
绘制
合成线程拿到每个层,每个块的位图,生成一个个指引信息,指导GPU进行绘制
指引会标识出每个每个位图应该画到屏幕的哪个位置,还会考虑到旋转缩放等变形,变形发生在合成线程与渲染主线程无关,这就是transform效率高的本质原因
浏览器的组成
-
用户界面(user interface)
用于呈现浏览器窗口部件,比如地址栏、前进后退按钮、书签、顶部菜单等
-
浏览器引擎(browser engine)
用户在用户界面和渲染引擎中传递指令
-
渲染引擎(rendering engine)
负责解析 HTML、CSS,并将解析的内容显示到屏幕上。我们平时说的浏览器内核就是指这部分。
-
网络(networking)
用户网络调用,比如发送 http 请求
-
用户界面后端(UI backend)
用于绘制基本的窗口小部件,比如下拉列表、文本框、按钮等,向上提供公开的接口,向下调用操作系统的用户界面。
-
JS 解释器(JavaScript interpreter)
解释执行 JS 代码。我们平时说的 JS 引擎就是指这部分。
-
数据存储(data storage)
用户保存数据到磁盘中。比如 cookie、localstorage 等都是使用的这部分功能。
资源提示关键词
当JavaScript中涉及到访问和使用CSSOM,必须等到CSS文件下载解析完成后才能访问
async和defer
preload:提前加载资源,会先加载,优先级高
<link ref="preload" href="资源位置" as="style|script|image|font">
prefetch:利用空闲时间加载将来可能需要用到的资源(放入缓存5分钟),一般为其他页面需要用到的资源,以便加载后续页面的首屏速度
<link ref="prefetch" href="资源位置">
dns-prefetch:提前查找dns解析,提高解析速度
<link rel="dns-prefetch" href="//cdn.domain.com(域名)">
prerender:提前收集资源并且渲染整个页面
<link rel="prerender" href="https://www.domain.com">
preconnect:预连接,提前建立TCP连接
<link rel="preconnect" href="https://www.domain.com">
缓存
浏览器在请求资源的时候会先查看缓存中是否有该资源,如果有则直接使用缓存,如果没有则向服务器请求资源,并将资源缓存到本地
缓存的位置:
- Service Worker缓存
- Memory Cache(内存缓存)
- Disk Cache(磁盘缓存)
- push Cache(推送缓存) 优先级按上述顺序由高到低
Service Worker
Service Worker自由度最高,可以让我们决定需要缓存哪些资源,如果匹配缓存的资源,如何读取缓存的资源,并且缓存时持续性的 Service Worker传输协议必须使用HTTPS,会涉及到请求拦截,所以必须使用HTTPS来保障安全 Service Worker实现分三个步骤,首先需要注册Service Worker,然后监听install事件就可以缓存需要的资源,在下次用户访问资源的时候就可以通过拦截的方式查询是否存在缓存,如果存在则直接读取缓存,如果不存在则通过fetch请求资源,继续按照缓存的优先级查找数据
Memery Cache
Memery Cache 也就是内存中的缓存,浏览器会自动将已经加载的资源缓存到内存中比如图片,脚本文件,样式文件等
读取内存中的数据快但是缓存持续性比较短,一旦关闭页面缓存也会被释放,而且因为内存有限,比较大的资源不会缓存到缓存里
Memory Cache 机制主要保证了页面有两个相同的请求比如两个相同的图片,只会请求一次,减少了请求次数,提高了页面的响应速度
Disk Cache
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。
Disk Cache 覆盖面基本是最大的,会根据HTTP Header字段判断哪些资源需要缓存,哪些资源不需要请求直接使用,哪些资源已经过期需要重新请求。
Disk Cache 因为是持续性缓存,会面临容量增长的问题 浏览器自动清理的时会有自动算法将最老的和最有可能过时的资源一个一个删除掉。
Push Cache
在服务器主动向浏览器推送资源的时候,浏览器会将资源临时存储在Push Cache上 失效极短,只有5s左右,并且是一次性的,只有当前会话中有效
缓存的类型
强缓存
强缓存是指在请求资源时,浏览器会先查看缓存中是否有该资源,如果有则直接使用缓存
可以造成强缓存的字段主要是Expires和Cache-Control
Expires:缓存到期时间 缺点:
- 由于是一个绝对时间,如果修改了客户端的时间会导致缓存失效,并且由于时差或者误差的因素也会导致客户端与服务器时间不一致导致缓存失效
- 写法太复杂,不易维护
Cache-Control:max-age=2592000(30天) 优点:
- 相对时间,修改客户端时间不会导致缓存失效
- 写法简单,易维护
Cache-Control常用的值
- max-age:缓存的最大有效时间,单位为秒
- no-cache:每次请求都需要向服务器验证资源是否有更新
- no-store:不使用缓存
- must-revalidate:缓存过期后,需要向服务器验证资源是否有更新
- public:可以被所有用户缓存
- private:只能被用户的浏览器缓存
协商缓存
强制缓存过期后,需要协商缓存,由服务器决定缓存内容是否失效 缓存未失效,浏览器返回304 Not Modified,告诉浏览器可以使用缓存 缓存失效,就返回新的数据和缓存规则,浏览器响应数据后再把规则写入到缓存数据库中
协商缓存的字段主要是Last-Modified和Etag
Last-Modified:最后修改时间 Etag:资源标识符,可以标识资源是否改变
Last-Modified&If-Modified-Since:
- 服务器返回资源的最后修改时间,浏览器会将这个时间记录在缓存中,下一次请求相同资源时时,浏览器从自己的缓存中找出“不确定是否过期的”缓存。因此在请求头中将上次的 Last-Modified 的值写入到请求头的 If-Modified-Since 字段 缺点:
- 如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
- 如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。
- 如果文件是服务器动态生成的,那么更新时间永远是生成时间,尽管文件没有变化
Etag&If-None-Match:
Etag一般存储的是文件的特殊标识(一般为Hash) 浏览器在下一次请求该资源时,会将Etag值放入请求头的 If-None-Match 字段,服务器会将当前Etag值与资源的最新Etag值进行比对,如果相同则返回304 Not Modified,告诉浏览器可以使用缓存,否则返回新的数据和缓存规则,浏览器响应数据后再把规则写入到缓存数据库中。
缓存的最佳策略
频繁变动的资源使用no-cache 不常变化的资源使用max-age=31536000(一年) 为了解决更新问题,需要在文件名中添加Hash,版本号等动态参数,如果更新了服务器就会重新请求资源
网络总结
五层网络模型
从上往下分别为应用层,传输层,网络层,数据链路层和物理层,在发送消息的时候,消息从上到下进行打包,每一层会在上一层的基础上加包。而接受信息的时候,从下往上解包,最终得到原始信息
其中:
应用层主要面向互联网中的应用场景,比如网页,邮件,文件中心等,它的代表协议有http、smtp、ftp等等
传输层主要面向传输过程,比如TCP协议是为了保证传输的可靠性,而UDP是一种无连接的广播,提供不同的传输方式
网络层主要解决如何定位目标以及如何寻找最优路径的问题,比如IP 等等
数据链路层的作用是将数据在一个子网内有效传输,MAC地址、交换机都属于该层
物理层是解决二进制数据到信号之间转换的问题,比如双绞线,集线器,光纤等
七层模型在上面的基础上添加表示层(数据加密)会话层(建立会话)
常见的请求方法
GET 请求获取数据 POST 向服务器提交一个信息,通常用于新增一个数据、 PUT 修改服务器的数据、 DELETE 删除服务器的数据, OPTIONS 发生在跨域请求的预检中,表示客户端向服务器发送跨域提交(一般是浏览器自动发送的,详细见跨域)
GET和POST方法的区别
根据http协议来说,GET和POST都是请求行中的第一个单词,除了语义不同外没有本质的区别
之所以在实际开发中产生各种区别主要是因为浏览器的默认行为造成的
受浏览器影响,会有以下区别
- 浏览器发送GET请求不会携带请求体
- GET请求传递信息少,适合传递少量数据,而POST请求传递信息量没有限制,适合传递大量数据
- GET请求只能传递ASCII数据,遇到非ASCII数据需要进行编码;POST请求没有限制
- 大部分GET请求传递的数据附带在path参数中,能够通过分享地址完整的重现页面,但也暴露了数据,如果有敏感数据,不应该使用GET请求,至少不应该放到path中
- 刷新页面,如果GET请求得到的页面会直接刷新,如果是POST请求得到的页面会提示你确认重新提交表单
- 如果get请求可以保存到书签,而POST不行
记忆点,牢记get没有请求体->只能将数据放到路径中->只能传递少量数据,并且只能传递ASCII码数据否则自动编码 如果是页面,刷新POST会提示你确认,因为有请求体无法保存,GET的话才可以保存到书签中
常见的状态码
- 1xx:表示请求已收到,继续处理
- 2xx:成功,表示请求已成功被服务器接收、理解、并处理
- 200 OK表示请求成功
- 204 No Content表示请求成功但没有返回内容
- 3xx:重定向,表示需要进行附加操作,以完成请求
- 301 Moved Permanently表示永久重定向
- 302 Found表示临时重定向
- 304 Not Modified表示资源未修改,可使用缓存
- 4xx:客户端错误,表示请求包含语法错误或无法完成请求
- 400 Bad Request表示请求报文语法错误
- 401 Unauthorized表示请求需要用户验证
- 403 Forbidden表示服务器拒绝请求
- 404 Not Found表示服务器找不到请求的资源
- 5xx:服务器错误,表示服务器在处理请求的过程中发生了错误
- 500 Internal Server Error表示服务器端发生了未知错误
- 503 服务器超负载或者停机维护,无法处理请求
cookie
http是无状态协议,每次请求都是独立,无法保存身份信息
所以浏览器出现了cookie的概念,类似浏览器的专属卡包,保存各个网站的身份信息
cookie记录了多个信息,比如 key,value,domain,path,secure(是否使用安全传输),expire(过期时间) 当浏览器发送请求的时候,如果cookie满足
- 没有过期,domain和path都满足
- 如果启用了secure,只能使用HTTPS
就会将满足要求的cookie全部发送出去 Cookie:key=value;key=value的形式
Cookie的设置
服务器设置Cookie 服务器返回响应的时候再请求头带一个set-cookie 格式:
键=值; path=?; domain=?; expire=?; max-age=?; secure; httponly(是否只能用于传输,设置为true后客户端不能获取)
键值对必须,其他的选填
domain默认为浏览器当前域
path默认为当前文件夹
比如 /api/v1/users path=/api/v1、/api path=/api
Cookie的删除
直接设置max-age为-1就可以了
客户端修改cookie
document.cookie = "键=值; path=?; domain=?; expire=?; max-age=?; secure";
Cookie和localStorage还有sessionStorage的区别
cookie、sessionStorage、localStorage 都是保存本地数据的方式
其中,cookie 兼容性较好,所有浏览器均支持。浏览器针对 cookie 会有一些默认行为,比如当响应头中出现
set-cookie字段时,浏览器会自动保存 cookie 的值;再比如,浏览器发送请求时,会附带匹配的 cookie 到请求头中。这些默认行为,使得 cookie 长期以来担任着维持登录状态的责任。与此同时,也正是因为浏览器的默认行为,给了恶意攻击者可乘之机,CSRF 攻击就是一个典型的利用 cookie 的攻击方式。虽然 cookie 不断的改进,但前端仍然需要另一种更加安全的保存数据的方式HTML5 新增了 sessionStorage 和 localStorage,前者用于保存会话级别的数据,后者用于更持久的保存数据。浏览器针对它们没有任何默认行为,这样一来,就把保存数据、读取数据的工作交给了前端开发者,这就让恶意攻击者难以针对登录状态进行攻击。 cookie 的大小是有限制的,一般浏览器会限制同一个域下的 cookie 总量不超过 4KB,而 sessionStorage 和 localStorage 则拥有更大的空间,多数浏览器一般要求不超过 5MB~10MB cookie 会与 domain、path 关联,而 sessionStorage 和 localStorage 只与 domain 关联
同源策略
浏览器同源策略:源 = 协议 + 主机 + 端口 两个源相同称为同源 同源策略指的就是,如果页面的源和页面加载的源不一致,出于安全考虑浏览器会做出一些限制
同源策略对ajax的跨域最狠,默认情况下不允许ajax访问跨域的资源
常见的解决方法有三种:代理服务器,cors,JSONP
代理服务器
主要适用于生产环境不会跨域但是开发环境会出现跨域
只需要对开发服务器加一个配置即可,会自动开启代理服务器
CORS
CORS的话简单来说浏览器访问跨域资源,需要获得服务器的允许 CORS针对不同的请求规定了三种交互模式
简单请求 预检请求 附带身份凭证的请求
简单请求
- 请求方法是get、post,
- 请求头要满足W3C规范,不改动请求头
- Content-Type只限于application/x-www-form-urlencoded、multipart/form-data、text/plain
简单请求的校验规则是: 如果浏览器请求的资源是简单请求,则浏览器会在请求头中添加Origin字段(当前源)
如果服务器根据当前源判断是否允许跨域请求,则会在响应头中添加Access-Control-Allow-Origin字段(允许跨域请求的源)
如果服务器不允许跨域请求,不会返回Access-Control-Allow-Origin字段,浏览器会拒绝响应。
预检请求
如果不是简单请求,浏览器会发送一个预检请求,询问服务器是否允许跨域请求 服务器允许 浏览器发送真实请求 服务器完成真实响应
方法名OPTIONS, 请求头Access-Control-Request-Method, 请求头Access-Control-Request-Headers, 请求头Origin 浏览器表明后续请求要做的事情
浏览器允许的话就会响应 Access-Control-Allow-Origin Access-Control-Allow-Methods Access-Control-Allow-Headers Access-Control-Max-Age:预检缓存的时间
然后就像简单请求一样,浏览器发送真实请求并且附带Origin,服务器返回Access-Control-Allow-Origin
附带身份凭证的请求
如果需要发送cookie,则需要设置withCredentials为true, 服务器需要在响应头添加Access-Control-Allow-Credentials字段,值为true
浏览器会将cookie发送到服务器,服务器可以根据cookie做一些验证
如果需要发送cookie,不能使用通配符 * 这就是为什么不推荐使用 * 的原因
补充
跨域的时候JS只能拿到一些基本的响应头,如果要访问其他头,需要服务器配置Access-Control-Expose-Headers字段,将其他头暴露出来
JSONP
在CORS之前,使用最多的就是JSONP方案
当需要跨域的时候,不使用AJAX,而是通过script标签的src属性来请求资源 服务器拿到请求后响应一段JS代码,就是函数的调用,将数据作为参数传入从而间接传入数据
JSONP只能使用GET请求,而且需要浏览器和服务器绝妙的配合
输入URL后发生了什么
- 补全协议和端口号
- 自动完成URL编码
- 查看本地缓存,如果命中就不再发送请求
- DNS解析,获取域名对应的IP地址
- 建立TCP连接
- 如果是HTTPS,还需要进行SSL握手
- 浏览器决定需要携带哪些Cookie
- 自动设置好请求头、协议版本、cookie发出GET请求
- 服务器处理请求,响应http响应报文
- 浏览器根据Connection头决定是否关闭TCP连接
- 浏览器根据状态码决定如何处理响应
- 浏览器根据Content-Type头决定如何解析响应内容
- 浏览器根据请求头进行缓存和cookie的设置
- 从上到下解析HTML,遇到外部链接,则进一步请求资源
- 解析过程中生成DOM树和CSSOM树,然后将DOM树和CSSOM树结合生成渲染树,然后对渲染树每个节点计算位置和大小,最后渲染到页面上
- 解析过程中触发一系列事件,比如DOM树完成后会触发DOMContentLoaded事件,所有资源加载完成后会触发load事件等等
vue2总结
组件之间通信总结
父子组件通信
- props、$emit
- style和class,会合并到子组件的根元素中,attribute,子组件没有声明的props属性但是父组件给子组件传递了其他属性(不包括style和class),子组件就会以this.$attrs的方式获取这些没有被声明的值
- 可以使用.native修饰符,将事件注册在子组件的根元素上
<template>
<div>
<child @click.native="handleClick"></child>
</div>
</template>
<script>
import Child from './Child.vue'
export default {
components: {
Child
},
methods: {
handleClick() {
console.log('click child')
}
}
}
</script>
- $listeners可以获取父组件传过来的所有事件处理函数
- v-model 双向绑定
- sync修饰符,和v-model一样只不过update:num1(vue3后废弃)
- $parent、$children还有$refs可以获取组件实例元素
跨组件通信
- provide/inject
- router 通路由传参
- vuex/pinia
- Store模式
- EventBus 原理, 创建一个空的Vue实例作为事件中心,然后通过这个空的实例的$on$emit来触发和监听事件,也可以将当前组件实例作为事件中心,挂载到Vue原型上$bus上面,通过$on/$emit触发和监听事件。
虚拟DOM
- 什么是虚拟DOM? 虚拟DOM其实就是一个js对象用于描述视图页面结构,在vue中每个组件都有一个render函数,返回一个虚拟dom
- 为什么需要虚拟dom 因为视图更新的时候,会重新渲染整个页面,如果直接操作真实DOM,真实DOM的创建,更新,插入都会造成巨大的性能浪费,所以需要利用虚拟DOM代理真实DOM,当页面更新的时候直接对比新旧两颗虚拟DOM树找到两颗树的差异,只更新差异部分的真实DOM,这样一来就保证了真实DOM的最小改动
- 模版和虚拟DOM的关系
vue框架中会有一个compile模块,将模版转化为render函数,而render函数调用后得到虚拟dom
步骤:
- 模版转化为ast语法树
- ast语法树转化为render函数 如果是传统的引入方式,就是运行时编译模版 如果是vue-cli默认配置下,在打包的时候就会提前进行模版编译并且还会移出掉compile模块,减少打包体积,提高性能
在vue运行的时候最终需要的都是render函数而不是模版,因此模版中的各个语法都是不存在的,最后都会变成虚拟dom的配置
数据响应式原理
响应式他的最终目标就是当数据发生变化的时候,将运行一些函数,最常见的就是render函数
在具体的实现上vue使用了几个核心的部件
- Observer
- Dep
- Watcher
- Scheduler
Observer
Observer核心目标就是使用Object.defineProperty()给对象的每一个属性添加getter和setter方法,这样vue就能监听到数据的变化,可以做更多的事情
具体来说就是深度递归遍历对象的每一个属性,使用defineProperty()进行监听
由于defineProperty只能监听对象已有属性的变化,不能监听到对象新增和删除属性,所以vue又使用了delete方法来实现新增和删除属性的监听
对于数组,vue重写了数据的方法,使得调用数组方法变更数组时vue可以监听,由于数组的长度一般都很大,所以vue没有对数组的索引进行监听
总之,Observer的目标就是让一个对象它的属性的读取,赋值能够被vue监听到
Dep
Dep的含义就是依赖,当vue的属性被读取的时候,需要记录读取自己的函数,这样一来,当数据发生变化的时候就会通知这些依赖函数重新运行达到响应式的效果
Dep的核心目标就是记录依赖并且在数据变化的时候通知所有依赖函数重新运行
Watcher
Watcher的核心目标就是当Dep收集依赖的时候,Dep能够知道是哪个函数正在使用属性
具体方法就是,当函数需要被调用的时候,不直接调用,而是将函数交给Wather的实例,这个Watcher实例会先将直接暴露给Dep的静态属性target上面,这样每个属性的Dep都能访问到这个Watcher了
然后Watcher再调用目标函数,目标函数被调用时就会访问到一些属性,这些属性的Dep实例就会收集到这个Watcher到自己的依赖里,这样一来,当数据变化的时候,Dep会通知所有Watcher实例重新运行,而Watcher实例就会调用到目标函数
Scheduler
当多个数据发生变化的时候,如果变化的数据有相同的依赖,那么可能就会执行重复的Watcher,导致频繁调用统一个函数使得效率低下,这个显然是不合适的
因此当watcher派发更新的时候不会立即执行,而是将自己交给一个调度器,这个调度器会维护一个队列,该队列中同一个watcher只会存在一个,队列中的watcher不会立即执行,而是通过一个nextTick的方法把需要执行的watcher放到事件循环的微队列中,nextTick就是通过Promise来实现的