Vue知识点

1,139 阅读18分钟

1. vue的生命周期

  • beforeCreate : eldata 并未初始化。是获取不到props或者data中的数据的
  • created :已经可以访问到之前不能访问的数据,但是这时候组件还没被挂载,所以是看不到的。
  • beforeMount:开始创建虚拟DOM
  • mounted:将虚拟DOM渲染为真实DOM,并且渲染数据。组件中如果有子组件的话,会递归加载子组件,只有当所有子组件全部挂载完毕,才会执行根组件的挂载钩子。
  • beforeUpdate:当组件或实例的数据更改之后,会立即执行beforeUpdate,然后vue的虚拟dom机制会重新构建虚拟dom与上一次的虚拟dom树利用diff算法进行对比之后重新渲染,一般不做什么事儿
  • updated:当更新完成后,执行updated,数据已经更改完成,dom也重新render完成,可以操作更新后的虚拟dom
  • beforeDestroy: 移除事件、定时器等,否则可能会引起内存泄漏。
  • destroyed:会进行一系列的销毁操作,如果有子组件的话,也会递归销毁子组件,所有子组件都销毁完毕后才会执行根组件的destroyed钩子函数

另外还有keep-alive独有的生命周期,分别为activateddeactivated。用keep-alive包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行deactivated函数,命中缓存渲染后会执行actived钩子函数。如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,可以使用keep-alive组件包裹需要保存的组件。<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。

keep-alive不会在函数式组件中正常工作,因为它们没有缓存实例keep-alive有三个 Props

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。

问题:Keep alive中的数据怎么更新

  • activated中,对数据进行更改
  • beforeRouteUpdate

Vue 的父组件和子组件生命周期钩子执行顺序是什么

  • 加载渲染过程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
  • 子组件更新过程 父beforeUpdate->子beforeUpdate->子updated->父updated
  • 父组件更新过程 父beforeUpdate->父updated
  • 销毁过程 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

