Vue

218 阅读6分钟

Vue响应式是怎么实现的?

整体思路是数据劫持+观察者模式

vue 初始化时会用Object.defineProperty()给data中每一个属性添加gettersetter,同时创建depwatcher进行依赖收集派发更新,最后通过diff算法对比新老vnode差异,通过patch即时更新DOM

对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的dep属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。 数据响应的实现由两部分构成: 观察者( watcher )依赖收集器( Dep ) ,其核心是 defineProperty这个方法,它可以 重写属性的 get 与 set 方法,从而完成监听数据的改变。

  • Observe (观察者)观察 props 与 state

    • 遍历 props 与 state,对每个属性创建独立的监听器( watcher )
  • 使用 defineProperty 重写每个属性的 get/set(defineReactive

    • get: 收集依赖

      • Dep.depend()

        • watcher.addDep()
    • set: 派发更新

      • Dep.notify()
      • watcher.update()
      • queenWatcher()
      • nextTick
      • flushScheduleQueue
      • watcher.run()
      • updateComponent()

image.png

image.png

为什么访问data属性不需要带data

vue中访问属性代理this.data.xxx 转换 this.xxx的实现

    /** 将 某一个对象的属性 访问 映射到 对象的某一个属性成员上 */
    function proxy( target, prop, key ) {
      Object.defineProperty( target, key, {
        enumerable: true,
        configurable: true,
        get () {
          return target[ prop ][ key ];
        },
        set ( newVal ) {
          target[ prop ][ key ] = newVal;
        }
      } );
    }

vue diff 算法

  • 只对比父节点相同的新旧子节点(比较的是Vnode),时间复杂度只有O(n)
  • 在 diff 比较的过程中,循环从两边向中间收拢 新旧节点对比过程

1、先到 不需要移动的相同节点,借助key值找到可复用的节点是,消耗最小

2、再找相同但是需要移动的节点,消耗第二小

3、最后找不到,才会去新建删除节点,保底处理

注意:新旧节点对比过程,不会对这两棵Vnode树进行修改,而是以比较的结果直接对 真实DOM 进行修改

Vue的patch是即时的,并不是打包所有修改最后一起操作DOM(React则是将更新放入队列后集中处理)

Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1)

为什么只劫持对象,而要对数组进行方法重写?

因为对象最多也就几十个属性,拦截起来数量不多,但是数组可能会有几百几千项,拦截起来非常耗性能,所以直接重写数组原型上的方法,是比较节省性能的方案

vue中针对7个数组方法的重写

Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的Observer。如果有新的值,就调用 observeArray 对新的值进行监听,然后调用 notify,通知 render watcher,执行 update

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

methodsToPatch.forEach(function(method) {
  // cache original method
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});

Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};
复制代码

Proxy 相比于 defineProperty 的优势

  • 数组变化也能监听到
  • 不需要深度遍历监听

computed和watch有何区别?

  • 1.computed是依赖已有的变量来计算一个目标变量,大多数情况都是多个变量凑在一起计算出一个变量,并且computed具有缓存机制,依赖值不变的情况下其会直接读取缓存进行复用,computed不能进行异步操作
  • 2.watch是监听某一个变量的变化,并执行相应的回调函数,通常是一个变量的变化决定多个变量的变化,watch可以进行异步操作
  • 3.简单记就是:一般情况下computed多对一watch一对多

watch的实现原理

watch的分类:

  • deep watch(深层次监听)
  • user watch(用户监听)
  • computed watcher(计算属性)
  • sync watcher(同步监听)

watch实现过程:

  • watch的初始化在data初始化之后(此时的data已经通过Object.defineProperty的设置成响应式)
  • watch的key会在Watcher里进行值的读取,也就是立马执行get获取value(从而实现data对应的key执行getter实现对于watch的依赖收集),此时如果有immediate属性那么立马执行watch对应的回调函数
  • 当data对应的key发生变化时,触发user watch实现watch回调函数的执行

computed运行原理

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})
复制代码
  • data 属性初始化 getter setter
  • computed 计算属性初始化,提供的函数将用作属性 vm.fullName 的 getter
  • 当首次获取 fullName 计算属性的值时,Dep 开始依赖收集
  • 在执行 message getter 方法时,如果 Dep 处于依赖收集状态,则判定firstNamelastNamefullName 的依赖,并建立依赖关系
  • firstNamelastName 发生变化时,根据依赖关系,触发 fullName 的重新计算
  • 如果计算值没有发生变化,不会触发视图更新

