Vue相关

136 阅读12分钟

1.Vue相关API

1.1 全局配置

  • Vue.config 包含 Vue 的全局配置
  • Vue.config.productionTip = false# 阻止显示生产模式的消息

1.2 全局API

  • Vue.component()
    • 用法: 注册或获取全局组件
    • Vue.component('my-component', myComponent)
  • Vue.use()
    • 用法: 安装vue插件 如果插件是一个对象,必须提供install方法; 如果插件是一个函数, 那么会被作为install方法. install方法被调用时,会将Vue作为参数传入
    • 注意需要在 new Vue() 启动应用之前完成
  • Vue.mixin(mixin)
    • 用法: 全局注册一个混入 注意: 会影响注册之后所有创建的Vue实例
  • Vue.nextTick()
    • 用法: 在下次DOM更新循环结束之后执行延迟回调 在修改数据之后立即使用这个方法, 获取更新后的DOM
    • 项目中的用法: 自动聚焦
  • Vue.set()
    • 用法: 向响应式对象添加一个(响应式)属性 如果直接添加则不是响应式的
    • 原因:通过defineProperty能新属性进行了数据劫持处理
  • Vue.delete
    • 用法: 删除对象的一个属性 如果这个对象是响应式的, 那么视图会自动更新 (很少用到)
  • Vue.directive
    • 用法: 注册或获取全局指令

1.3 配置选项

数据

  • data
    • Vue实例的数据对象 注意:data必须一个函数 而不能是一个对象
    • 原因: 如果data是对象,其他组件实例化时也会用这个data对象 会产生冲突
  • computed
    • 计算属性 ,计算属性的结果会被缓存,依赖项改变时,才会重新计算
    • 有两种用法 分为仅读取 、读取和设置
computed: {  
    // 仅读取  
    aDouble: function () {  
        return this.a * 2  
    },  
    // 读取和设置  
    aPlus: {  
        get: function () {  
            return this.a + 1  
        },  
        set: function (v) {  
            this.a = v - 1  
        }  
    }  
}
  • methods
    • 放置所有的方法
  • watch
    • 侦听器 : watch可以侦听到data/computed属性值的改变
// 基本语法
  watch:{
    "被侦听的属性名"(newVal,oldVal){
      // newVal(现在的值) 和 oldVal(上一刻的值)自动被触发  
    } 
  }
// 深度侦听:
  watch:{
    "要侦听的属性名":{ 
      immediate:true,  // 是否立即执行
      deep:true,       // 深度侦听复杂类型变化
			// handler watch的回调函数
      handler(newVal , oldVal){ }
    }
  }

面试题 小试牛刀

  • watch和computed区别:

    • computed能完成的功能,watch都可以完成。watch能完成的功能,computed不一定能完成,最具代表性的:watch可以进行异步操作。
    • watch擅长处理的场景:一个数据影响多个数据 ,侧重在【监视】,核心是:xxx变了,我要做???事。无需return,靠内部逻辑去完成要做的事。例如当需要在数据变化时执行异步或复杂的业务逻辑处理操作时,这个方式是最有用的
    • computed擅长处理的场景:一个数据受多个数据影响 (购物车商品结算的时候),侧重在【算】,核心是:计算出来的值。靠return来输出计算的结果以供使用。不能开启异步任务。
  • props

    • 可以是数组 / 对象 ,用于接收来自父组件的数据,props如果是数组,那么是简单的接收数据,如果是对象,则可以配置高级选项:类型检测、设置默认值等

1.4 实例属性

  • $el
  • $parent
  • $children
  • $refs
  • $attrs
  • $listeners

1.5 实例方法

  • 数据

    • $watch()
    • $nextTick()
    • $set()
    • $delete()
  • 事件

    • $on()
    • $once()
    • $off()
    • $emit()
  • 生命周期

    • $mount()
    • $nextTick()
    • $destroy()

1.6 指令

  • v-text
  • v-html
  • v-show
  • v-if
  • v-else
  • v-else-if
  • v-for
  • v-on
  • v-once
  • v-bind
  • v-model
  • v-slot

