一、简述Vue中key的作用及原理
key是虚拟Dom对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟Dom】,随后Vue进行【新虚拟Dom】与【旧虚拟Dom】的差异比较。
原理(比较规则):
- 旧虚拟Dom中找到了与新虚拟Dom相同的key:
- 若虚拟Dom中内容没变,直接使用之前的Dom;
- 若虚拟Dom中内容变了,则生成新的真实Dom,随后替换掉页面中之前的真实Dom;
- 创建新的真实Dom,随后渲染到页面。
index作为key 可能会引发的问题:
- 若对数据进行逆序添加,逆序删除等破坏顺序操作:会产生没有必要的真实Dom更新===》界面效果没问题,但效率低;
- 如果结构中还包含输入类的Dom:会产生错误Dom更新===》界面有问题。
二、简述对于Vue的diff算法的理解
- diff算法的作用:用来修改dom的一小段,不会引起dom树的重绘;
- diff算法的实现原理:将virtual dom的某个节点数据改变后生成的新的vNode与旧节点进行比较,并替换为新的节点,具体过程就是调用patch方法,比较新旧节点,一边比较一边给真实的dom打补丁进行替换。
- 具体过程详解:
- 在采用diff算法进行新旧节点进行比较的时候,比较是在同级进行比较的,不会进行跨级比较;
- 当数据发生改变的时候,set方法会调用dep.notify通知所有的订阅者watcher,订阅者会调用patch函数给响应的dom进行打补丁,从而更新真实的视图;
- patch函数接受两个参数,第一个是旧节点,第二个是新节点,首先判断两个节点是否值得比较,值得比较则执行patchVnode函数,不值得比较则直接将旧节点替换为新节点。如果两个节点一样就直接检查对应的子节点,如果子节点不一样就说明整个子节点全部改变,不再往下对比,直接进行新旧节点的整体替换。
- patchVnode函数:找到真实的dom元素;判断新旧节点是否指向同一个对象,如果是就直接返回;如果新旧节点都有文本节点,那么直接将新的文本节点赋值给dom元素,并且更新旧的节点为新的节点;如果旧节点有子节点而新节点没有,则直接删除dom元素中的子节点;
三、简述Vue的实现原理
vue.js是采用数据劫持结合发布者-订阅者模式的方法,通过Object.defineProperty()来劫持各个属性的setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体步骤:
第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter;这样子的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化;
第二步:compile解析模版指令,将模版中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦有数据变动,收到通知,更新视图;
第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器里面添加自己;
- 自身必须有一个update方法;
- 待属性变动dep.notice通知时,能调用自身的update方法,并触发Compile中绑定的回调,则功成身退。
第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模版指令,最终利用Watcher搭建起Observer和Compile之间的通信桥梁,达到数据变化---->视图更新;视图交互变化---->数据model变更,这样子的双向绑定效果。
四、简述Vue2和Vue3的区别
1、双向数据绑定原理不同
vue2:利用ES5的一个API:Object.defineProperty()对数据进行劫持,结合发布订阅模式的方式来实现的;
vue3:使用ES6的Proxy对数据代理,去实现双向数据绑定的。
相比vue2.x,使用proxy的优势如下:
- defineProperty只能监听某个属性,不能对全局对象监听;
- 可以省去for in,闭包等内容来提升效率(直接绑定整个对象即可);
- 可以监听数组,不用再去单独的对数组做特异性操作,vue3.x可以检测到内部数据的变化。
2、是否支持碎片
vue2:不支持碎片;
vue3:支持碎片(Fragments),就是说可以拥有多个根节点。
3、API类型不同
vue2:使用选项类型api,此种方式在代码里分割了不同的属性:data、computed、methods等;
vue3:使用组合式api,新的合成型api能让我们使用方法来分割,相比于旧的api使用属性来分组,这样子代码会更加简便和整洁。
4、定义数据变量和方法不同
vue2:把数据放在data中,在vue2中定义数据变量是data(){},创建方法要在methods:{}中;
vue3:需要使用一个新的setup()方法,此方法在组件初始化构造的时候触发。使用一下三个步骤来建立反应性数据:
- 从vue引入reactive;
- 使用reactive()方法来声明数据作为响应性数据;
- 使用setup()方法来返回我们的响应性数据,从而template可以获取这些响应性数据。
5、生命周期钩子函数不同
vue2生命周期:
beforeCreate 组件创建之前
created 组件创建之后
beforeMount 组价挂载到页面之前执行
mounted 组件挂载到页面之后执行
beforeUpdate 组件更新之前
updated 组件更新之后
vue3生命周期:
setup 开始创建组件前
onBeforeMount 组价挂载到页面之前执行
onMounted 组件挂载到页面之后执行
onBeforeUpdate 组件更新之前
onUpdated 组件更新之后
而且vue3.x生命周期需要在调用前先进行引入。除了这些钩子函数外,vue3.x还增加了onRenderTracked 和onRenderTriggered函数。
6、父子传参数不同
vue2:父传子,用props,子传父,用时间Emitting Events。在vue2中,会调用this.$emit,然后传入事件名和对象;
vue3:父传子,用props,子传父,用时间Emitting Events。在vue3中的setup()中的第二个参数content对象中就有emit,那么我们只要在setup()接收第二个参数,使用分解对象法,取出emit,就可以在setup方法中随意使用了。
7、指令与插槽不同
vue2:使用slot,可以直接使用slot;v-for与v-if在vue2中优先级高的是v-for指令,而且不建议一起使用;
vue3:必须使用v-slot的形式;vue3中v-for与v-if,只会把当前v-if当作v-for中的一个判断语句,不会互相冲突;vue3中移除keyCode作为v-on修饰符,当然也不支持config.keyCodes;vue3中移除v-on.native修饰符;vue3中移除过滤器filter。
8、main.js文件不同
vue2:可以使用prototype(原型)的形式去进行操作,引入的是构造函数;
vue3:需要使用结构的形式去操作,引入的是工厂函数;vue3中的app组件中可以没有跟标签。
五、简述Vue中watch用法
在vue中使用watch来监听数据的变化;
- 监听的数据后面可以写成对象形式,包含handle方法,immediate和deep;
- immediate标识在watch中首次绑定的时候,是否执行handler,值为true,则表示在watch中声明的时候,就立即执行handler方法,值为false,则和一般使用watch一样,在数据变化的时候才执行handle;
- 当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的变化,只有data中的数据才能够监听到变化,此时就需要deep属性对这个对象进行深度监听。
可以看一下示例代码:
watch: {
name: {
handler(newName, oldName) {},
deep: true,
immediate: true
}
}
六、简述为什么Vue采用异步渲染
vue是组件级更新,当前组件里的数据变了,它就会去更新这个组件。当数据更改一次组件就重新渲染一次,性能不高,为了防止数据一更新就更新组件,所以做了个异步更新渲染(核心的方法就是nextTick)。
源码实现原理:
当数据变化后会调用notify方法,将watcher遍历,调用update方法通知watcher进行更新,这时候watcher并不会立即去执行,在update中会调用queueWatcher方法将watcher放到一个队列里,在queue Watcher会根据watcher的进行去冲,多个属性依赖一个watcher,如果队列中没有该watcher就会将该watcher添加到队列中,然后通过nextTick异步执行flushSchedulerQueue方法刷新watcher队列。flushSchedulerQueue方法中开始会触发一个before方法,其实就是before Update,然后watcher.run()才开始真正执行watcher,执行完页面就渲染完成啦,更新完成后会调用update钩子。
七、请解释Vue的父子组件生命周期钩子函数执行顺序
- 加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created
-> 子 beforeMount -> 子 mounted -> 父 mounted
- 子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
- 父组件更新过程
父 beforeUpdate -> 父 updated
- 销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
总结: 父组件先开始 子组件先结束
八、SPA首屏加载速度慢怎么解决?
1、请求优化:
CDN将第三方库放到CDN上,能否大幅度减少生产环境中的项目体积,另外CDN能够实时的根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
2、缓存:
将长时间不会改变的第三方库或者静态资源设置为强缓存,将max-age设置为一个非常长的时间,再将访问路径加上哈希值,哈希值变化了以后保证获取到最新资源,好的缓存策略有助于减轻服务器的压力,并且显著的提升用户体验。
3、gzip:
开启gzip压缩,能够有效的压缩传输资源的大小。
4、http2:
如果系统首屏同一时间需要加载的静态资源非常多,但是浏览器对同域名的tcp连接数量是有限制的(chrome上限是6个),超过规定数量的tcp连接,则必须要等到之前的请求收到响应后才能继续发送;而http2则可以在多个tcp连接中并发多个请求没有限制,在一些网络较差的环境开启http2性能提升尤为明显。
5、懒加载
当url匹配到相应的路径时,通过import动态加载页面组件,这样首屏的代码量会大幅减少,webpack会把动态加载的页面组件分离成单独的一个chunk.js文件
6、预渲染
由于浏览器在渲染出页面前,需要先加载和解析相应的html、css和js文件,为此会有一段白屏的时间,可以添加loading,或者骨架屏幕尽可能的减少白屏对用户的影响。
7、合理使用第三方库
对于一些第三方ui框架、类库,尽量使用按需加载,减少打包体积
8、使用可视化工具分析打包后的模块体积
webpack-bundle-analyzer这个插件在每次打包后能够更加直观的分析打包后模块的体积,再对其中比较大的模块进行优化
9、封装
构建良好的项目架构,按照项目需求就全局组件、插件、过滤器、指令、utils等做一些公共封装,可以有效的减少我们的代码量,而且更容易维护资源优化
10、图片懒加载和压缩图片,压缩图片可以使用image-webpack-loader,在用户肉眼分辨不清的情况下,一定程度上压缩图片