2.组件通信

  • 父子组件通信
    • 父组件传props给子组件,子组件通过emit发送事件传递数据给父组件

    • 通过访问$parent$children对象来访问组件实例中的方法和数据

    • $listeners.sync

      • 包含了父作用域中的 (不含.native修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用

      • .sync 修饰符:它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器。当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定

      注意带有 .sync 修饰符的v-bind不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的 property 名,类似 v-model

      当我们用一个对象同时设置多个 prop 的时候,也可以将这个 .sync 修饰符和 v-bind配合使用:<text-document v-bind.sync="doc"></text-document>

      • v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。
  • 兄弟组件通信:通过查找父组件中的子组件实现,也就是this.$parent.$children,在children中可以通过组件name查询到需要的组件实例,然后进行通信
  • 跨多层级组件通信:provide/inject
  • 任意组件:vuex

3.computed和watch区别

  • computed是计算属性,依赖其他属性计算值,并且computed的值有缓存,只有当计算值变化才会返回内容。
  • watch监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作 所以需要依赖别的属性来动态获得值的时候可以使用computed,对于监听到值的变化需要做一些复杂业务逻辑的情况可以使用watchwatch不缓存,可以执行异步方法

关于它的原理可参考:计算属性 VS 侦听属性

4.mixin和mixins区别

  • mixin用于全局混入,会影响到每个组件实例
      Vue.mixin({
          beforeCreat(){
              #...逻辑
              # 这种方式会影响到每个组件的beforeCreat钩子函数
          }
      })
    
  • mixins:如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过mixins混入代码,比如上拉下拉加载数据这种逻辑等等。PS.mixins混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择行性的合并。

5.AST

vue会通过编译器将模板通过几个阶段最终编译为render函数,然后通过执行render函数生成Virtual DOM最终映射为真实DOM,分为三个阶段:

  • 1.将模版解析为AST:通过各种各样的正则表达式去匹配模板中的内容,然后将内容提取出来做各种逻辑操作,然后生成一个最基本的AST对象
  • 2.优化AST:将永远不会变动的节点提取出来,实现复用Virtual DOM,跳过对比算法的功能
  • 3.将AST转换为render函数:遍历整个AST,根据不同的条件生成不同的代码

引申1:组件更新

  • 组件更新的过程核心就是新旧 vnode diff,对新旧节点相同以及不同的情况分别做不同的处理。新旧节点不同的更新流程是创建新节点->更新父占位符节点->删除旧节点;而新旧节点相同的更新流程是去获取它们的 children,根据不同情况做不同的更新逻辑。最复杂的情况是新旧节点相同且它们都存在子节点,那么会执行updateChildren逻辑

引申2:parse

  • parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

  • AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。其实这里我觉得源码写的不够友好,这种是典型的魔术数字,如果转换成用常量表达会更利于源码阅读

Diff算法

  • 根据真实DOM生成virtual DOM,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后VnodeoldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode
  • virtual DOM是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构
  • diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。 在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较

diff流程图如下 diff算法图

参考链接:详解vue的diff算法

6.vue的双向绑定原理

cn.vuejs.org/v2/guide/re…

  • dep类中有两个方法:addSub(添加依赖)和notify(触发更新,调用了watcher中的update方法)
  • defineReactive最开始初始化Dep 对象的实例,对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 gettersetter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter(将watcher添加到订阅,执行了depaddSub方法) 和 setter(执行watcherupdate方法)
function observe(obj){
    //判断类型
    if(!obj||typeof obj!=='object'){
        return 
    }
    Object.keys(obj).forEach(key=>{
        defineReactive(obj,key,obj[key])
    })
}

演示:

var data={name:'aa'}
observe(data)//手动触发一次属性的getter来实现依赖收集
function update(value){
    document.querySelector('div').innerText=value
}
new Watcher(data,'name',update)//触发属性的getter添加监听
data.name='yyy'//触发属性的setter更新

针对给对象新增属性并不会触发组件的重新渲染问题,可以使用一个set方法,它主要做了这些:

  • 判断是否为数组且下标是否有效---调用target.splice(key,1,val)方法触发更新
//验证数组索引是否是一个非无穷大的正整数
function isValidArrayIndex (val) {
 var n = parseFloat(String(val));
 return n >= 0 && Math.floor(n) === n && isFinite(val)//Math.floor(n) === n验证是否是整数 
}
  • 判断key是否已经存在,存在的话target[key]=val
  • 如果对象不是响应式对象就赋值返回target[key]=val
  • 进行双向数据绑定defineReactive(ob.value,key,val),手动派发更新op.dep.notify() 针对通过下标修改数组数据并不会触发组件的重新渲染问题,Vue内部重写了一些数组函数实现派发更新

引申:Vue响应式原理-如何监听Array的变化

  1. 先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。
  2. Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
  3. 把需要被拦截的 Array 类型的数据原型指向改造后原型。

我们将代码进行下改造,拦截的过程中还是要将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变,然后我们再去做视图的更新等操作。

7.Vue.set、vm.$set作用

  • 向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。

  • 它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')。

  • PS.注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象

  • 用法:

    • 对于对象:
      • 对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property
      • 有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign()_.extend()。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
    • 对于数组:
      • 当利用索引直接设置一个数组项时:Vue.set(vm.items, indexOfItem, newValue)或者vm.items.splice(indexOfItem, 1, newValue)
      • 当修改数组的长度时:用splice---vm.items.splice(newLength)
  • 引申:

    set派发更新
    • 队列排序
      1. 组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
      2. 用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher是在渲染watcher 之前创建的。
      3. 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行
    • 队列遍历
      • 在对 queue 排序后,接着就是要对它做遍历,拿到对应的 watcher,执行 watcher.run()。这里需要注意一个细节,在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher,这样会再次执行到 queueWatcher

8.前端路由原理?两种实现方式有什么区别?

vue的路由模式hash依赖于window.onhashchange

Hash模式History模式
只可以更改#后面的内容可以通过API设置任意的同源URL
历史记录只能更改哈希值,也就是字符串可以添加任意类型的数据到历史记录中
后端无需后端配置,兼容性好在用户手动输入地址或者刷新页面的时候会发起URL请求,后端需要配置index.html页面用于匹配不到静态资源的时候

HTML5的History API为浏览器的全局history对象增加的扩展方法。一般用来解决ajax请求无法通过回退按钮回到请求前状态的问题

在HTML5中,window.history对象得到了扩展,新增的API包括:

  • history.pushState(data[,title][,url])//向历史记录中追加一条记录
  • history.replaceState(data[,title][,url])//替换当前页在历史记录中的信息。
  • history.state//是一个属性,可以得到当前页的state信息
  • window.onpopstate//是一个事件,在点击浏览器后退按钮或**js调用forward()、back()、go()**时触发。监听函数中可传入一个event对象,event.state即为通过pushState()replaceState()方法传入的data参数。用history.pushState()或者history.replaceState()不会触发popstate事件
//比如
window.onpopstate = function(event) {
  console.log("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
history.back(); // Logs "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // Logs "location: http://example.com/example.html, state: null
history.go(2);  // Logs "location: http://example.com/example.html?page=3, state: {"page":3}

详情请查看:深入理解前端中的 hash 和 history 路由

9.路由

  • this.$route.params动态路由匹配
  • $route$router
    • $route 表示当前路由信息对象
    • $router对象:全局的路由实例,是router构造方法的实例。
  • 路由守卫有哪些:守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中
  • 全局导航
    • beforeEach((to, from, next) => {// ...})(全局前置守卫):确保 next 函数在任何给定的导航守卫中都被严格调用一次
    • beforeResolve(全局解析守卫):在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。
    • afterEach((to, from) => {//...})(全局后置钩子):不会接受next函数也不会改变导航本身
    • beforeEnter: (to, from, next) => {// ...}:可以在路由配置上直接定义 beforeEnter守卫
  • 组件内的守卫
    • beforeRouteEnter((to, from, next) => {// ...}):守卫执行前,组件实例还没被创建。不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数(支持给 next 传递回调的唯一守卫)
    • beforeRouteUpdate((to, from, next) => {// ...}):在当前路由改变,但是该组件被复用时调用,可以访问组件实例 this
    • beforeRouteLeave((to, from, next) => {// ...}):导航离开该组件的对应路由时调用,可以访问组件实例 this。通常用来禁止用户在还未保存修改前突然离开

执行顺序:在失活的组件里调用beforeRouteLeave>全局调用beforeEach>在路由配置里调用beforeEnter>解析异步路由组件>在被激活的组件里调用 beforeRouteEnter>beforeResolve>导航被确认>afterEach>触发 DOM 更新>用创建好的实例调用beforeRouteEnter 守卫中传给 next 的回调函数

引申1:vue-router原理

1.实现一个静态install方法,因为作为插件都必须有这个方法,给Vue.use()去调用 2.可以监听路由变化 3.解析配置的路由,即解析router的配置项routes,能根据路由匹配到对应组件 4.实现两个全局组件router-linkrouter-view

参考链接:Vue-Router核心实现原理

引申2:前端路由的权限控制

可参考这篇文章:Vue 权限控制(路由验证)

10.和react的区别

ReactVue
使用v-model支持双向绑定,开发更加方便
改变数据方式setState修改状态要简单许多
页面更新渲染需要用户手动去优化vue的底层使用了依赖追踪,页面更新渲染已经是最优了
使用JSX,可以完全通过JS来控制页面,更加的灵活使用了模版语法,相比于JSX来说没有那么灵活,但是完全可以脱离工具链,通过直接编写render函数就能在浏览器中运行

11.什么是Virtual DOM?为什么Virtual DOM比原生DOM快?

首先操作DOM是很慢的(因为DOM是属于渲染引擎中的东西,而JS又是JS引擎中的东西。当我们通过JS操作DOM的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗,操作DOM次数一多,也就等同于一直在进行线程之间的通信,并且操作DOM可能还会带来重绘回流的情况,所以也就导致了性能上的问题),可以使用JS对象模拟并渲染出对应的DOM,而且通过比较DOM新旧节点的变化(DOM是一个多叉树的结构),去局部更新DOM,实现性能的最优化。初次之后还有其他优点:

  • Virtual DOM作为一个兼容层,让我们还能对接非Web端的系统,实现跨端开发
  • 同样的,通过Virtual DOM我们可以渲染到其他平台,比如实现SSR(服务器端渲染)、同构渲染等等
  • 实现组件的高度抽象化

**引申:**网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?

答:www.zhihu.com/question/31…

12.MVVM与MVC

  • 传统的MVC架构通常是使用控制器更新模型,视图从模型中获取数据去渲染。当用户有输入时,会通过控制器去更新模型,并且通知视图进行更新。
  • MVVM:引入了ViewModel的概念。ViewModel只关心数据和业务的处理,不关心View如何处理数据,在这种情况下,ViewModel都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个ViewModel中,让多个View复用这个ViewModel

13.vuexmutationaction用法

  • mutation:
    • 每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数
    • mutation 必须是同步函数
  • Action
    • Action提交的是 mutation,而不是直接变更状态。
    • Action可以包含任意异步操作。

14.介绍下nextTick

  • nextTick可以让我们在下次DOM更新循环结束之后执行延迟回调,用于获得更新后的DOM
  • $nextTick() 返回一个 Promise 对象,可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: {
  updateMessage: async function () {
    this.message = '已更新'
    console.log(this.$el.textContent) // => '未更新'
    await this.$nextTick()
    console.log(this.$el.textContent) // => '已更新'
  }
}

15.设计模式有哪些,vue单例模式怎么实现

单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个访问它的全局访问点

可以通过两种方法来实现:类和闭包

export function install (_Vue) {
  // 是否已经执行过了 Vue.use(Vuex),如果在非生产环境多次执行,则提示错误
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 如果是第一次执行 Vue.use(Vuex),则把传入的 _Vue 赋值给定义的变量 Vue
  Vue = _Vue
  // Vuex 初始化逻辑
  applyMixin(Vue)
}

Vue.use(Vuex) 的时候,会调用 install 方法,真正的 Vue 会被当做参数传入,如果多次执行 Vue.use(Vuex),也只会生效一次,也就是只会执行一次 applyMixin(Vue),所以只会有一份唯一的 Store,这就是 Vuex 中单例模式的实现。

16.Vue组件 v-model

//lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新
<base-checkbox v-model="lovingVue"></base-checkbox> 
Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

参考链接:自定义事件

17.源码中有用到object.create(null),它的作用是?

  • 你需要一个非常干净且高度可定制的对象当作数据字典的时候
  • 想节省hasOwnProperty带来的一丢丢性能损失并且可以偷懒少些一点代码的时候

Object.create(null)吧!其他时候,请用{} 参考:详解Object.create(null)

18.Vue.config.productionTip = false

设置为 false 以阻止vue在启动时生成生产提示

19.Vue性能优化

  • 首先考虑性能优化的价值、成本、指标
  • 性能优化的手段,过程---详细步骤,有一个量化的描述,比如减少了多少请求,结果怎样
  • Vue 应用运行时性能优化措施:
    • 引入生产环境的 Vue 文件
    • 使用单文件组件预编译模板
    • 提取组件的 CSS 到单独到文件
    • 利用Object.freeze()提升性能
    • 扁平化 Store 数据结构
    • 合理使用持久化 Store 数据
    • 组件懒加载:juejin.cn/post/684490…
    • 延迟埋点接口请求
    • 增加sw
    • 去除非首屏的js预加载
    • Vue全家桶使用CDN资源,并用Ngnix Combo的方式合并资源
      • nginx_http_concat,将请求合并,通过这样的方式http://example.com/??style1.css,style2.css,foo/style3.css访问合并后的资源。(注意是2个问号)
      • 同目录下JS可通过Nginx combo合并, 例:libs目录下vue.min.jsvue-router.min.jsvuex.min.js合并成一个请求下载:
    https://cdn.abc.com.cn/libs/vue/vue_2.6.10/vue.min.js,libs/vue-router/router_3.1.3/vue-router.min.js,libs/vuex/vuex_3.1.1/vuex.min.js
    
    • 埋点和分享组件和非首屏的第三方JS在页面onload事件之后加载
    • 第三方依赖不参与打包:
      config.externals = {
      vue: 'Vue',
      vuex: 'Vuex',
      'vue-router': 'VueRouter',
       axios: 'axios'
      }
      
  • Vue 应用加载性能优化措施
    • 服务端渲染 / 预渲染
    • 组件懒加载
  • 使用webpack-bundle-analyzer分析js包大小

参考链接:Vue 应用性能优化指南

20.修饰符

  • 事件修饰符
    • .stop:阻止冒泡行为,不让当前元素的事件继续往外触发,如阻止点击div内部事件,触发div事件
    • .prevent:阻止事件本身行为,如阻止超链接的点击跳转,form表单的点击提交
    • .capture:是改变js默认的事件机制,默认是冒泡,capture功能是将冒泡改为倾听模式
    • .self:只有是自己触发的自己才会执行,如果接受到内部的冒泡事件传递信号触发,会忽略掉这个信号
    • .once:是将事件设置为只执行一次,如 .click.prevent.once 代表只阻止事件的默认行为一次,当第二次触发的时候事件本身的行为会执行。.once 修饰符还能被用到自定义的组件事件上
    • .passive:滚动事件的默认行为 (即滚动行为) 将会立即触发,而不会等待 onScroll 完成。这个 .passive 修饰符尤其能够提升移动端的性能。

PS: .passive.prevent 不能一起使用:.prevent 将会被忽略

  • 按键修饰符
    • Vue 允许为 v-on在监听键盘事件时添加按键修饰符:比如<input v-on:keyup.enter="submit">
    • 可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符。比如:<input v-on:keyup.page-down="onPageDown">处理函数只会在 $event.key 等于 PageDown 时被调用
  • 系统修饰符
    • .ctrl.alt.shift.meta.exact.left.right.middle

21.vue强制更新

  • this.$forceUpdate():迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
  • Object.assign 对象改变:oldObj = Object.assign({},newObj); 原理:对象是引用类型,直接改变oldObj的某属性指向地址没变,vue不一定能监控到,所以当我们新建一个对象并赋值给oldObj字段的话,直接改变了它的指向地址
  • Vue.set

22.自定义指令

一个指令定义对象可以提供如下几个钩子函数 (均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。

钩子函数的参数有elbindingvnodeoldVnode