1.7 特殊属性

  • key
  • ref
  • is

1.8 内置组件

  • component
  • transition
  • keep-alive
  • slot

2. vue组件的生命周期

单个组件声明周期

vue生命周期是指vue中实例或组件从创建到消灭经过的一系列过程。

一 初始化阶段

new Vue({}) 或者是 new VueComponent({}):实例化一个vm或者组件实例 init:初始化实例,比如事件系统和生命周期激活等等

1.1 beforeCreate

执行的节点是: 数据代理和数据劫持之前( 不能获取到任何数据 ) 注入所有的配置(数据劫持、数据代理、计算属性、方法、事件/侦听器的回调函数)

1.2created

执行的节点是: 数据代理和数据劫持之后( 可以获取到数据 ) 可以访问到data里的数据和methods里的方法

模板编译阶段

  • 判断有没有el选项,如果有则继续判断是否有template,如果没有则看一下有没有$mount挂载,然后再走判断是否template(判断是否有容器进行挂载)
  • 判断是否有template配置向 如果有,则使用render函数对模板进行编译为VNode 如果没有,则把容器(el)的outerHTML作为模板,编译为VNode

二 挂载阶段

2.1 beforeMount

真实DOM挂载之前 (此时页面呈现的是原始el容器的DOM元素,这个节点不能操作DOM) 挂载的过程:把虚拟DOM转为真实DOM(vm.el),并且把vm.el(Vue 实例使用的根 DOM 元素)替换原始模板中的el

2.2 mounted

真实DOM挂载之后 此时页面呈现的是真正的DOM,此时实例的所有初始化已经结束,在这个生命周期中就可以进行初始化操作 (开启定时器,订阅,绑定自定义事件,发送初始化数据请求))

三 更新阶段

数据发生变化之后进入更新阶段

3.1 beforeUpdate

DOM更新之前 (视图未更新) 此时数据是最新的 但是视图是旧的

3.2 Updated

DOM更新之后 根据更新的数据生成新的虚拟DOM 经过diff算法 更新视图

四 销毁阶段

销毁的方式: v-if 路径切换

4.1 beforeDestroy

进行收尾工作,若该页面开启了定时器,定时器不会随着组件销毁而停止,需要手动停止定时器

4.2 destroyed

网络请求一般在 mounted/created 都可以, mounted中会稍微晚一丁点

父子组件生命周期

初始化阶段

  • 父 beforeCreated
  • 父 created
  • 父 beforeMount
  • 子 beforeCreated
  • 子 created
  • 子 beforeMount
  • 子 mounted
  • 父 mounted

更新阶段

  • 父 beforeUpdate
  • 子 beforeUpdate
  • 子 updated
  • 父 updated

卸载阶段

  • 父 beforeDestory
  • 子 beforeDestory
  • 子 destored
  • 父 destored

补充的生命周期函数

动态路由用到的钩子函数:
activated: 激活时的钩子函数
deactivated: 未激活时的钩子函数

3. 组件深入

1 动态组件

通过<component>动态确定要显示的组件,is指定要显示组件的组件名

<component :is="currentComp" />

2 缓存组件

  • 使用<keep-alive>缓存动态组件, 可以通过includeexclude指定只缓存特定组件
<keep-alive :exclude="['Home']">
	<component :is="currentComp"/>
</keep-alive>
  • 使用<keep-alive>也可以缓存路由组件
<keep-alive include="Life1">
	<router-view></router-view>
</keep-alive>
  • 路由组件对象什么时候创建?
    • 默认: 每次跳转/访问对应的路由路径时
    • 有缓存: 第一次访问时
  • 路由组件对象什么时候死亡?
    • 默认: 离开时
    • 有缓存: 离开时不死亡, 只有当destroy/父组件死亡/刷新页面

