1、组件通信

父子组件:props/$emit/$parent/$attrs

props & $emit:这是最最常用的父子组件通信方式,父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的

$attrs & listeners : 第一种方式处理父子组件之间的数据传输有一个问题:如果多层嵌套,父组件A下面有子组件B,组件B下面有组件C,这时如果组件A想传递数据给组件C怎么办呢?

如果采用第一种方法,我们必须让组件A通过prop传递消息给组件B,组件B在通过prop传递消息给组件C;要是组件A和组件C之间有更多的组件,那采用这种方式就很复杂了。从Vue 2.4开始,提供了imglisteners来解决这个问题,能够让组件A之间传递消息给组件C。

兄弟组件:$parent/$root$/eventbus/vuex

新建一个Vue事件bus对象,然后通过bus.emit触发事件,bus.on监听触发的事件

跨层级:eventbus/vuex/provide&inject

父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据,只要在父组件的生命周期内,子组件都可以调用。

v-model:

父组件通过v-model传递值给子组件时,会自动传递一个value的prop属性,在子组件中通过this.$emit(‘input',val)自动修改v-model绑定的值

attrs和listeners

解决多层嵌套情况下,父组件A下面有子组件B,组件B下面有组件C,组件A传递数据给组件B的问题

Vue.component('C',{ 
     template:` 
     <div> 
     <input type="text" v-model="$attrs.messageC" @input="passCData($attrs.messageC)"> 
     </div> 
     `, 
     methods:{ 
         passCData(val){ 
             //触发父组件A中的事件 
             this.$emit('getCData',val) 
         } 
     } 
}) 

Vue.component('B',{ 
 data(){ 
     return { 
         myMessage:this.message 
     } 
 }, 
 template:` 
 <div> 
 <input type="text" v-model="myMessage" @input="passData(myMessage)"> 
 <C v-bind="$attrs" v-on="$listeners"></C> 
 </div> 
 `, 
 //得到父组件传递过来的数据 
 props:['message'], 
 methods:{ 
     passData(val){ 
         //触发父组件中的事件 
         this.$emit('getChildData',val) 
     } 
 } 
}) 

Vue.component('A',{ 
 template:` 
 <div> 
 <p>this is parent compoent!</p> 
 <B  
 :messageC="messageC"  
 :message="message"  
 v-on:getCData="getCData"  
 v-on:getChildData="getChildData(message)"> 
 </B> 
 </div> 
 `, 
 data(){ 
     return { 
         message:'Hello', 
         messageC:'Hello c' 
     } 
 }, 
 methods:{ 
     getChildData(val){ 
         console.log('这是来自B组件的数据') 
     }, 
     //执行C子组件触发的事件 
     getCData(val){ 
            console.log("这是来自C组件的数据:"+val) 
     } 
 } 
}) 
var app=new Vue({ 
 el:'#app', 
 template:` 
 <div> 
 <A></A> 
 </div> 
 ` 
}) 

2、v-if和v-for的优先级

vue:2 v-for优先级高于v-if

Vue3:v-if优先级高于v-for

3、生命周期函数

创建前后、挂载前后、更新前后、销毁前后

Vue2: beforeCreate created beforeMount mounted beforeUpdate updated beforeDestory destoryed (组件缓存:activated/deactivated)

Vue3:beforeCreate created beforeMount mounted beforeUpdate updated

beforeUnmount unmounted ()

Component lifecycle diagram

beforeCreate:通常用于插件开发中执行一些初始化任务

created:组件初始化完毕,可以访问各种数据,获取接口数据等

mounted:dom已创建,可用于获取访问数据和dom元素;访问子组件等。

beforeUpdate:此时view层还未更新,可用于获取更新前各种状态

updated:完成view层的更新,更新后,所有状态已是最新

beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消

unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

4、响应式的理解

所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制

MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。

以vue为例说明,通过数据响应式加上虚拟DOM和patch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。

vue2中的数据响应式会根据数据类型来做不同处理,如果是**对象则采用Object.defineProperty()*的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是*数组则通过覆盖数组对象原型的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题。

为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6的Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了。

5、虚拟DOM

虚拟DOM是什么?

虚拟的DOM对象,它本身技术一个JavaScript对象,只不过它是通过不同的属性去描述一个视图结构

引入虚拟DOM的好处

(1)将真实元素节点抽象成VNode,有效减少直接操作DOM次数,从而提升程序性能

(2)方便跨平台

虚拟DOM如何生成,又如何成为DOM?

在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。

虚拟DOM在后续diff中的作用?

挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图。

6、diff算法

diff算法是干什么的?

vue中的diff算法成为patching算法,由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patching 方法转换

diff算法的必要性?

降低watcher粒度,每个组件只有一个watcher与之对应,此时就需要patching算法才能精确找到发生改变的地方并高效更新

diff算法何时执行?

组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作。

diff算法执行方式?

patch过程是一个递归过程,遵循深度优先、同层比较的策略

Vue3优化了什么?

新的更新策略:编译期优化patchFlags、block等

7、如何定义动态路由?怎么获取传过来的动态参数?

动态路由:将给定匹配模式的路由映射到同一个组件

如何定义和使用动态路由?

我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数

如何获取参数?

this.$route.params

多个参数?

$route.params $route.query $route.hash

8、从零写一个Vue路由,思路是什么?

思路:

首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。

  • 借助hash或者history api实现url跳转页面不刷新
  • 同时监听hashchange事件或者popstate事件处理跳转
  • 根据hash值或者state值从routes表中匹配对应component并渲染之

首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事:

  • 保存用户传入的配置项
  • 监听hash或者popstate事件
  • 回调里根据path匹配对应路由

将router定义成一个Vue插件,即实现install方法,内部做两件事:

  • 实现两个全局组件:router-link和router-view,分别实现页面跳转和内容显示
  • 定义两个全局变量:$route和$router,组件内可以访问当前路由和路由器实例

9、key的作用?

key的作用主要是为了更高效的更新虚拟DOM。

需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点。主要是为了高效的更新虚拟DOM。

vue在patch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能。

实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果。

从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的。

10、nextTick的使用和原理?

nextTick是DOM循环更新后的一次延迟回调(Vue3文档:等待下一次DOM更新刷新的工具方法)

Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick。

开发场景:

created想要获取DOM时

响应式数据变化后获取DOM更新后的状态

结合异步更新理解nextTick

在Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值。

11、watch和computed的区别

computed:具有响应式的返回值

watch:侦测变化,执行回调

const count = ref(1)
const plusOne = computed(()=> {
  count.value+1
})

// watch
const state = reactive({count:0})
watch(
	()=>state.count,
  (count,prevCount) => {
    
  }
)

计算属性可以从组件数据中派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computed和methods的差异是计算属性具有缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用。常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑。

使用细节:

计算属性也是可以传递对象,成为既可读又可写的计算属性。

watch可以传递对象,设置deep、immediate等选项

Vue3的变化:

watch的一些变化:不再能侦听一个点操作符之外的字符串形式的表达式;

reactivity api新出现的watch、watchEffect可以完全替代目前的watch选项,且功能更加强大。

12、Vue子组件和父组件的创建和挂载顺序?

父组件 created

子组件created

子组件mounted

父组件mounted

原因:

vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时,先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

13、如何缓存当前组件?缓存后怎么更新?

缓存组件使用keep-alive组件,keep-alive是Vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染

<keep-alive>
	<component :is="view"></component>
</keep-alive>

结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。

Vue 的缓存机制并不是直接存储 DOM 结构,而是将 DOM 节点抽象成了一个个 VNode节点,所以,keep- alive的缓存也是基于VNode节点的而不是直接存储DOM结构。

Vue3的变化:

之前是keep-alive包裹router-view,Vue3相反,router-view包裹keep-alive

<router-view v-slot="{Component}">
	<keep-alive :is="Component"></keep-alive>
</router-view>

组件缓存后更新可以利用activated或者beforeRouteEnter

beforeRouteEnter:在有vue-router的项目中,每次进入路由的时候,都会执行beforeRouteEnter

activated:在keep-alive缓存的组件被激活的时候,都会执行actived钩子

beforeRouteEnter(to, from, next){
  next(vm=>{
    console.log(vm)
    // 每次进入路由执行
    vm.getData()  // 获取数据
  })
},
  
activated(){
	  this.getData() // 获取数据
},

原理:

keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。

14、template到render处理过程

Vue中有个独特的编译器模块,称为“compiler”,它的主要作用是将用户编写的template编译为js中可执行的render函数。

之所以需要这个编译过程是为了便于前端程序员能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。

在Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们成为抽象语法树AST,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数。

15、Vue实例挂载的过程发生了什么?

挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化建立更新机制

初始化会创建组件实例、初始化组件状态,创建各种响应式数据

建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数。

16、v-model

  1. v-bind:绑定响应式数据
  2. 触发oninput 事件并传递数据

17、vuex

vuex是一种状态管理机制,将全局组件的共享状态抽取出来为一个store,以一个单例模式存在,应用任何一个组件中都可以使用,vuex更改state的唯一途径是通过mutation,mutation需要commit触发, action实际触发是mutation,其中mutation处理同步任务,action处理异步任务。

state:存储的单一状态。存储的基本数据

getters: getters是store的计算属性,对state的加工,是派生出来的数据。就像computed计算属性一样,getter返回的值会根据它的依赖被缓存起来,且只有当它的依赖值发生改变才会被重新计算。

mutations:mutations提交更改数据,使用store.commit方法更改state存储的状态。(mutations同步函数)

actions: 像一个装饰器,提交mutation,而不是直接变更状态。(actions可以包含任何异步操作)

Module是store分割的模块,每个模块拥有自己的state、getters、mutations、actions。

Vuex提供了mapState、MapGetters、MapActions、mapMutations等辅助函数给开发在vm中处理store。

18、路由守卫

(1)全局前置守卫:

const router = new VueRouter({...})
router.beforeEach((to,from,next) => {
  ...
})

当一个导航开始时,全局前置守卫按照注册顺序调用。守卫是异步链式调用的,导航在最后的一层当中

(2) 全局后置守卫

router.afterEach((to, from) => {
    // 你并不能调用next
  // ...
})

(3)路由独享守卫:在路由内写守卫

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

(4)组件内的守卫

beforeRouteEnter

beforeRouteUpdate

beforeRouteLeave

const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 路由被 confirm 前调用
    // 组件还未渲染出来,不能获取组件实例 `this`
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`,一般用来数据获取。
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

小结:

  • 导航被触发。
  • 在准备离开的组件里调用 beforeRouteLeave 守卫。
  • 调用全局的 beforeEach 守卫。
  • 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。(如果你的组件是重用的)
  • 在路由配置里调用 beforeEnter。
  • 解析即将抵达的组件。
  • 在即将抵达的组件里调用 beforeRouteEnter。
  • 调用全局的 beforeResolve 守卫 (2.5+)。
  • 导航被确认。
  • 调用全局的 afterEach 钩子。
  • 触发 DOM 更新。
  • 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

19、路由守卫进行判断登录

在vue项目中,切换路由时肯定会碰到需要登录的路由,其原理就是在切换路径之前进行判断,你不可能进入页面再去判断有无登录重新定向到login,那样的话会导致页面已经渲染以及它的各种请求已经发出。

20、vue-router路由懒加载

结合 Vue 的异步组件 (opens new window)和 Webpack 的代码分割功能 (opens new window),可以实现路由组件的懒加载

21、js是如何监听HistoryRouter的变化的

  1. 通过hash改变,利用window.onhashchange 监听。

  2. **HistoryRouter:**通过history的改变,进行js操作加载页面,然而history并不像hash那样简单,因为history的改变,除了浏览器的几个前进后退(使用 history.back(), history.forward()和 history.go() 方法来完成在用户历史记录中向后和向前的跳转。)等操作会主动触发popstate 事件,pushState,replaceState 并不会触发popstate事件,要解决history监听的问题,

首先完成一个订阅-发布模式,然后重写history.pushState, history.replaceState,并添加消息通知,这样一来只要history的无法实现监听函数就被我们加上了事件通知,只不过这里用的不是浏览器原生事件,而是通过我们创建的event-bus 来实现通知,然后触发事件订阅函数的执行。

22、hash和history路由模式的区别和原理

hash模式:

hash模式的工作原理是hashchange事件,可以在window监听hash的变化。我们在url后面随便添加一个#xx触发这个事件。vue-router默认的是hash模式—使用URL的hash来模拟一个完整的URL,于是当URL改变的时候,页面不会重新加载,也就是单页应用了,当#后面的hash发生变化,不会导致浏览器向服务器发出请求,浏览器不发出请求就不会刷新页面,并且会触发hasChange这个事件,通过监听hash值的变化来实现更新页面部分内容的操作

对于hash模式会创建hashHistory对象,在访问不同的路由的时候,会发生两件事: HashHistory.push()将新的路由添加到浏览器访问的历史的栈顶,和HasHistory.replace()替换到当前栈顶的路由

history模式:

主要使用HTML5的pushState()和replaceState()这两个api结合window.popstate事件(监听浏览器前进后退)来实现的,pushState()可以改变url地址且不会发送请求,replaceState()可以读取历史记录栈,还可以对浏览器记录进行修改

23、Vue 通过数据劫持可以精准探测数据在具体dom上的变化,为什么还需要虚拟DOM?

熟悉Vue的响应式系统就知道,通常一个绑定一个数据就需要一个Watcher,一但我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff获取更加具体的差异,而Virtual Dom Diff则是pull操作,Vue是push+pull结合的方式进行变化侦测的。

24、new Vue后发生的事

  1. new Vue会调用 Vue 原型链上的_init方法对 Vue 实例进行初始化;
  2. 首先是initLifecycle初始化生命周期,对 Vue 实例内部的一些属性(如 children、parent、isMounted)进行初始化;
  3. initEvents,初始化当前实例上的一些自定义事件(Vue.$on);
  4. initRender,解析slots绑定在 Vue 实例上,绑定createElement方法在实例上;
  5. 完成对生命周期、自定义事件等一系列属性的初始化后,触发生命周期钩子beforeCreate;
  6. initInjections,在初始化data和props之前完成依赖注入(类似于 React.Context);
  7. initState,完成对data和props的初始化,同时对属性完成数据劫持内部,启用监听者对数据进行监听(更改);
  8. initProvide,对依赖注入进行解析;
  9. 完成对数据(state 状态)的初始化后,触发生命周期钩子created;
  10. 进入挂载阶段,将 vue 模板语法通过vue-loader解析成虚拟 DOM 树,虚拟 DOM 树与数据完成双向绑定,触发生命周期钩子beforeMount;
  11. 将解析好的虚拟 DOM 树通过 vue 渲染成真实 DOM,触发生命周期钩子mounted;

25、Vue首屏白屏的解决方式

  1. 路由懒加载
  2. vue-cli开启打包压缩 和后台配合 gzip访问
  3. 进行cdn加速
  4. 开启vue服务渲染模式
  5. 用webpack的externals属性把不需要打包的库文件分离出去,减少打包后文件的大小
  6. 在生产环境中删除掉不必要的console.log
  7. 添加loading动画

26、vue.$set的实现原理

  1. 如果目标是数组,直接使用数组的 splice 方法触发相应式;
  2. 如果目标是对象,会先判读属性是否存在、对象是否是响应式,
  3. 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理