文章目录
-
- vue响应式原理
- defineProperty() 的缺点
- vue2.0如何监听数组或对象
- vue.$set的实现原理
- vue2.0为什么不劫持数组
- vue3.0是怎么做的劫持
- 为什么vue2.0不用proxy
- proxy的优势
- 为什么要用 Proxy 替代 defineProperty ?
- mvvm和mvc的区别
- computed和watch的区别
- v-if和v-show的区别
- v-model 是如何实现的,语法糖实际是什么?
- data为什么是一个函数而不是对象
- 说说你对vue的理解
- 为什么Vue中的v-if和v-for不建议一起用?
- 那两个都想用怎么办?
- 为什么vue采用异步渲染
- 子组件可以直接改变父组件的数据吗?
- vue和react有什么区别?
- 组件通信的方式
- vue的生命周期?
- kepp-alive的理解,如何实现呢?
- $nexTtick的原理及作用
- vue的性能优化有哪些
- 对前端路由的理解
- Vue-router 导航守卫有哪些
- `$route 和$router` 的区别
- 路由的hash和history模式的区别
vue响应式原理
数据响应式原理的核心就是采用了数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。get()方法可以读取数据、收集依赖,set()方法可以改写数据,在数据变动时会对数据进行比较,如果数据发生了变化,会发布消息通知订阅者,触发监听回调,更新视图。
watcher: 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而致使它关联的组件得以更新。
defineProperty() 的缺点
- 在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
vue2.0如何监听数组或对象
Vue 不允许动态添加根级响应式属性
- 官网给的建议是:使用vue.$Set(你要改变的数组/对象,你要改变的位置/key,你要改成什么value)或者vue.set
- vue源码里缓存了array的原型链,然后重写了这几个方法,触发这几个方法的时候会observer数据,意思是使用这些方法不用再进行额外的操作,视图自动进行更新。splice()