3 异步组件

  • 好处: 能更快的看到当前需要展现的组件界面(异步组件的代码开始没有加载)

  • 无论是路由组件还是非路由组件都可以实现异步组件效果

    • 拆分单独打包
    • 需要时才请求加载组件对应的打包文件
  • 配置组件: component: () => import(modulePath)

    • import(modulePath): 被引入的模块会被单独打包(code split) --ES8的新语法
    • () => import(modulePath): 函数开始不调用, 当第一次需要显示对应的界面时才会执行, 请求加载打包文件
  • 细节

    • import()返回promise, promise成功的结果就是整体模块对象
    • 本质上: 可以利用import()实现对任意模块的懒加载

4 函数组件: functional + render + JSX

  • 函数组件的特点
    • 无状态
    • 无法实例化
    • 内部没有任何生命周期处理函数
    • 轻量,渲染性能高,适合只依赖于外部数据传递而变化的组件(展示组件,无逻辑和状态修改)
    • 可以没有根标签
  • 编码
export default {
    functional: true, // 当前是函数组件
    render (createElement, context) {
        return 要显示界面的虚拟DOM
    }
}


VueComponent.prototype = Object.create(Vue.prototype);

5 递归组件

  • 递归组件: 组件内部有自己的子组件标签
  • 应用场景: 用于显示树状态结构的界面
  • 注意: 递归组件必须有name
  • 编码: 实现一个简单的可开关的树状结构界面的 Tree 组件

4. Vue组件间多种通信方式

通信方式可分类

  • 父子间通信:
    • 父->子
      • props (非函数)
      • v-model
      • refs/refs / children
      • 作用域插槽
    • 子->父
      • props (函数)
      • 自定义事件 $emit 触发
      • .sync
      • v-model
      • $parent
      • 作用域插槽
  • 爷孙间通信
    • $attr / $listeners
    • provide / inject
  • 任意组件通信
    • 事件总线
    • vuex

4.1 props

  1. 实现父向子通信: 属性值是非函数
  2. 实现子向父通信: 属性值是函数

4.2 自定义事件

// 父元素中绑定事件
<Child @eventName="callback($event)">
// 子组件中触发事件
this.$emit('eventName', 2)

4.3 全局事件总线 / pubsub

/* 
事件总线:
可以实现任意组件间通信
将入口文件 main.js 中的vm作为全局事件总线对象
*/ 
beforeCreated() {
    Vue.prototype.$bus = this // 创建事件总线
}

// 传递数据的组件分发事件: this.$bus.$emit('eventName', data)
// 接收数据的组件处理监听: this.$bus.$on('eventName', (data) => {})
    
 Vue内部的处理: VueComponent.prototype = Object.create(Vue.prototype)
 Object.create(Vue.prototype): 就是创建Vue原型对象的子对象

4.4 v-model

/*
实现父子之间相互通信
v-model的本质: 动态value属性与自定义input监听(接收子组件发布的数据更新父组件的数据)
*/
 父组件: 
  <CustomInput v-model="name"/>
  <!-- 等价于 -->
  <CustomInput :value="name" @input="name=$event"/>
子组件: 
<input type="text" :value="value" @input="$emit('input', $event.target.value)">
    ....
props: ['value']

4.5 .sync

  • 在原本父向子的基础上增加子向父
  • 一般和自定义事件配合使用, 代码更加简洁
  • element中dialog的显示隐藏用到这个方法
父组件:
<child :money.sync="total"/>
<!-- 等价于 -->
<Child :money="total" @update:money="total=$event"/>
...
data () {
  return {
     total: 1000
   }
}
----------------------------------------------------------------------
子组件: (固定写法)
<button @click="$emit('update:money', money-100)">花钱</button>
...
props: ['money']

4.6 作用域插槽

1) 实现父组件向子组件传递标签内容
2) 什么情况下使用作用域插槽?
    父组件需要向子组件传递标签结构内容
    但决定父组件传递怎样标签结构的数据在子组件中
3) 编码:
    子组件:
        <slot :row="item" :$index="index">  <!-- slot的属性会自动传递给父组件 -->
        </slot>
    父组件:
        <template slot-scope="{row, $index}">
        <template v-slot="{row, $index}">
            <span>{{$index+1}}</span> &nbsp;&nbsp;
            <span :style="{color: $index%2===1 ? 'blue' : 'green'}" >{{row.text}}				</span>
        </template>
