10.13面试题
1、一个页面从输入url到页面加载显示完成这个过程中都发生了什么?
浏览器从输入网址到页面展示主要分为以下几个过程:
第一 地址栏输入url地址 -> 第二 进行DNS解析 -> 第三 建立TCP连接 ->第四 发送http/https请求 -> 第五 服务器返回数据 -> 第六 浏览器解析并渲染页面 ->第七 断开TCP连接
-
首先第一步
-
url全称叫做统一资源定位服,用于定位互联网上的资源,俗称叫做网址,我们在地址栏输入网址敲下回车,浏览器会对输入的网址进行以下的判断:
-
一,检查输入的网址url是否是一个合法的链接,二,如果合法的话,判断输入的url是否完整,如果不完整,浏览器可能会对地址进行猜测,补全地址的前缀或者后缀。三,如果不合法的话,将输入内容作为搜索条件使用用户设置的默认搜索引擎进行搜索。大部分浏览器会从历史记录,书签等地方开始查找我们输入的内容,并给出智能提示,让你可以补全url地址,对于谷歌浏览器,他甚至会直接从缓存中把网页展示出来,也就是说,你还没有按下回车键,页面已经自动显示出来了
-
第二步DNS解析
-
因为浏览器不能直接通过域名找到对应的服务器ip地址,所以需要进行DNS解析找到对应的IP地址进行访问。
-
DNS
是域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。通俗来讲,我们更习惯于记住一个网站的名字,比如www.baidu.com,而计算机更擅长记住网站的ip地址。所以,我们可以把DNS理解为电话本,你找哪个域名就解析出哪个地址 -
当用户在浏览器中输入域名操作系统会检查浏览器缓存和本地的hosts文件中是否有这个网址记录,有的话就从记录里找到对应的ip地址完成域名的解析,没有的话,再接着使用tcp
ip参数中设置的DNS服务器进行查询,如果要查询的域名包含在本地配置区域资源中则返回解析结果,完成域名的解析,还是没有的话,再接着检查本地dns服务器是否缓存有该网址记录,有的话就返回解析结果,完成域名的解析,如果还是没有的话,本地DNS服务器会发送查询报文到根dns服务器,根dns服务器收到请求后,返回顶级域DNS服务器地址,然后本地DNS服务器再发送查询报文到顶级域dns服务器,顶级域dns服务器收到请求之后返回权威dns服务器的地址,然后本地dns服务器再发送查询报文到权威服务器,权威dns服务器收到请求后会返回最终的IP地址完成域名的解析 -
第三建立TCP连接
-
当浏览器获取到服务器的ip地址后,浏览器会用一个随机的端口号向服务器80端口发送tcp链接请求,这个链接请求到达服务端之后通过tcp三次握手建立tcp的链接
-
第四发送http/https请求
-
建立tcp连接后就可以通过http进行数据的传输了,如果使用了https,会在tcp与http之间多添加一层协议作为加密及验证的服务,https使用ssl和tls协议保障了信息的安全-------ssl协议的作用是认证客户端和服务器,确保数据发送到正确的客户端和服务器,加密数据防止数据中途被窃取,维护数据的完整性,确保数据在传输过程中不被改变-----tls协议的作用是用于在两个通信应用程序之间提供保密性和数据完整性。tls协议有两层组成----tls记录协议和tls握手协议
-
第五服务器相应请求并返回数据
-
当浏览器到外部服务器的链接建立后,浏览器会发送一个初始的http
get请求,请求目标通常是一个html文件,服务器收到请求后,将返回一个http的响应报文,内容包括相关的响应头和html正文 -
第六浏览器解析并渲染页面
-
不同的浏览器引擎渲染过程不太一样的,我以谷歌浏览器为例来介绍一下渲染的过程—首先处理html标记并构建dom树,第二部处理css标记并构建cssdom树,第三步将dom树和cssdom树合并为一颗渲染树,第四步根据渲染树来布局以计算每个节点的几何信息,第五步将各个节点渲染到屏幕上,这样就完成了页面的渲染
-
第七断开TCP连接
-
现在的浏览器页面为了优化请求的耗时,默认都会开启持久连接,也就是说标签页关闭的时候,tcp链接才会关闭,这个关闭的过程就是4次挥手
2、vue响应式原理
定义
- 响应式指的是组件 data 的数据一旦变化,立刻触发视图的更新。它是实现数据驱动视图的第一步。
vue响应式也叫作数据双向绑定,大致原理阐述:
- 首先我们需要通过Object.defineProperty()方法把数据(data)设置为getter和setter的访问形式,这样我们就可以在数据被修改时在setter方法设置监视修改页面信息,也就是说每当数据被修改,就会触发对应的set方法,然后我们可以在set方法中去调用操作dom的方法。
- 此外,如果页面有input用v-model绑定数据,我们需要在这种绑定了data的input元素上添加监听,添加input事件监听,每当input事件被触发时,就修改对应的data。
- vue实现数据响应式,是通过数据劫持侦测数据变化,发布订阅模式进行依赖收集与视图更新,换句话说是Observe,Watcher以及Compile三者相互配合,
– Observe实现数据劫持,递归给对象属性,绑定setter和getter函数,属性改变时,通知订阅者
– Compile解析模板,把模板中变量换成数据,绑定更新函数,添加订阅者,收到通知就执行更新函数
– Watcher作为Observe和Compile中间的桥梁,订阅Observe属性变化的消息,触发Compile更新函数
1.什么是响应式
- 当price 发生变化的时候,Vue就知道自己需要做三件事情:
– 更新页面上price的值
– 计算表达式 price*quantity 的值,更新页面
– 调用totalPriceWithTax 函数,更新页面 - 数据发生变化后,会重新对页面渲染,这就是Vue响应式,为了完成这个过程,我们需要:
– 侦测数据的变化
– 收集视图依赖了哪些数据
– 数据变化时,自动“通知”需要更新的视图部分,并进行更新 - 对应专业俗语分别是:
– 数据劫持 / 数据代理
– 依赖收集
– 发布订阅模式
2、如何侦测数据的变化
首先有个问题,在Javascript中,如何侦测一个对象的变化?其实有两种办法可以侦测到变化:使用 Object.defineProperty和ES6的 Proxy,这就是进行数据劫持或数据代理。
方法1.Object.defineProperty
Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
方法2.Proxy
Proxy 是 JavaScript 2015 的一个新特性。 Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性, Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外 Proxy支持代理数组的变化。
3、为什么要收集依赖
我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如第一例子中,模板中使用了price数据,当它发生变化时,要向使用了它的地方发送通知。
订阅者 Dep
1.为什么引入 Dep
收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,说得具体点,它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
观察者 Watcher
1.为什么引入Watcher
Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释:
当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。
依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中,形成如下所示的这样一个关系(图参考《剖析 Vue.js 内部运行机制》)。
收集依赖
所谓的依赖,其实就是Watcher。至于如何收集依赖,总结起来就一句话,在getter中收集依赖,在setter中触发依赖。先收集依赖,即把用到该数据的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就行了。
具体来说,当外界通过Watcher读取数据时,便会触发getter从而将Watcher添加到依赖中,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
最后我们对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码,实现了一个简易的数据响应式。
总结
- 在 newVue() 后, Vue 会调用 _init 函数进行初始化,也就是init 过程,在
这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行
getter 函数,而在当被赋值的时候会执行 setter函数。 - 当render function
执行的时候,因为会读取所需对象的值,所以会触发getter函数从而将Watcher添加到依赖中进行依赖收集。 - 在修改对象的值的时候,会触发对应的 setter, setter通知之前依赖收集得到的 Dep 中的每一个
Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。
3、vue项目的优化
vue项目的优化分为三部分:
Vue 代码层面的优化、webpack 配置层面的优化、Web 技术层面的优化
vue代码层面的优化
- 1 v-for 遍历为 item 添加 key
在列表数据进行遍历渲染时,需要为每一项 item 设置唯一 key 值,方便 Vue.js内部机制精准找到该条列表数据。 - 2 v-for 遍历避免同时使用 v-if
v-for 比 v-if 优先级高,这意味着 v-if 将分别重复运行于每个 v-for 循环中,将会影响速度。建议替换成 computed 属性。 - 3 v-if 和 v-show 区分使用场景
v-if 是真正的条件渲染,也是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多,不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 display 属性进行样式切换。
v-if 适用于很少改变条件,不需要频繁切换的场景;v-show 则适用于需要非常频繁切换的场景。 - 4 computed 和 watch 区分使用场景
computed 计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch 更多的是观察的作用,每当监听的数据变化时都会执行相关回调。
当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,watch 允许我们执行异步操作。 - 5 keep-alive
利用keep-alive包裹,将不活动的组件缓存起来。
在组件切换过程中将状态保留在内存中,防止重复渲染dom,减少加载时间及性能消耗,提高用户体验。 - 6 图片资源懒加载(使用插件)
对于图片过多的页面,为了加快页面加载速度,可以将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件。 - 7 路由懒加载
路由过多,加载的资源过多,页面会出现白屏的情况,不利于用户体验。所以我们把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件: - 8 组件懒加载
在 vue-router 配置路由的时候,采用动态加载路由的形式:
routes:[
path: 'Blogs',
name: 'ShowBlogs',
component: () => import('./components/ShowBlogs.vue')
]
以这种函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件。
- 9 第三方库按需引入
在日常使用UI框架,例如element-UI、iView,我们经常性直接引用整个UI库。但实际上我用到的组件只有按钮,分页,表格等很少一部分,所以我们要按需引用:
import { Button, Input, Pagination } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)
Webpack 层面的优化
- 1 Webpack 对图片进行压缩
对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader 来压缩图片 - 2 模板预编译
打包时,直接把组件中的模板转换为render函数,这叫做模板预编译。这样一来,运行时就不再需要编译模板了,提高了运行效率。
基础的 Web 技术优化
- 1 浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的。
4、项目优化
data扁平化,冻结不能改变的数据,深度vuex,服务器用webpack,异步组件