通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。

computed如何实现传参?

// html
<div>{{ total(3) }}

// js
computed: {
    total() {
      return function(n) {
          return n * this.num
         }
    },
  }

nextTick的实现原理是什么?

在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。 nextTick主要使用了宏任务和微任务。 根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
Promise > MutationObserver > setImmediate > setTimeout

Vue的el属性和$mount优先级?

比如下面这种情况,Vue会渲染到哪个节点上

new Vue({
  router,
  store,
  el: '#app',
  render: h => h(App)
}).$mount('#ggg')
复制代码

这是官方的一张图,可以看出el$mount同时存在时,el优先级 > $mount

image.png

动态指令和参数使用过吗?

<template>
    ...
    <aButton @[someEvent]="handleSomeEvent()" :[someProps]="1000" />...
</template>
<script>
  ...
  data(){
    return{
      ...
      someEvent: someCondition ? "click" : "dbclick",
      someProps: someCondition ? "num" : "price"
    }
  },
  methods: {
    handleSomeEvent(){
      // handle some event
    }
  }  
</script>

动态路由router.addRoutes

let route=[
{
  path: '/pageA',
  name: 'pageA',
  component: pageA,
},
{
  path: '/pageB',
  name: 'pageB',
  component: pageB,
},
{
  path: '/pageC',
  name: 'pageC',
  component: pageC,
}
]
let commonUser=['pageA','pageB']
let commonUserRoute=route.filter(function(page){
    return commonUser.includes(page.name)
})
router.addRoutes(commonUserRoute);

动态组件

<component :is="currentTabComponent"></component>

相同的路由组件如何重新渲染?

开发人员经常遇到的情况是,多个路由解析为同一个Vue组件。问题是,Vue出于性能原因,默认情况下共享组件将不会重新渲染,如果你尝试在使用相同组件的路由之间进行切换,则不会发生任何变化。

const routes = [
  {
    path: "/a",
    component: MyComponent
  },
  {
    path: "/b",
    component: MyComponent
  },
];
复制代码

如果依然想重新渲染,怎么办呢?可以使用key

<template>
    <router-view :key="$route.path"></router-view>
</template>

如何将获取data中某一个数据的初始状态?

在开发中,有时候需要拿初始状态去计算。例如

data() {
    return {
      num: 10
  },
mounted() {
    this.num = 1000
  },
methods: {
    howMuch() {
        // 计算出num增加了多少,那就是1000 - 初始值
        // 可以通过this.$options.data().xxx来获取初始值
        console.log(1000 - this.$options.data().num)
    }
  }

周期函数有哪些(beforeCreatecreated中间都做了什么)

初始化 datapropscomputedwatcherprovide

父子组件生命周期顺序

父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted

vue中生命周期beforeMount和mounted理解

beforeMount是在挂载前执行,执行了render函数返回虚拟DOM,然后递归虚拟DOM创建了一个完整的 DOM 树并插入到 Body 上。
mountedDOM已完成挂载,可以访问DOM元素节点了

对象新属性无法更新视图,删除属性无法更新视图,为什么?怎么办?

  • 原因:Object.defineProperty没有对对象的新属性进行属性劫持
  • 对象新属性无法更新视图:使用Vue.$set(obj, key, value),组件中this.$set(obj, key, value)
  • 删除属性无法更新视图:使用Vue.$delete(obj, key),组件中this.$delete(obj, key)

直接arr[index] = xxx无法更新视图怎么办?为什么?怎么办?

  • 原因:Vue没有对数组进行Object.defineProperty的属性劫持,所以直接arr[index] = xxx是无法更新视图的
  • 使用数组的splice方法,arr.splice(index, 1, item)
  • 使用Vue.$set(arr, index, value)

vue路由

路由配置

image.png Webpack通过增加内联注释来告诉运行时,该有怎样的行为。通过向import中添加注释,我们可以执行诸如命名chunk或选择不同模式之类的操作。webpack在打包的时候,对异步引入的库代码(lodash)进行代码分割时(需要配置webpackSplitChunkPlugin插件),为分割后的代码块取得名字

按需加载:可以将import在上方写成箭头函数的形式赋值给变量,然后在路由组件部分引入
常用配置项
path:路由请求的路径
component:路径匹配成功后需要渲染的组件或者页面
redirect:重定向
children:路由嵌套
name:命名路由 给当前路由取一个别名
props:路由解耦 路由传参的一种方式 针对动态路由
meta:路由元信息 当前路由所携带的一些信息

路由跳转与传参

声明式路由导航##  ==>> 即 <router-link>
<router-link :to="{ 
                    name:'router1',
                    params: { id: status ,id2: status3},
                    query: { queryId: status2 }
                  }">
    router-link跳转router1 
