最近在准备面试,自己表达能力不是很好,所以要提前组织一下语言梳理一下逻辑😀
Vue的双向绑定
第一步: 在页面初始化加载的时候,vue会对data中的数据进行遍历,通过Object.defineProperty()给属性都加上getter和setter来实现数据劫持。
第二步: 设置监听器Observe来监听所有属性,如果属性变化了,就需要通知订阅者watcher是否需要更新视图,由于订阅者数量不止一个,所以需要消息订阅器Dep来专门收集这些订阅者,在监听器和订阅者之间进行统一管理
第三步: 如果数据变化,setter函数通知订阅者,订阅者执行对应的更新函数,解析器Compile对每个节点进行扫描并更新。
请说一下Hash模式和History模式
Hash: url中的 # 之后对应的就是hash值,hash值的变化会触发hashChange()事件,然后根据路由表对应的hash值来判断加载对应的组件。
优点:
- 只需要前端配置,不需要后端配置
- 兼容性好,浏览器都支持
缺点
- hash值后面需要加#,不符合url规范也不美观
- 每次URL的改变不属于一次http请求,所以不利于SEO优化
History: 基于HTML5的pushState()和replaceState()两个api,以及浏览器的popstate事件,地址变化时,通过window.location.pathname找到对应组件。
优点:
- 没有#,符合url规范,更美观
- 每次URL的改变都会触发http请求,有利于SEO优化
缺点:
- URL的改变属于http请求,会重新请求服务器,所以前端的URL必须与后端配置的页面请求URL一致,否则匹配不到任何资源就会返回404页面
- 兼容性差,特定浏览器才支持
你都用过哪些跨域的方法
JSONP:
利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以
CPORS: 后端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。 后端配置CORS后浏览器会自动进行CORS通信,只要后端实现了 CORS,就实现了跨域。
Websocket: WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。
nginx反向代理: 使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。
说几个项目优化的方法
图片懒加载
防抖和节流
安装代码压缩的插件
异步加载js文件
静态资源防在CDN上
flex: 1代表什么
我们打印出来看一下
flex-grow:用于设置弹性盒子的扩展比率
flex-shrink:用于设置弹性盒子的收缩比率
flex-basis:用于设置弹性盒子的伸缩基准值
map和set
Set:
set类似于数组,但他的成员是唯一的,没有重复的值,set中的值不会进行类型转换。
Set 结构的实例有以下属性:
Set.prototype.constructor:构造函数,默认就是Set函数。Set.prototype.size:返回Set实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
Set.prototype.add(value):添加某个值,返回 Set 结构本身。Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。Set.prototype.clear():清除所有成员,没有返回值。 Map:
它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
Map 结构的实例有以下属性和操作方法:
size:属性返回 Map 结构的成员总数。Map.prototype.set(key, value):方法设置键名key和对应的value,如果key已经存在则键值会被更新Map.prototype.get(key):方法读取key对应的键值,如果找不到key,返回undefinedMap.prototype.has(key):方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
JS事件循环机制
JS拥有所有语言中最简单的并发模型——JS使用单线程的"事件循环(Event Loop) "来处理多个任务的执行
简单来说,js的事件循环,每次读取一个任务,然后执行这个任务,执行完再继续获取下一个,如果暂时没有任务,就暂停执行,等待下一个任务到来;如果在执行任务的过程中有新的任务到达,也不会中断现有任务的执行,而是添加到队列的尾部等待
结论是,JS使用基于事件循环的单线程执行方式,而且是非抢断执行的(也就是说,无论发生什么,都会把当前任务执行完,不会出现执行到一半就去执行别的任务的情况)
定时器为什么不准
因为定时器的时间,并不是函数执行的时间,而是"最短x毫秒后,将任务添加到队列中"
js的任务队列分为两条:宏任务和微任务
微任务的优先级要高于宏任务
- 宏任务:正常的异步任务都是宏任务,最常见的就是定时器(setInterval, setImmediate, setTimeout)、IO任务
- 微任务:微任务出现比较晚,queueMicrotask、Promise和async属于微任务(async就是promise,await相当于then),还有nextTick
来看下面的代码
var value = 1
function foo() {
console.log(value);
}
function bar() {
var value = 2
foo();
}
bar();
//结果是 1
foo里访问了本地作用域中没有的变量value,根据前面说的,引擎为了拿到这个变量要去foo的上层作用域查询,那么foo的上层作用域是什么呢?是它调用时所在的bar作用域?还是它定义时所在的全局作用域?
词法作用域:一个函数在被定义的时候,它的作用域就已经被确定了,和拿到哪里执行没有关系,因此词法作用域也被称为“静态作用域”
块级作用域
块级作用域:花括号内{...}的区域就是块级作用域。
先看一段代码:
if (true) {
var a = 1
}
console.log(a);
//结果是 1
运行后发现结果是1,花括号内定义并赋值的a变量跑到全局了,这足以说明,js不是原生支持块级作用域的。
但是ES6标准提出了使用let和const代替var关键字,来创建块级作用域
if (true) {
let a = 1;
}
console.log(a) //Uncaught ReferenceError: a is not defined
虚拟DOM
虚拟Dom是一个用来表示真实DOM的对象
以下为真是DOM:
<ul id="list">
<li class="item">哈哈</li>
<li class="item">呵呵</li>
<li class="item">嘿嘿</li>
</ul>
对应的虚拟DOM为:
let oldDOM = { // 旧虚拟DOM
tagName:'ul', //标签名
props: {
id: 'list'
},
children: [ // 标签子节点
{
tagName: 'li', props: { class: 'item' }, children: ['哈哈']
},
{
tagName: 'li', props: { class: 'item' }, children: ['呵呵']
},
{
tagName: 'li', props: { class: 'item' }, children: ['嘿嘿']
},
]
}
修改li标签的文本后会生成新虚拟DOM,新虚拟DOM是数据的最新状态,我们可以直接拿新虚拟DOM进行渲染,但效率不如直接操作真实DOM高
Diff算法
上面li标签修改了文本,其他是不变的,所以没必要所有节点都更新,只更新li标签就行,Diff算法就是要查出这个li标签的算法
Diff算法就是拿新旧虚拟DOM进行对比,找出更改的虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不更新其他数据没发生改变的节点,进而提高效率。
使用虚拟DOM算法的损耗计算: 总损耗 = 虚拟DOM增删改+(与Diff算法效率有关)真实DOM差异增删改+(较少的节点)排版与重绘
直接操作真实DOM的损耗计算: 总损耗 = 真实DOM完全增删改+(可能较多的节点)排版与重绘
原理
新旧虚拟DOM对比的时候,Diff算法只会在同层级进行,不会跨层级比较,所以Diff算法是:深度优先算法。
当数据改变时,会触发setter,并且通过Dep.notify去通知所有的订阅者Watcher,订阅者就会调用patch方法,更新相应的视图。
执行时刻
vue中的diff执行的时刻是组件内相应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过对比两者找到变化的地方,最后将其转化为对应的DOM操作。
自定义指令
vue自定义指令和组件一样存在全局注册和局部注册两种方式
全局注册:通过Vue.directive(id, [definition]) 方式注册全局指令,第一个参数为自定义指令名称,第二个参数可以是对象数据,也可以是一个指令函数。
<div id="app" class="demo">
<!-- 全局注册 -->
<input type="text" placeholder="我是全局自定义指令" v-focus>
</div>
<script>
Vue.directive("focus", {
inserted: function(el){
el.focus();
}
})
new Vue({
el: "#app"
})
</script>
这个简单案例当中,我们通过注册一个 v-focus 指令,实现了在页面加载完成之后自动让输入框获取到焦点的小功能。
全局注册好了,那么再来看看如何注册局部自定义指令,通过在Vue实例中添加 directives 对象数据注册局部自定义指令。
<div id="app" class="demo">
<!-- 局部注册 -->
<input type="text" placeholder="我是局部自定义指令" v-focus2>
</div>
<script>
new Vue({
el: "#app",
directives: {
focus2: {
inserted: function(el){
el.focus();
}
}
}
})
</script>
钩子函数
一个指令定义对象可以提供如下几个钩子函数 (均为可选),具体可参考官网cn.vuejs.org/v2/guide/cu…