文章目录
- 1、spa(单页面)与多页面
- 2、前端工程化MVC和MVVM
- 3、Vue组件间通信方式
- 4、前端路由hash和history
- 5、Vue 钩子函数执行顺序
- 6、vue3.0相对vue2.0有什么变化或者改进
- computed原理
- 7、computed 和 watch 的差异
- 8、vue项目的运行流程
- 9、双向绑定、单向数据流
- 10、数据响应式更新原理(data->View)
- 11、Vue 组件 data 为什么必须是函数
- 12、Vue 中怎么自定义指令
- 13、如何封装一个组件、插槽slot(腾讯一面)
- 13、Vuex(vue的状态管理)
- mutation和action的区别
- Vue 中的 key 到底有什么用?
- vm.$set()实现原理是什么?
- Vue 的渲染过程
1、spa(单页面)与多页面
参考文献
哪些网站使用了vue,及其seo
Vue单页面应用
单页应用和多页应用:超级详细,超级好的一篇文章
单页面:
说白就是无刷新,整个webapp就一个html文件,里面的各个功能页面是javascript通过hash,或者history api来进行路由,并通过ajax拉取数据来实现响应功能。因为整个webapp就一个html,所以叫单页面!
通俗点来讲,在应用整个使用流程里浏览器由始至终没有刷新,所有的数据交互由router
和ajax
完成。但是用户体验起来和app一样,有明确的页面区分,即所谓的web app。
单页面应用优点:
- 分离前后端关注点,前端负责界面显示,后端负责数据存储和计算,不会把前后端的逻辑混杂在一起,由此也减轻了服务端的压力,服务器只需要提供数据给前端就可以
- 同一套后端程序代码,不用修改就可以用于Web界面、手机移动端、平板等多种客户端
- 用户体验好,快,内容的改变不需要重新加载整个页面,Web应用更具响应性,页面切换快
单页面应用缺点:
- 首屏时间稍慢
单页应用的首屏时间慢,首屏时需要请求一次html,同时还要发送一次js请求,两次请求回来了,首屏才会展示出来。 - 不利于SEO(搜索引擎优化效果)问题,现在可以通过Prerender等技术解决一部分
因为搜索引擎只认识html里的内容,不认识js的内容,而单页应用的内容都是靠js渲染生成出来的,搜索引擎不识别这部分内容,也就不会给一个好的排名,会导致单页应用做出来的网页在百度和谷歌上的排名差 - 导航不可用,如果一定要导航需要自行实现前进、后退,需要程序来实现管理
- 使用脚本修改页面,这个脚本我们都知道,他的兼容性是个大问题、
多页面:
每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用
优点:
- 首屏时间快
首屏时间叫做页面首个屏幕的内容展现的时间,当我们访问页面的时候,服务器返回一个html,页面就会展示出来,这个过程只经历了一个HTTP请求,所以页面展示的速度非常快。 - SEO效果好
搜索引擎在做网页排名的时候,要根据网页内容才能给网页权重,来进行网页的排名。搜索引擎是可以识别html内容的,而我们每个页面所有的内容都放在Html中,所以这种多页应用,seo排名效果好
缺点:
3. 页面切换慢
因为每次跳转都需要发出一个http请求HTML,如果网络比较慢,在页面之间来回跳转时,就会发现明显的卡顿
2、前端工程化MVC和MVVM
MVC
MVC 即 Model-View-Controller 的缩写,就是 模型-视图-控制器 , 也就是说一个标准的Web 应用程式是由这三部分组成的:
- View :UI布局,展示数据。
- Model :管理数据。
- Controller :响应用户操作,并将 Model 更新到 View 上。
存在问题:
- 开发者在代码中大量调用相同的 DOM API, 处理繁琐 ,操作冗余,使得代码难以维护。(后来出现jq解决此问题)
- 大量的DOM 操作使页面渲染性能降低,影响用户体验。
- 当 Model 频繁发生变化,开发者需要主动更新到View ;当用户的操作导致 Model 发生变化,开发者同样需要将变化的数据同步到Model 中,这样的工作不仅繁琐,而且很难维护复杂多变的数据状态。
MVVM的出现完美解决了以上问题
MVVM
MVVM 由 Model,View,ViewModel 三部分构成
- Model 层代表数据模型
- View 代表UI 组件
- ViewModel 是一个同步View 和 Model的对象(核心)
在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 追踪依赖,在属性被访问和修改时通知变化。
3、Vue组件间通信方式
vue组件间通信5种方式(完整版)
1、父组件向子组件传值:子组件设置props
2、子组件向父组件传值(通过事件形式): 子组件通过$emit
自定义事件触发父的方法进行通信
3、任何组件间通信(全局的)$emit
/$on
:
- 通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级
- 在main.js里面
Vue.prototype.$EventBus = new Vue()
this.$EventBus.$emit(事件名,数据);
触发事件- 一般在mounted钩子函数里
this.$EventBus.$on(事件名,data => {});
自定义事件并监听
4、vuex
全局
state
:页面状态管理容器对象;getters
:state对象读取方法commit
配合mutations
(不允许异步)更新 statethis.$store.commit("mutations里的方法名", data)
dispatch
配合action
(异步)更新 state
5、注入provide/inject
- 祖先组件中通过
provider
来提供变量,然后在子孙组件中通过inject
来注入变量。祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深 - provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
- provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的----vue官方文档
- provide与inject 怎么实现数据响应式:
- 使用2.6最新API
Vue.observable
优化响应式 provide(推荐)
- 使用2.6最新API
6、 $attrs
、$listeners
这两个属性的模式,也就是父组件A把很多数据传给子组件B,子组件B利用$attrs
收集起来,然后可以利用v-bind="$attrs
"在传给B的子组件C(也就是A组件的孙组件),这样就不用一个一个属性去传了。至于$listeners
与$attrs
类似,$listeners
传递的是事件,在子组件以及孙组件通过$emit
触发事件
4、前端路由hash和history
《Hash和History两种模式的区别优缺点》
路由这个概念最早是由后端提出的,根据客户端不同的请求url返回不同的数据。其中有一个很大的缺点,就是每次切换路由的时候都要刷新页面,发出请求。因此为了提升用户体验,前端路由就出现了
hash
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事件。 - 浏览器前进后退改变 URL
history(h5的API)
- HTML5规范提供了
history.pushState()
和history.replaceState()
来进行路由控制,通过这两个方法可以改变url且不向服务器发送请求,只是导致History
对象发生变化,地址栏会有反应。 - 在window对象中提供了
onpopstate
事件来监听路由的改变,但popstate
事件相比hashchange
有些不同:通过pushState
/replaceState
或<a>
标签改变 URL地址栏 不会触发popstate
事件,只有用户点击浏览器倒退和前进按钮,或者调用History.back()
、History.forward()
、History.go()
方法时才会触发。因此history
模式下,我们不仅要在 popstate 事件回调里处理 url 的变化,还需要分别在 history.pushState() 和 history.replaceState() 方法里处理 url 的变化 - 好在我们可以拦截
pushState/replaceState
的调用和<a>
标签的点击事件来检测URL
变化,即重写pushState/replaceState
方法达到监听路由,只是没有hashchange
那么方便《JavaScript如何实现history路由变化监听》 - 同时不会像hash有一个#,更加的美观。但是history路由需要服务器的支持,并且需将所有的路由重定向倒根页面。
hash和history区别
- hash模式下,仅
hash
符号之前的内容会被包含在请求中,如 http://www.abc.com/#asdd 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误; - history模式下,前端的url必须和实际后端发起请求的url一致,如http://www.abc.com/book/id 。如果后端缺少对/book/id 的路由处理,将返回404错误。所以要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个index.html 页面,这个页面就是你 app 依赖的页面。
5、Vue 钩子函数执行顺序
beforeCreate
:此钩子函数发生在实例创建之前,此时data,methods,computed,watch未初始化,观测数据和事件初始化完成,created
:此钩子函数data,methods,watch数据初始化完成;实例未挂载beforemount
:此钩子函数内就运用了dom虚拟技术 即是先占位置 数据不更新(操作dom时拿不到数据),el未初始化完成mounted
:el实例挂载到dom上,此时可以获取DOM
节点,$ref
属性可以访问,computed的属性,当在mounted或者dom中使用到时,才会属性的执行代码。beforeupdate
:此函数发生在视图dom数据更新之前,dom虚拟和下载补丁之前,即data更新。但操作dom时拿不到数据,updated
:视图更新完成,beforedestory
:此时实例仍未销毁,this可以使用destoryed
:实例销毁完成,
6、vue3.0相对vue2.0有什么变化或者改进
- 重写虚拟DOM
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的支持
computed原理
- computed 本质是一个惰性求值的观察者。
- 在
initComputed
时,会遍历computed里面的每个属性,并创建对应的Watcher
- 其中在新建
watcher
的时候,传入lazy
,作用是把计算结果缓存起来,而不是每次使用都要重新计算 - 其内部通过
this.dirty
属性标记计算属性是否需要重新求值。如果为 true,表示缓存脏了,需要重新计算,否则不用 - 当
computed
的依赖状态发生改变时,就会通知这个惰性的watcher
, - 判断
watcher.dirty
,只有dirty
为 true 的时候,才会执行evaluate
重新计算值,进行派发更新,并重置dirty
为false
,表示缓存已更新
7、computed 和 watch 的差异
computed
是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而watch
是监听已经存在且已挂载到 vm 上的数据,所以用watch
同样可以监听computed
计算属性的变化(其它还有 data、props)computed
具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数,而watch
则是页面重新渲染时值不变化也会执行- 从使用场景上说,
computed
适用一些简单的表达式求值,而 watch 适用于需要监听某数据的变化从而执行一些复杂的操作(异步请求等等)
用 computed
和 写一个method
去调用有什么区别:
- computed是响应式的,methods并非响应式
- 调用方式不一样,computed定义的成员像属性一样访问,methods定义的成员必须以函数形式调用
{ test() }
。 - computed是带缓存的,只有其引用的响应式属性发生改变时才会重新计算,而methods里的函数在每次视图更新时都要执行
- computed中的成员可以只定义一个函数作为只读属性,也可以定义get/set变成可读写属性,这点是methods中的成员做不到的
8、vue项目的运行流程
《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
组件就是我们页面显示的内容- 然后根据路由配置,确定根页面是显示哪个组件。
9、双向绑定、单向数据流
双向绑定:
双向绑定就是model
的更新会触发view
的更新,view
的更新会触发model
的更新,它们的作用是相互的,同步的
所以我们需要关注的是:
- 如何检测到数据
model
的变化然后通知我们去更新视图view
- 如何检测到视图
view
的变化然后去更新数据model
。 - 检测视图这个比较简单,利用事件的监听即可。比如
input
标签监听@input
事件 - 然后检测数据这个就是我们平时说的
响应式原理
的分析了
单向数据流(Prop):
《深入理解 Vue 单向数据流》、《理解vue的单向数据流》、《props浅拷贝》
- Vue是单向数据流,不是双向绑定
V-module
双向绑定不过是语法糖Object.definePropert
是用来做数据响应式更新的
从v-model
的分析我们可以这么理解,双向数据绑定就是在单向绑定的基础上给可输入元素(input、textare等)添加了 change(input) 事件,来动态修改 model 和 view ,即通过触发($emit)父组件的事件来修改mv来达到 mvvm 的效果。
所有的prop
都使得其父子prop
之间形成了一个单向下行绑定:父级prop
的更新会向下流动到子组件中,但是反过来则不行,这样会防止从子组件意外改变父级组件的状态。当开发者尝试这样做的时候,vue 将会报错。这样做是为了组件间更好的解耦,在开发中可能有多个子组件依赖于父组件的某个数据,假如子组件可以修改父组件数据的话,一个子组件变化会引发所有依赖这个数据的子组件发生变化,所以 vue 不推荐子组件修改父组件的数据,在子组件里面直接修改 props
会抛出警告。
这里有两种常见的试图改变一个 prop 的情形:
- 如果传递的prop仅仅用作展示,不涉及修改,则在模板中直接使用即可
- 如果需要对prop的值进行转化然后展示,则应该使用computed计算属性
- 如果prop的值用作初始化,应该定义一个子组件的data属性并将prop作为其初始值
以上两种情况,传递的值都是基本数据类型,但是大多数情况下,我们需要向子组件传递一个引用类型数据
注意:在 JavaScript 中对象
和数组
是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态(底层是直接浅拷贝的)
单向数据流:子组件不能直接改变父组件传给你的数据,只能通过事件触发$emit
通知父组件,并在父组件中修改原始的prop
数据;父级 prop 的更新会向下流动到子组件中,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值
10、数据响应式更新原理(data->View)
数据劫持 + 发布订阅模式
在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()
来更新数据
11、Vue 组件 data 为什么必须是函数
因为 JS 本身的特性带来的,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到所有 Vue 实例的数据。如果将 data 作为一个函数返回一个对象,那么每一个实例的 data 属性都是独立的,不会相互影响了。
12、Vue 中怎么自定义指令
作用:代码复用和抽象
全局注册:
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
局部注册:
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}
13、如何封装一个组件、插槽slot(腾讯一面)
《论如何用Vue实现一个弹窗-一个简单的组件实现》、《Vue插槽》、《Vue 插槽使用(通俗易懂)》
- 需求分析=>详细设计=>代码实现
- 主要是结合
props
和emit
来实现父子组件值的传递,有时候还会配合slot
插槽
作用域插槽:
- 使用情景:
child
组件在很多地方会被调用,希望在不同的地方调用child
的组件时,子组件列表的样式不是child
组件控制的,而是外部child模版站位符即父组件告诉我们子组件的每一项该如何渲染 - 作用域插槽绑定了一套数据,父组件可以拿来用。于是,情况就变成了这样:样式父组件说了算,但里面的数据是由子组件插槽绑定的
13、Vuex(vue的状态管理)
《vuex直接修改state 与 用dispatch/commit来修改state的差异》
Vuex使用场景:为什么不用局部或者全局变量
- 不用局部变量:跨组件方便传输
- 不用全局变量:为了模块化编程
使用场景:
- 多个组件依赖于同一状态时。
- 来自不同组件的行为需要变更同一状态。
有什么缺点:
- 刷新浏览器,vuex中的state会重新变为初始状态(用
localstorage
存储) - 非严格模式的话,直接通过
this.$store.state.变量 = xxx
修改state不会报错,但是这样就记录不到每一次state的变化,无法保存状态快照,这样错误就很难定位了 - 因此都是使用
this.$store.commit(commitType, payload)
来修改state,而不能直接修改state,状态的每次改变都很明确且可追踪,这样可以以便于调试
Vuex 状态的所有改变都必须在 store 的 mutation handler (变更句柄)中管理。(action也是通过mutation,即commit
的方式)
mutation和action的区别
Mutation
:必须同步执行。更改 Vuex 的 store 中的状态的唯一方法是提交 mutation;this.$store.commit
Action
:不能直接变更state
状态,提交的还是mutation
。可以包含任意异步操作;this.$store.dispatch
Vue 中的 key 到底有什么用?
-
key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)
-
diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.
-
更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。
-
更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)
vm.$set()实现原理是什么?
- 如果目标是数组,使用 vue 实现的变异方法
splice
实现响应式 - 如果目标是对象,判断属性存在,即为响应式,直接赋值
- 如果 target 本身就不是响应式,直接赋值,则调用 defineReactive 方法进行响应式处理
- 最后执行
dep.notify()
进行更新
Vue 的渲染过程
1.、new Vue,执行初始化
2、调用 compile 函数,挂载$mount
方法,通过自定义render方法、template或者el等生成render函数 ,编译过程如下:
- parse 函数解析 template,生成 ast(抽象语法树)
- optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
- generate 函数生成 render 函数字符串
2、调用 new Watcher
函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
3、调用 patch
方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素