</router-link>

<router-link> 组件有以下这些属性:

属性说明
to需要跳转的路径。当然,我们也可以使用 v-bind 来实现类似功能。
tag指定渲染成什么标签,默认渲染为 <a> 标签。
replace使用替换模式,所以不会留下 History 记录。
active-class<router-link> 与当前所对应的路由一致时,就会自动给当前元素设置一个名为 router-link-active 的 class。设置这一属性,可以修改默认的 class 名称( router-link-active )。设计导航栏时,可以利用该属性,实现高亮显示当前页面对应的导航菜单项 。

Example:
<router-link to='xxx' tag='li'> To PageB </router-link>

注意:
<router-link> 会默认解析成 a 标签,可以通过 tag 属性指定它解析成什么标签

编程式路由导航## ==>> 即写 js 的方式

相关 API:

1) this.$router.push(path):

相当于点击路由链接(可以返回到当前路由界面) ==>> 队列的方式(先进先出)

2)this.$router.replace(path):

用新路由替换当前路由(不可以返回到当前路由界面) ==>> 栈的方式(先进后出)

3)this.$router.back(): 请求(返回)上一个记录路由

4)this.$router.go(-1): 请求(返回)上一个记录路由

5) this.$router.go(1): 请求下一个记录路由

query传参与params传参的区别

image.png

image.png

  • query可以使用name或者path方式跳转,并且不需要对路由进行额外配置;params只能使用name方式进行跳转,并且必须要在路由配置中对path属性设置参数名占位
  • query 传递的参数会'routername?age=18&...'的方式显示在地址栏中,刷新不变,params传参初次渲染时会以'routername/18/...'的方式显示在地址栏中,刷新会无效,可以配合本地存储进行使用 params一旦设置在路由,params就是路由的一部分,如果这个路由有params传参,但是在跳转的时候没有传这个参数,会导致跳转失败或者页面会没有内容。

比如:跳转/router1/:id

<router-link :to="{ name:'router1',params: { id: status}}" >正确</router-link>
<router-link :to="{ name:'router1',params: { id2: status}}">错误</router-link>
共享路由组件的路由之间跳转

如果需要在共享路由组件的页面之间跳转,可以给router-view标签加上key属性,并使其值为路由path或者其他独有标识

路由模式

image.png

默认值: "hash" (浏览器环境) | "abstract" (Node.js 环境) 可选值: "hash" | "history" | "abstract"

  1. hash模式

即地址栏URL中的#符号,通过#号后面的内容的更改,触发hashchange事件,实现路由切换,它的特点在于:hash 虽然出现URL中,但不会被包含在HTTP请求中,对后端完全没有影响,不需要后台进行配置,因此改变hash不会重新加载页面。

  1. history模式

利用了HTML5 History Interface 中新增的pushState() 和replaceState() 方法(需要特定浏览器支持)。history模式改变了路由地址,因为需要后台配置地址。

相关API:

  • history.go(n):路由跳转,比如n为 2 是往前移动2个页面,n为 -2 是向后移动2个页面,n为0是刷新页面

  • history.back():路由后退,相当于 history.go(-1)

  • history.forward():路由前进,相当于 history.go(1)

  • history.pushState():添加一条路由历史记录,如果设置跨域网址则报错

  • history.replaceState():替换当前页在路由历史记录的信息

  • popstate 事件:当活动的历史记录发生变化,就会触发 popstate 事件,在点击浏览器的前进后退按钮或者调用上面前三个方法的时候也会触发。

  1. abstract模式 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

路由缓存Keep-alive

keep-alive组件接受三个属性参数:includeexcludemax

  • include 指定需要缓存的组件name集合,参数格式支持String, RegExp, Array。当为字符串的时候,多个组件名称以逗号隔开。
  • exclude 指定不需要缓存的组件name集合,参数格式和include一样。
  • max 指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持String、Number。

如果需要缓存路由信息使其在离开时不被销毁,可以在<router-view>外嵌套<Keep-alive :includes="[要缓存的路由组件名称]">

原理:keep-alive实例会缓存对应组件的VNode,如果命中缓存,直接从缓存对象返回对应VNode,LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)