4) 应用
	element-ui中绝大部分组件都用了插槽
	element-ui中的 table-column 组件使用了作用域插槽

4.7 provide / inject

1) 实现一个组件与它内部的后代任意组件直接通信
2) 使用
	在祖组件中通过provide配置向后代组件提供数据
	在后代组件中通过inject配置来声明接收数据
3) 注意:
	不太建议在应用开发中使用, 一般用来封装vue插件
	provide提供的数据本身不是响应式的 ==> 父组件更新了数据, 后代组件不会变化
	provide提供的数据对象内部是响应式的 ==> 父组件更新了数据, 后代组件也会变化
	方法二:
		祖: 定义返回数据的方法, 通过provide提供这个方法
		后代: 注入这个方法, 定义计算属性返回这个方法返回的数据
4) 应用: 
	element-ui中的Form组件中使用了provide和inject

4.8 $attr / $listeners

1) $attrs
    实现当前组件的父组件向当前组件的子组件通信(祖孙间通信)
    它是包含所有父组件传入的标签属性(排除props声明, class与style的属性)的对象
    使用: 通过 v-bind="$attrs" 将父组件传入的n个属性数据传递给当前组件的子组件
2) $listeners
    实现当前组件的子组件向当前组件的父组件通信 (孙向祖通信)
    $listeners是包含所有父组件传入的自定义事件监听名与对应回调函数的对象
    使用: 通过v-on="$listeners" 将父组件绑定给当前组件的事件监听绑定给当前组件的子组件
3) 应用 
	利用它封装了一个自定义的带hover文本提示的el-button

4.9 vuex

4.10 $children$parent

1) $refs
    实现父组件向指定子组件通信
    $refs是包含所有有ref属性的标签对象或组件对象的容器对象
    使用: 通过 this.$refs.child 得到子组件对象, 从而可以直接更新其数据或调用其方法更新数据
    this.$refs.xxx
2) $children
    实现父组件向多个子组件通信
    $children是所有直接子组件对象的数组
    使用: 通过this.$children 遍历子组件对象, 从而可以更新多个子组件的数据
3) $parent
    实现子组件向父组件通信
    $parent是当前组件的父组件对象
    使用: 通过this.$parent 得到父组件对象, 从而可以更新父组件的数据
4) 应用 
	在后台管理项目中使用了$refs

5. vue响应式

5.1 MVC和MVVM模式

MVC模式

  • M:Model: 模型层,负责在数据库中存取数据
  • V:View: 视图层, 负责数据显示
  • C:Controller:控制器,将用户输入的指令传递给业务

MVVM模式

  • M: Model模型,包含数据 data对象
  • V: View视图,数据显示界面
  • VM: vm是vue.js实例,通过vm读取model里的数据到view上 实现了数据的响应式

MVVM的优势:不用亲自操作DOM, 数据是响应式的, 一旦数据变化, 界面自动更新

data 为什么只能是函数不能是对象

  • 同一个组件的多个组件实例的data必须是不同的对象(内容初始数据可以相同)
  • 如果是data是对象, 组件的多个实例共用一个data对象
  • 如果是函数, 组件对象通过调用函数得到的一个新的data对象

响应式数据与非响应式数据?

  • 响应式: data / props / computed/ vuex的state与getters

  • 非响应式: 

    • 给组件对象添加一个新属性: this.xxx = value
    • 直接给一个响应式对象添加一个新属性: this.product.xxx = 'abc' ==> this.$set(this.product, 'xxx', 3)
  • 模板中可以直接读取显示, 但只是改了数据, 界面不会更新 - 对象的响应式与数组的响应式有什么区别?

    • 对象: 通过Object.defineProperty()添加getter/setter方法来监视属性数据的改变 + 订阅-发布
    • 数组: 重写更新数组元素的一系列方法 + 订阅-发布 (7个方法: )
      • 调用原生的对应对数组元素进行相应的操作
      • 更新界面去
      • arr.push(3) vue重写的方法
  • 为什么vue在数组上使用重写方法的形式来实现响应式?

    • 占用空间小, 效率高 ===> 不用给每个元素都加setter
    • 数组元素更新我们一般是调用其方法, 对象属性的更新我们基本都是通过.属性的方式
    • 这种方式的限制是什么? 不能通过下标直接更新元素/length

