《12道vue高频原理面试题,你能答出几道?》
参考文献
哪些网站使用了vue,及其seo
Vue单页面应用
单页应用和多页应用:超级详细,超级好的一篇文章
单页面:
说白就是无刷新,整个webapp就一个html文件,里面的各个功能页面是javascript通过hash,或者history api来进行路由,并通过ajax拉取数据来实现响应功能。因为整个webapp就一个html,所以叫单页面!
通俗点来讲,在应用整个使用流程里浏览器由始至终没有刷新,所有的数据交互由router
和ajax
完成。但是用户体验起来和app一样,有明确的页面区分,即所谓的web app。
单页面应用优点:
单页面应用缺点:
多页面:
每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用
优点:
缺点:
3. 页面切换慢
因为每次跳转都需要发出一个http请求HTML,如果网络比较慢,在页面之间来回跳转时,就会发现明显的卡顿
Vue.js 和 MVVM
MVC 即 Model-View-Controller 的缩写,就是 模型-视图-控制器 , 也就是说一个标准的Web 应用程式是由这三部分组成的:
存在问题:
MVVM的出现完美解决了以上问题
MVVM 由 Model,View,ViewModel 三部分构成
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel
进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。这些同步是自动完成的,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
Vue.js和MVVM:
Vue.js 可以说是MVVM 架构的最佳实践
Vue.js 是采用 Object.defineProperty 的 getter 和 setter,并结合观察者模式来实现数据绑定的。当把一个普通 Javascript 对象传给 Vue 实例来作为它的 data 选项时,Vue 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。用户看不到getter/setter,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
vue组件间通信5种方式(完整版)
1、父组件向子组件传值:子组件设置props
2、子组件向父组件传值(通过事件形式): 子组件通过$emit
自定义事件触发父的方法进行通信
3、任何组件间通信(全局的)$emit
/$on
:
Vue.prototype.$EventBus = new Vue()
this.$EventBus.$emit(事件名,数据);
触发事件this.$EventBus.$on(事件名,data => {});
自定义事件并监听4、vuex
全局
state
:页面状态管理容器对象;getters
:state对象读取方法commit
配合mutations
(不允许异步)更新 statethis.$store.commit("mutations里的方法名", data)
dispatch
配合action
(异步)更新 state5、注入provide/inject
provider
来提供变量,然后在子孙组件中通过inject
来注入变量。祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深Vue.observable
优化响应式 provide(推荐)6、 $attrs
、$listeners
这两个属性的模式,也就是父组件A把很多数据传给子组件B,子组件B利用$attrs
收集起来,然后可以利用v-bind="$attrs
"在传给B的子组件C(也就是A组件的孙组件),这样就不用一个一个属性去传了。至于$listeners
与$attrs
类似,$listeners
传递的是事件,在子组件以及孙组件通过$emit
触发事件
《Hash和History两种模式的区别优缺点》
路由这个概念最早是由后端提出的,根据客户端不同的请求url返回不同的数据。其中有一个很大的缺点,就是每次切换路由的时候都要刷新页面,发出请求。因此为了提升用户体验,前端路由就出现了
hash路由模式是这样的:http://xxx.abc.com/#/xx,这里的hash是指url尾巴后的#号及后面的字符。改变后面的hash值,它不会向服务器发出请求,因此也就不会刷新页面。并且每次hash值发生改变的时候,会触发hashchange
事件。因此我们可以通过监听该事件,来知道hash的变化进行一些逻辑处理。在vue的路由配置中有mode选项vue默认使用hash。
触发hash值的变化有2种方法:
a
标签,设置href
属性,标签点击之后,地址栏会改变,同时会触发hashchange事件js
直接赋值给location.hash
,也会改变url,触发hashchange事件。history.pushState()
和history.replaceState()
来进行路由控制,通过这两个方法可以改变url且不向服务器发送请求,只是导致History
对象发生变化,地址栏会有反应。onpopstate
事件来监听路由的改变,但 popstate
事件相比hashchange
有些不同:通过pushState
/replaceState
或
标签改变 URL地址栏 不会触发 popstate
事件,只有用户点击浏览器倒退和前进按钮,或者调用History.back()
、History.forward()
、History.go()
方法时才会触发。因此history
模式下,我们不仅要在 popstate 事件回调里处理 url 的变化,还需要分别在 history.pushState() 和 history.replaceState() 方法里处理 url 的变化pushState/replaceState
的调用和
标签的点击事件来检测 URL
变化,即重写pushState/replaceState
方法达到监听路由,只是没有 hashchange
那么方便《JavaScript如何实现history路由变化监听》hash和history区别
hash
符号之前的内容会被包含在请求中,如 http://www.abc.com/#asdd 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误;Vue生命周期与Vue.nextTick()使用
beforeCreate
:此钩子函数发生在实例创建之前,此时data,methods,computed,watch未初始化,观测数据和事件初始化完成,created
:此钩子函数data,methods,computed,watch数据初始化完成;实例未挂载beforemount
:此钩子函数内就运用了dom虚拟技术 即是先占位置 数据不更新(操作dom时拿不到数据),el未初始化完成mounted
:el实例挂载到dom上,此时可以获取DOM
节点,$ref
属性可以访问beforeupdate
:此函数发生在视图dom数据更新之前,dom虚拟和下载补丁之前,即data更新。但操作dom时拿不到数据,updated
:视图更新完成,beforedestory
:此时实例仍未销毁,this可以使用destoryed
:实例销毁完成,vue3重新审视了 vdom,更改了自身对于 vdom的diff算法。vdom从之前的每次更新,都需要对某个组件进行一次完整遍历对比,改为了切分区块树,来进行动态内容更新。将vdom的操作颗粒度变小,每次触发更新不再以组件为单位进行遍历,也就是只更新 vdom的绑定了动态数据的部分,把速度提高了6倍;
2.x的响应式是基于Object.defineProperty
实现的代理,兼容主流浏览器和ie9以上的ie浏览器,能够监听数据对象的变化,但是监听不到对象属性的增删、数组元素和长度的变化。同时会在vue初始化的时候对data里面的所有属性都进行绑定监听Observe(data)
3.0采用了ES2015的Proxy
来代替Object.defineProperty,可以做到监听对象属性的增删和数组元素和长度的修改,还可以监听Map、Set、WeakSet、WeakMap,同时还实现了惰性的监听(因为Proxy自带lazy特性),不会一开始就把所有定义在data函数中的数据进行绑定监听,而是会在用到的时候才去监听。但是,虽然主流的浏览器都支持Proxy,ie系列却还是不兼容,所以针对ie11,vue3.0决定做单独的适配,暴露出来的api一样,但是底层实现还是Object.defineProperty,这样导致了ie11还是有2.x的问题。但是绝大部分情况下,3.0带来的好处已经能够体验到了。
tree shaking
(更小了)之前 vue的代码,只有一个 vue对象进来,所有的东西都在 vue上,这样的话其实所有你没用到的东西也没有办法扔掉,因为它们全都已经被添加到 vue这个全局对象上了。
vue3的话,一些不是每个应用都需要的功能,我们就做成了按需引入。用 ES module imports按需引入,举例来说,内置组件像 keep-alive、transition,指令的配合的运行时比如 v-model、v-for、帮助函数,各种工具函数。比如 async component、使用 mixins、或者是 memoize都可以做成按需引入。
slots
)使用 Vue 3 ,可以单独重新渲染父组件和子组件。2.x的机制导致作用域插槽变了,父组件会重新渲染,而3.0把作用于插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
typescript
的支持虽然我们在 vue2已经可以使用 typescript了,但是在 vue3中,进一步加强了对 typescript的支持
《Vue原理Computed 》
initComputed
时,会遍历computed里面的每个属性,并创建对应的Watcher
watcher
的时候,传入 lazy
,作用是把计算结果缓存起来,而不是每次使用都要重新计算this.dirty
属性标记计算属性是否需要重新求值。如果为 true,表示缓存脏了,需要重新计算,否则不用computed
的依赖状态发生改变时,就会通知这个惰性的watcher
,watcher.dirty
,只有 dirty
为 true 的时候,才会执行 evaluate
重新计算值,进行派发更新,并重置 dirty
为false
,表示缓存已更新computed
是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch
是监听已经存在且已挂载到 vm 上的数据,所以用watch
同样可以监听computed
计算属性的变化(其它还有 data、props)computed
具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数,而watch
则是页面重新渲染时值不变化也会执行computed
适用一些简单的表达式求值,而 watch 适用于需要监听某数据的变化从而执行一些复杂的操作(异步请求等等)用 computed
和 写一个method
去调用有什么区别:
{ test() }
。《vue-cli webpack项目npm run dev启动过程》、《html-webpack-plugin详解》、《npm run dev&build的流程梳理》
index.html
和main.js
文件main.js
文件中给id=“app”
的div创建一个Vue的实例,该实例中有一个名叫APP
的组件APP
组件就是我们页面显示的内容双向绑定就是model
的更新会触发view
的更新,view
的更新会触发model
的更新,它们的作用是相互的,同步的
所以我们需要关注的是:
model
的变化然后通知我们去更新视图view
view
的变化然后去更新数据model
。input
标签监听@input
事件响应式原理
的分析了《深入理解 Vue 单向数据流》、《理解vue的单向数据流》、《props浅拷贝》
V-module
双向绑定不过是语法糖Object.definePropert
是用来做数据响应式更新的从v-model
的分析我们可以这么理解,双向数据绑定就是在单向绑定的基础上给可输入元素(input、textare等)添加了 change(input) 事件,来动态修改 model 和 view ,即通过触发($emit)父组件的事件来修改mv来达到 mvvm 的效果。
所有的prop
都使得其父子prop
之间形成了一个单向下行绑定:父级prop
的更新会向下流动到子组件中,但是反过来则不行,这样会防止从子组件意外改变父级组件的状态。当开发者尝试这样做的时候,vue 将会报错。这样做是为了组件间更好的解耦,在开发中可能有多个子组件依赖于父组件的某个数据,假如子组件可以修改父组件数据的话,一个子组件变化会引发所有依赖这个数据的子组件发生变化,所以 vue 不推荐子组件修改父组件的数据,在子组件里面直接修改 props
会抛出警告。
这里有两种常见的试图改变一个 prop 的情形:
以上两种情况,传递的值都是基本数据类型,但是大多数情况下,我们需要向子组件传递一个引用类型数据
注意:在 JavaScript 中对象
和数组
是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态(底层是直接浅拷贝的)
单向数据流:子组件不能直接改变父组件传给你的数据,只能通过事件触发$emit
通知父组件,并在父组件中修改原始的prop
数据;父级 prop 的更新会向下流动到子组件中,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值
《ceq数据响应式笔记》
数据劫持 + 发布订阅模式
在initData()
里面会执行observe(data)
,会遍历data里面的属性,依次执行defineReactive()
这个核心函数,通过defineReactive()
为每个属性都新建一个dep
对象(发布-订阅模式的中介者),并且调用Object.defineProperty()
方法,会给data的每个属性添加getter
和setter
,以此来达到依赖收集、派发更新的目的
依赖收集
每个data里面的属性都有dep
对象,dep
对象里面的subs
属性用来存放watcher
数组,在mountComponent
的过程中,会new Watcher()
将updateComponent
传入,new Watcher()
会将自身watcher观察者实例设置给Dep.target
,之后调用updateComponent
回调函数,该回调函数会执行 render()
方法,即访问到data,此时会触发某个属性的getter
,该属性的dep对象就会将当前的watcher
收集到subs
数组中
派发更新
当修改某个属性时,会触发setter
,此时该属性的dep对象就会通过 dep.notify()
遍历了 dep.subs
通知每个 watcher
观察者去执行update()
来更新数据
因为 JS 本身的特性带来的,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到所有 Vue 实例的数据。如果将 data 作为一个函数返回一个对象,那么每一个实例的 data 属性都是独立的,不会相互影响了。
作用:代码复用和抽象
全局注册:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
局部注册:
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
《论如何用Vue实现一个弹窗-一个简单的组件实现》、《Vue插槽》、《Vue 插槽使用(通俗易懂)》
props
和emit
来实现父子组件值的传递,有时候还会配合slot
插槽作用域插槽:
child
组件在很多地方会被调用,希望在不同的地方调用child
的组件时,子组件列表的样式不是child
组件控制的,而是外部child模版站位符即父组件告诉我们子组件的每一项该如何渲染《vuex直接修改state 与 用dispatch/commit来修改state的差异》
Vuex使用场景:为什么不用局部或者全局变量
使用场景:
有什么缺点:
localstorage
存储)this.$store.state.变量 = xxx
修改state不会报错,但是这样就记录不到每一次state的变化,无法保存状态快照,这样错误就很难定位了this.$store.commit(commitType, payload)
来修改state,而不能直接修改state,状态的每次改变都很明确且可追踪,这样可以以便于调试Vuex 状态的所有改变都必须在 store 的 mutation handler (变更句柄)中管理。(action也是通过mutation,即commit
的方式)
Mutation
:必须同步执行。更改 Vuex 的 store 中的状态的唯一方法是提交 mutation;this.$store.commit
Action
:不能直接变更state
状态,提交的还是 mutation
。可以包含任意异步操作;this.$store.dispatch
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)
diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.
更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)
splice
实现响应式dep.notify()
进行更新1.、new Vue,执行初始化
2、调用 compile 函数,挂载$mount
方法,通过自定义render方法、template或者el等生成render函数 ,编译过程如下:
2、调用 new Watcher
函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
3、调用 patch
方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素