路由组件独有的生命周期

actived(被激活)和deactived(失活)

  1. 路由配置时,如果需要加其他的配置项,可以写在meta路由元信息配置项中

路由守卫

  • 全局路由守卫:
    前置:beforeEach(to,from,next)
    后置:afterEach(to,from)
  • 独享路由守卫:
    前置:beforeEnter(to,from,next)
    后置:无
  • 组件内路由守卫:
    进入组件时:beforeRouteEnter(to,form,next)
    离开组件时:beforeRouteLeave(to,form,next)

组件之间的传值通信

  • 父组件传值给子组件,子组件使用props进行接收

  • 子组件传值给父组件,子组件使用$emit+事件对父组件进行传值

  • 组件中可以使用$parent$children获取到父组件实例和子组件实例,进而获取数据

  • 使用$attrs$listeners,在对一些组件进行二次封装时可以方便传值,例如A->B->C

  • 使用$refs获取组件实例,进而获取数据

  • 使用Vuex进行状态管理

  • 使用eventBus进行跨组件触发事件,进而传递数据

  • 使用provideinject,官方建议我们不要用这个,我在看ElementUI源码时发现大量使用

  • 使用浏览器本地缓存,例如localStorage

eventBus用法如下:

//main.js
import Vue from 'vue'
export const eventBus = new Vue()

//brother1.vue
import eventBus from '@/main.js'
export default{
	methods: {
	    toBus () {
	        eventBus.$emit('greet', 'hi brother')
	    }
	}
}

//brother2
import eventBus from '@/main.js'
export default{
    mounted(){
        eventBus.$on('greet', (msg)=>{
            this.msg = msg
        })
    }
}
复制代码

image.png

使用过哪些Vue的修饰符呢?

截屏2021-07-11 下午9.56.53.png

使用过哪些Vue的内部指令呢?

image.png

深层选择器

有时,你需要修改第三方组件的CSS,这些都是 scoped 样式,移除 scope 或打开一个新的样式是不可能的。

现在,深层选择器 >>> /deep/ ::v-deep 可以帮助你。

<style scoped>
>>> .scoped-third-party-class {
  color: gray;
}
</style>

<style scoped>
/deep/ .scoped-third-party-class {
  color: gray;
}
</style>

<style scoped>
::v-deep .scoped-third-party-class {
  color: gray;
}
</style>

axios拦截器怎么配

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
}, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

函数柯里化

柯里化: 一个函数原本有多个参数, 只传入一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构

在模块中,getter和mutation接收的第一个参数state,是全局的还是模块的?

第一个参数state是模块的state,也就是局部的state。

在模块中,getter和mutation和action中怎么访问全局的state和getter?

  • 在getter中可以通过第三个参数rootState访问到全局的state,可以通过第四个参数rootGetters访问到全局的getter。
  • 在mutation中不可以访问全局的satat和getter,只能访问到局部的state。
  • 在action中第一个参数context中的context.rootState访问到全局的state,context.rootGetters访问到全局的getter。

在组件中怎么访问Vuex模块中的getter和state,怎么提交mutation和action?

  • 直接通过this.$store.gettersthis.$store.state来访问模块中的getter和state。
  • 直接通过this.$store.commit('mutationA',data)提交模块中的mutation。
  • 直接通过this.$store.dispatch('actionA,data')提交模块中的action。

用过Vuex模块的命名空间吗?为什么使用,怎么使用。

如果要使你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true 的方式使其成为带命名空间的模块。

export default{
    namespaced: true,
    state,
    getters,
    mutations,
    actions
}

怎么在带命名空间的模块内提交全局的mutation和action?

将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。

this.$store.dispatch('actionA', null, { root: true })
this.$store.commit('mutationA', null, { root: true })

Vue3

  • 所有配置项都写在setUp中,并在最后以对象形式返回
  • 定义基本数据类型用ref()包裹,其底层与Vue2一样仍然是Object.defineProperty的get set,修改时要先从.value中取值再修改,模板渲染时不用加.value
  • 定义引用数据类型用reactive()包裹,其底层为ES6的Proxy代理,无论嵌套层级多深都可以响应,修改时可以直接使用对象和数组的方法进行操作,并支持利用数组下标修改(Vue2不支持下标修改,必须使用this.$set)
  • vue2 中对data中的对象添加删除属性和通过数组下标修改数组都不会触发响应式,可以使用this.$set、this.$delete,而vue3中不存在这些问题