Vue的深入理解

《12道vue高频原理面试题,你能答出几道?》

文章目录

    • 1、spa(单页面)与多页面
    • 2、前端工程化MVC和MVVM
        • MVC
        • MVVM
    • 3、Vue组件间通信方式
    • 4、前端路由hash和history
        • hash
        • history(h5的API)
    • 5、Vue 钩子函数执行顺序
    • 6、vue3.0相对vue2.0有什么变化或者改进
    • computed原理
    • 7、computed 和 watch 的差异
    • 8、vue项目的运行流程
    • 9、双向绑定、单向数据流
        • 双向绑定:
        • 单向数据流(Prop):
    • 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,所以叫单页面!

通俗点来讲,在应用整个使用流程里浏览器由始至终没有刷新,所有的数据交互由routerajax完成。但是用户体验起来和app一样,有明确的页面区分,即所谓的web app。

单页面应用优点:

  1. 分离前后端关注点,前端负责界面显示,后端负责数据存储和计算,不会把前后端的逻辑混杂在一起,由此也减轻了服务端的压力,服务器只需要提供数据给前端就可以
  2. 同一套后端程序代码,不用修改就可以用于Web界面、手机移动端、平板等多种客户端
  3. 用户体验好,快,内容的改变不需要重新加载整个页面,Web应用更具响应性,页面切换快

单页面应用缺点:

  1. 首屏时间稍慢
    单页应用的首屏时间慢,首屏时需要请求一次html,同时还要发送一次js请求,两次请求回来了,首屏才会展示出来。
  2. 不利于SEO(搜索引擎优化效果)问题,现在可以通过Prerender等技术解决一部分
    因为搜索引擎只认识html里的内容,不认识js的内容,而单页应用的内容都是靠js渲染生成出来的,搜索引擎不识别这部分内容,也就不会给一个好的排名,会导致单页应用做出来的网页在百度和谷歌上的排名差
  3. 导航不可用,如果一定要导航需要自行实现前进、后退,需要程序来实现管理
  4. 使用脚本修改页面,这个脚本我们都知道,他的兼容性是个大问题、

多页面:

每一次页面跳转的时候,后台服务器都会给返回一个新的html文档,这种类型的网站也就是多页网站,也叫做多页应用

优点:

  1. 首屏时间快
    首屏时间叫做页面首个屏幕的内容展现的时间,当我们访问页面的时候,服务器返回一个html,页面就会展示出来,这个过程只经历了一个HTTP请求,所以页面展示的速度非常快。
  2. SEO效果好
    搜索引擎在做网页排名的时候,要根据网页内容才能给网页权重,来进行网页的排名。搜索引擎是可以识别html内容的,而我们每个页面所有的内容都放在Html中,所以这种多页应用,seo排名效果好

缺点:
3. 页面切换慢
因为每次跳转都需要发出一个http请求HTML,如果网络比较慢,在页面之间来回跳转时,就会发现明显的卡顿

2、前端工程化MVC和MVVM


Vue.js 和 MVVM

MVC

MVC 即 Model-View-Controller 的缩写,就是 模型-视图-控制器 , 也就是说一个标准的Web 应用程式是由这三部分组成的:

  1. View :UI布局,展示数据。
  2. Model :管理数据。
  3. Controller :响应用户操作,并将 Model 更新到 View 上。

存在问题:

  1. 开发者在代码中大量调用相同的 DOM API, 处理繁琐 ,操作冗余,使得代码难以维护。(后来出现jq解决此问题)
  2. 大量的DOM 操作使页面渲染性能降低,影响用户体验。
  3. 当 Model 频繁发生变化,开发者需要主动更新到View ;当用户的操作导致 Model 发生变化,开发者同样需要将变化的数据同步到Model 中,这样的工作不仅繁琐,而且很难维护复杂多变的数据状态。

MVVM的出现完美解决了以上问题

MVVM

MVVM 由 Model,View,ViewModel 三部分构成

  1. Model 层代表数据模型
  2. View 代表UI 组件
  3. 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最新APIVue.observable 优化响应式 provide(推荐)

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)

hash和history区别

  1. hash模式下,仅hash符号之前的内容会被包含在请求中,如 http://www.abc.com/#asdd 因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回404错误;
  2. history模式下,前端的url必须和实际后端发起请求的url一致,如http://www.abc.com/book/id 。如果后端缺少对/book/id 的路由处理,将返回404错误。所以要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个index.html 页面,这个页面就是你 app 依赖的页面。

5、Vue 钩子函数执行顺序


Vue生命周期与Vue.nextTick()使用

  1. beforeCreate:此钩子函数发生在实例创建之前,此时data,methods,computed,watch未初始化,观测数据和事件初始化完成,
  2. created:此钩子函数data,methods,computed,watch数据初始化完成;实例未挂载
  3. beforemount:此钩子函数内就运用了dom虚拟技术 即是先占位置 数据不更新(操作dom时拿不到数据),el未初始化完成
  4. mounted:el实例挂载到dom上,此时可以获取DOM节点,$ref属性可以访问
  5. beforeupdate:此函数发生在视图dom数据更新之前,dom虚拟和下载补丁之前,即data更新。但操作dom时拿不到数据,
  6. updated:视图更新完成,
  7. beforedestory:此时实例仍未销毁,this可以使用
  8. 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原理

《Vue原理Computed 》

  • computed 本质是一个惰性求值的观察者。
  • initComputed时,会遍历computed里面的每个属性,并创建对应的Watcher
  • 其中在新建 watcher 的时候,传入 lazy,作用是把计算结果缓存起来,而不是每次使用都要重新计算
  • 其内部通过 this.dirty 属性标记计算属性是否需要重新求值。如果为 true,表示缓存脏了,需要重新计算,否则不用
  • computed 的依赖状态发生改变时,就会通知这个惰性的watcher,
  • 判断watcher.dirty,只有 dirty 为 true 的时候,才会执行 evaluate重新计算值,进行派发更新,并重置 dirtyfalse,表示缓存已更新

7、computed 和 watch 的差异

  1. computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm 上的数据,所以用watch同样可以监听computed计算属性的变化(其它还有 data、props)
  2. computed具有缓存性,页面重新渲染值不变化,计算属性会立即返回之前的计算结果,而不必再次执行函数,而watch 则是页面重新渲染时值不变化也会执行
  3. 从使用场景上说,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的流程梳理》

  1. 首先加载index.htmlmain.js文件
  2. main.js文件中给id=“app”的div创建一个Vue的实例,该实例中有一个名叫APP的组件
  3. APP组件就是我们页面显示的内容
  4. 然后根据路由配置,确定根页面是显示哪个组件。

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)


《ceq数据响应式笔记》

数据劫持 + 发布订阅模式

initData()里面会执行observe(data),会遍历data里面的属性,依次执行defineReactive()这个核心函数,通过defineReactive()为每个属性都新建一个dep对象(发布-订阅模式的中介者),并且调用Object.defineProperty()方法,会给data的每个属性添加gettersetter,以此来达到依赖收集派发更新的目的

依赖收集

每个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 插槽使用(通俗易懂)》

  • 需求分析=>详细设计=>代码实现
  • 主要是结合propsemit来实现父子组件值的传递,有时候还会配合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()实现原理是什么?

  1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
  2. 如果目标是对象,判断属性存在,即为响应式,直接赋值
  3. 如果 target 本身就不是响应式,直接赋值,则调用 defineReactive 方法进行响应式处理
  4. 最后执行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 元素

你可能感兴趣的:(Vue)