vue数据绑定&响应式原理

  • 理解:

    • 说vue的数据绑定的原理, 或者数据响应式的原理, 都是在说一个事
    • 当我们修改了data中的数据时, 组件界面是如何自动更新的
    • 这里涉及下面几个重点
      • 数据代理: Object.defineProperty()
        • vm._data 对象, 内部有数据
        • vm._data.msg
        • vm.msg
        • 数据: data对象中的属性
        • 代理: vm
        • 被代理对象: data对象
      • 数据劫持/监视: Object.defineProperty() 给data中的属性添加setter
      • 发布-订阅: observer 与 dep 与 watcher
        • 收集依赖
        • 派发更新
      • 相关对象
        • observer
          • 给data中所有层次属性都通过defineProperty添加get/set
          • 为每个属性都创建对应的dep, 用于收集依赖(收集依赖当前属性的watcher)
          • 发布者
        • dep
          • 帮助observer来收集依赖的watcher: subs数组
          • 一旦数据更新了, dep就会遍历内部保存的所有watcher, 去更新
          • 订阅器
        • watcher
          • 用于做对应的节点的更新
          • 它是订阅者
  • 数据代理

    • 通过Object.defineProperty()给vm添加与data对象中对应的属性
    • 在getter中, 读取data中对应的属性值返回 ==> 当我们通过this.xxx读值时, 读取的是data中对应的属性值
    • 在setter中, 将最新的值保存到data中对应的属性上 ==>当我们通过this.xxx = value时, value保存在data中对应的属性上
    • 作用: 简化对vm/组件对象内部的data对象的属性数据的操作(读/写)
  • 数据劫持/监视

    • 在observer中, 通过Object.defineProperty()给data中所有层次属性都添加上getter/setter
    • 为每个属性都创建一个dep对象, 用于后面更新
    • 注意: 在解析模板时, 为每个表达式都创建了一个用于更新对应节点的watcher
    • 在getter中, 去建立dep与watcher之间的关系
      • dep与data中的属性一一对应
      • watcher与模板中的表达式一一对应
      • 一个dep中, 保存了包含n个watcher的数组 ==> 当多个表达式用到当前dep所对应的属性
      • 一个watcher中, 保存了包含n个dep的对象 ==> 当表达式是一个多层的表达式
    • 在setter中, 通过dep去通知所有watcher去更新对应的节点
  • 发布-订阅模式

    • 发布者: observer

    • 订阅者: watcher: 订阅数据的变化==> 一旦数据变以, 得告诉我, 我来负责做更新节点的操作

    • 订阅器/中间人: dep 初始化: 一个data中的属性 ==> dep ==> 对应n个watcher ==> 每个watcher都有一个更新对应节点的函数

      更新: this.name = 'xxx' ​ 由于有数据代理的存在 ===> data中的name属性更新了 ​ 由于有数据劫持/监视的存在 ===> observer中name属性对应的setter方法调用了 ​ 利用发布-订阅模式机制 ==> 由name对应的deep对象来通知所有对应的watcher去更新对应的节点

Vue双向数据绑定

  • 通过v-model来实现双向数据绑定
  • v-model的本质
    • 将动态的data数据通过value属性传给input显示 ==> data到view的绑定
    • 给input标签绑定input监听, 一旦输入改变读取最新的值保存到data对应的属性上 ==> view到data的绑定
  • 双向数据绑定是在单向数据绑定(data-->view)的基础, 加入input事件监听(view ==> data)