区别在于Vue.set()是将set函数绑定在Vue构造函数上,this.$set()
是将set函数绑定在Vue原型上。 set和$set的区别
vue.$set的实现原理
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)
vue2.0为什么不劫持数组
1.Obejct.definePropetry这个api,其实他是可以监听到数组下标变化的,对于js来说,数组也是Object。
2.为什么vue不劫持数组:
因为数组不像对象那样,即使位置错乱,key和value对应关系一般不会变,每个value更新只会造成一次set。而数组则不一样,它只能通过下标去做key,这个key是不固定的,位置变化就会造成多次set操作,vue官方也考虑到性能问题,所以没有对数组做劫持,而是将数组的7个变异方法进行重写,也就是更改了Array原型上的方法达到劫持的效果。
vue3.0是怎么做的劫持
1.通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
2.Proxy只会代理对象的第一层,Vue3是怎样处理这个问题的呢?
判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
为什么vue2.0不用proxy
vue2.x之前之所以不用Proxy,主要Proxy是es6提供的新特性,兼容性不好,最主要的是这个属性无法用polyfill来兼容。Polyfill 指的是用于实现浏览器并不支持的原生 API 的代码。
比如说 querySelectorAll 是很多现代浏览器都支持的原生 Web API,
但是有些古老的浏览器并不支持,那么假设有人写了一段代码来实现这个功能.
使这些浏览器也支持了这个功能,那么这就可以成为一个 Polyfill。
proxy的优势
Proxy 与 Object.defineProperty 优劣对比
- 响应式是惰性的。
在 Vue.js 2.x 中,对于一个深层属性嵌套的对象,要劫持它内部深层次的变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的,这无疑会有很大的性能消耗。
在 Vue.js 3.0 中,使用 Proxy API 并不能监听到对象内部深层次的属性变化,因此它的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部属性才会变成响应式,简单的可以说是按需实现响应式,减少性能消耗。
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的
为什么要用 Proxy 替代 defineProperty ?
1.defineProperty API 的局限性最大原因是它只能针对单例属性做监听。
Vue2.x中的响应式实现正是基于defineProperty中的descriptor,对 data 中的属性做了遍历 + 递归,为每个属性设置了 getter、setter。这也就是为什么 Vue 只能对 data 中预定义过的属性做出响应的原因。
2.Proxy API的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作, 这就完全可以代理所有属性,将会带来很大的性能提升和更优的代码。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
mvvm和mvc的区别
这都是软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。
- mvc:MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。Controller 层是 View 层和 Model 层的纽带,当用户与页面产生交互的时候,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
- mvvm:Model代表数据模型,View代表UI视图,负责数据的展示;ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。当Model中的数据改变时会触发View层的刷新,View中用户交互操作而改变的数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
computed和watch的区别
对于Computed计算属性:
- 支持缓存,只有依赖的数据发生了变化,才会重新计算
- 不支持异步,当Computed中有异步操作时,无法监听数据的变化
- 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
- 在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
对于watch侦听器:
- 它不支持缓存,数据变化时,它就会触发相应的操作
- 支持异步监听,第一个参数是最新的值,第二个是变化之前的值
- 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
immediate:组件加载立即触发回调函数
deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。
v-if和v-show的区别
- v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
- v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染;
- v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display;
- v-if适合运营条件不大可能改变;v-show适合频繁切换
- v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
v-model 是如何实现的,语法糖实际是什么?
(1)作用在表单元素上 : 给input绑定了v-on的value值,这个值指向了变量,并且在触发 input 事件的时候通过v-bind去动态把 message设置为目标值。
(2)作用在组件上 :在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件
本质是一个父子组件通信的语法糖,通过prop和$.emit实现。
data为什么是一个函数而不是对象
在vue中,我们想要实现组件的复用,那就需要每个组件有自己的私有数据,而不互相干扰。
数据以函数返回值的形式定义,这样当每次复用组件的时候,就会返回一个新的data,也就是说每个组件都有自己的私有数据空间,它们各自维护自己的数据,不会干扰其他组件的正常运行。
如果是一个对象的话,当多个实例引用同一个对象时,只要一个实例对这个对象进行操作,其他实例中的数据也会发生变化,就会造成混乱难以维护。
除此之外:
1.根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况。
2.组件实例对象data必须为函数,**目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。**采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象。
在源码中,自定义组件会进入mergeOptions进行选项合并,进入if判断,若data类型不是function,则出现警告提示。
说说你对vue的理解
为什么Vue中的v-if和v-for不建议一起用?
作用:
- v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值的时候被渲染
- v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组或者对象,而 item 则是被迭代的数组元素的别名
- 在 v-for 的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法进行优化
- 从源码里面可以看到v-for的优先级大于v-if
那两个都想用怎么办?
- 永远不要把 v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)
- 如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环。
- 如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项
为什么vue采用异步渲染
vue是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能,Vue会在本轮数据更新后,再异步更新视图,核心思想是nextTick。
比如我们修改this.message三次,我们真正想要的其实只是最后一次更新而已,也就是说前三次DOM更新都是可以省略的,我们只需要等所有状态都修改好了之后再进行渲染就可以减少一些性能损耗。
这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
子组件可以直接改变父组件的数据吗?
- 不可以,为了维护父子组件的单向数据每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中发出警告。
- 只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
vue和react有什么区别?
- 数据监听的方式。Vue 通过 getter/setter 以及一些函数的劫持,能精确知道数据变化,不需要特别的优化就能达到很好的性能。React 默认是通过比较引用的方式进行的,如果不优化(PureComponent/shouldComponentUpdate)可能导致大量不必要的vDOM的重新渲染。这是因为 Vue 使用的是可变数据,而React更强调数据的不可变。
- 模板渲染方式Vue鼓励写近似常规HTML的模板。写起来很接近标准 HTML元素,只是多了一些属性。React推荐你所有的模板通用JavaScript的语法扩展——JSX书写。不引入新的概念。
- 数据流不同。vue是响应式的数据双向绑定系统,而react是单向数据流,没有双向绑定。
- 组件通信方式父组件通过 props 向子组件传递数据或者回调,虽然可以传递回调,但是我们一般只传数据,而通过事件的机制来处理子组件向父组件的通信。子组件通过 事件 向父组件发送消息。通过 V2.2.0 中新增的 provide/inject 来实现父组件向子组件注入数据,可以跨越多个层级。在React中,父组件通过 props 可以向子组件传递数据或者回调,可以通过 context 进行跨层级的通信 ,这其实和 provide/inject 起到的作用差不多。
React 本身并不支持自定义事件 ,Vue中子组件向父组件传递消息有两种方式: 事件和回调函数,而且Vue更倾向于使用事件 。但是 在 React 中我们都是使用回调函数的 ,这可能是他们二者最大的区别。
- 虚拟dom和diff算法。Vue基于snabbdom库,它有较好的速度以及模块机制。 Vue Diff使用双向链表,边对比,边更新DOM 。React主要使用 diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。
组件通信的方式
根据组件之间关系讨论组件通信最为清晰有效:
父子组件
props/$emit/$parent/ref/$attrs
兄弟组件
$parent/$root/eventbus/vuex
跨层级关系
eventbus/vuex/provide+inject
vue的生命周期?
Vue 实例从创建到销毁的过程,就是生命周期。也就是从开始创建、初始化数据、编译模板、挂载DOM-渲染、更新-渲染、卸载等一系列的过程,我们称这是 Vue 的生命周期。
- 第一次页面加载会触发哪几个钩子?
beforeCreate , created , beforeMount ,mounted
- 简述每个周期具体适合哪些场景?
beforeCreate:创建前,此阶段为实例初始化之后,this指向创建的实例,可以在这加个loading事件。
created:创建后,此阶段为实例已经创建,完成数据(data、props、computed)的初始化导入依赖项
beforeMount:挂载前,虽然得不到具体的DOM元素,但vue挂载的根节点已经创建,下面vue对DOM的操作将围绕这个根元素继续进行。
**mounted:**挂载,完成创建vm.$el,和双向绑定,完成挂载DOM和渲染,可在mounted钩子函数中对挂载的DOM进行操作。可在这发起后端请求,拿回数据,配合路由钩子做一些事情。
beforeUpdate:数据更新前,数据驱动DOM。可在更新前访问现有的DOM,如手动移出添加的事件监听器。
updated:数据更新后,完成虚拟DOM的重新渲染和打补丁。
组件DOM已完成更新,可执行依赖的DOM操作
kepp-alive的理解,如何实现呢?
keep-alive有以下三个属性:
- include 字符串或正则表达式,只有名称匹配的组件会被匹配;
- exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;
- max 数字,最多可以缓存多少组件实例。
实现步骤:
- 获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名
- 通过当前组件名去匹配原来 include 和exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例vNode
- 需要缓存,判断他当前是否在缓存数组里面:
- 存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)
- 不存在,将组件 key放入数组,然后判断当前 key数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
- 最后将这个组件的keepAlive 设置为 true
- 其核心思想是 “如果数据最近被访问过,那么将来被访问的几率也更高”。
$nexTtick的原理及作用
- nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
- Vue采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick了。
所以,在以下情况下,会用到nextTick:
- 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函数中。
- 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。
因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。
vue的性能优化有哪些
(1)编码阶段
尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher v-if和v-for不能连用
如果需要使用v-for给每项元素绑定事件时使用事件代理 SPA 页面采用keep-alive缓存组件
在更多的情况下,使用v-if替代v-show key保证唯一 使用路由懒加载、异步组件 防抖、节流 第三方模块按需导入
长列表滚动到可视区域动态加载 图片懒加载
(2)SEO优化
预渲染 服务端渲染SSR
(3)打包优化
压缩代码 Tree Shaking/Scope Hoisting 使用cdn加载第三方模块 多线程打包happypack
splitChunks抽离公共文件 sourceMap优化
对前端路由的理解
- 在前端技术早期,一个 url 对应一个页面,如果要从 A 页面切换到 B 页面,那么必然伴随着页面的刷新。
- Ajax 出现了,它允许人们在不刷新页面的情况下发起请求,在这样的背景下,出现了 SPA(单页面应用)。
- 但是在 SPA 诞生之初,人们并没有考虑到“定位”这个问题——在内容切换前后,页面的 URL 都是一样的,这就带来了两个问题:
只要刷新一下页面,一切就会被清零,必须重复之前的操作、才可以重新对内容进行定位——SPA 并不会“记住”你的操作。
由于有且仅有一个 URL 给页面做映射,这对 SEO 也不够友好,搜索引擎无法收集全面的信息。
- 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化掉。
- 感知url的变化,不是改造url。一旦我们感知到了,我们就根据这些变化、用 JS 去给它生成不同的内容。
Vue-router 导航守卫有哪些
全局前置/钩子:beforeEach、beforeResolve、afterEach
路由独享的守卫:beforeEnter
组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
$route 和$router
的区别
路由的hash和history模式的区别
hash:
hash模式是开发中默认的模式,它的URL带着一个#。hash值会出现在URL里面,但是不会出现在HTTP请求中,对后端完全没有影响。所以改变hash值,不会重新加载页面。低版本的IE浏览器也支持这种模式,
原理: hash模式的主要原理就是onhashchange()事件:使用onhashchange()事件的好处就是,在页面的hash值发生变化时,无需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。
history:
history模式的URL中没有#,它使用的是传统的路由分发模式,即用户在输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
特点: 当使用history模式时。相比hash模式更加好看。但是,history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。修改历史状态:包括了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。
两种模式对比
调用 history.pushState() 相比于直接修改 hash,存在以下优势:
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL;
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发动作将记录添加到栈中;
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中;而 hash 只可添加短字符串;
- hash模式下,仅hash符号之前的url会被包含在请求中,后端如果没有做到对路由的全覆盖,也不会返回404错误;history模式下,前端的url必须和实际向后端发起请求的url一致,如果没有对用的路由处理,将返回404错误。
hash模式和history模式都有各自的优势和缺陷,还是要根据实际情况选择性的使用。