面试时,交流响应式原理

  • 数据代理
    • 通过object.definepropertyvm定义与data中属性对应的带有getter/setter的属性
    • getter中,读取data中对应的属性值返回 例如:读取this.msg 读取的是data中msg属性值
    • setter中,将最新值保存到data对应的属性上 例如: 设置 this.msg = 'ts' 就是把 'ts'保存到data的msg上
  • 创建observer
    • 目标: 对data中所有层次的属性进行监听/劫持
    • 通过defineproperty给data中所有层级的属性,都重新定义,添加getter和setter
      • getter:用来简历dep和wacher的关系 **作用:**收集依赖
      • setter:用来替换data, 当数据发生改变去更新视图
    • 为data中所有层级的属性创建一个对应的dep 未来用于更新页面的
  • 创建complie
    • 目标1: 实现界面的初始化显示
    • 目标2: 为更新页面做准备:
      • 为模板中每个包含表达式的节点创建一个对应的wather
      • 为watcher绑定用于更新对应节点的回调函数
      • 将watcher添加到n个对应的dep中

6. 可复用性

6.1 mixin (混入)

  • 用来复用多个组件中相同的js代码的技术
  • 将多个组件相同的js代码提取出来, 定义在一个mixin中配置对象
  • 在多个组件中通过mixins配置引入mixin中的代码, 会自动合并到当前组件的配置中

6.2 自定义指令

Vue.directive('upper-text', (el, binding) => {
    el.innerText = binding.value.toUpperCase()
})
<p v-upper-text="msg"></p>

msg: 'I Will Back'

6.3 自定义插件

// 对象插件
const myPlugin = {
    // 必须有此方法
    install (Vue) {
        // 通过Vue来扩展新的功能语法, 如注册全局组件/指令/过滤器/...
      Vue.filter()
      Vue.directive()
      Vue.prototype.$test = function test () {}
    }
}
// 函数插件
const myPlugin = (Vue) => {
    // 通过Vue来扩展新的功能语法, 如注册全局组件/指令/过滤器/...
}
export default myPlugin
// 在入口JS中引入, 并声明使用来安装插件
import myPlugin from './vue-myPlugin'
Vue.use(myPlugin)
  • 对象插件: 调用插件对象install方法(传入Vue)来安装插件(执行定义新语法的代码)
  • 函数插件: 直接将其作为install来调用(传入Vue)来安装插件(执行定义新语法的代码)

7. Vue2状态管理: Vuex

Vuex的五大属性

  • state
  • mutations
  • actions
  • getters
  • modules
  • namespaced: true # 开启命名空间

面试问题:

问题1 vuex中的mutation可以执行异步操作吗?

  • 功能可以 ==> 异步更新数据后界面确实会自动更新
  • 问题 ==> vuex的调试工具监视不到mutation中的异步更新, 工具记录还是更新前的数据(不对)
  • 扩展: 工具如何记录数据变化? ==> 每次mutation函数执行完后, 立即记录当前的数据 ==> 在mutation中同步更新state, 才能被记录到

问题2 vuex中的状态 数据的响应式原理

响应式: 更新数据,界面会自动更新 data中所有层级的数据都添加了getter/setter

  • 创建了一个vm对象 new Vue() => data
  • state中的数据都是vm的data数据(是响应式的)
  • 组件中读取的state数据本质读取的就是data中的数据
  • 一旦更新了state中的数据, 所有用到这个数据的组件就会自动更新

问题3 解决vuex数据刷新丢失问题

  • 绑定事件监听: 在卸载前保存当前数据
window.addEventListener('beforeunload', () => { // 当页面刷新时, 页面卸载前的事件回调
	sessionStorage.setItem('CART_LIST_KEY', 
		JSON.stringify(this.$store.state.shopCart.cartList))
})

window.removeEventListener('beforeunload')
  • 在初始时读取保存数据作为状态的初始值
cartList: JSON.parse(sessionStorage.getItem('CART_LIST_KEY')) || [],

8. Vue路由 vue-router

8.1 跳转/导航路由的两种基本方法

  • 声明式导航
    • <router-link :to="{path: '/xxx'}" replace>xxx</router-link/>
    • 类似于a标签, to属性指向要跳转的路径
  • 编程式导航
    • 给标签/组件 绑定事件, 通过router.push或replace方法
    • this.$router.push/replace(location)