基础知识-vue

309 阅读1分钟

vue使用

插值、动态属性、v-html

<template>
  <div>
    <p>文本插值{{message}}</p>
    <p>Js 表达式 {{ flag ? 'yes' : 'no'}}</p><!-- (只能式表达式,不能是js语句) -->
    <p :id="dynamicId">动态属性 id</p>

    <hr />
    <p v-html="rawHtml">
      <span>注意使用v-html之后,将会覆盖当前标签的子元素</span>
    </p>
  </div>
</template>

<script lang='ts'>
import { defineComponent, reactive, toRefs } from 'vue'
export default defineComponent({
  name: 'defualt',
  components: {
  },
  setup () {
    const data = reactive({
      message: 'hello vue',
      flag: true,
      rawHtml: `指令 - 原始html<b>加粗</b><i>斜体</i>`,
      dynamicId: `id-${Date.now()}`
    })
    return {
      ...toRefs(data)
    }
  }
})

</script>

computed和watch

computed用在v-model中需要有get和set方法

<template>
  <div>
    <p>{{num}}</p>
    <p>double1 {{double1}}</p>
    <input v-model="double2" />
  </div>
</template>

<script>
export default {
  data () {
    return {
      num: 20,
    }
  },
  computed: {
    double1 () {
      return this.num * 2
    },
    double2: {
      get () {
        return this.num * 2
      },
      set (val) {
        this.num = val / 2
      }
    }
  }
}

</script>

watch 如果监听值为引用类型需要设置深度监听,deep:true,因为引用类型的值oldVal和val的值都指向同一个地址,所以监听不到

<template>
  <div>
    <input v-model="name" />
    <input v-model="info.city" />
  </div>
</template>

<script>
export default {
  data () {
    return {
      name: '百岁',
      info: {
        city: '北京'
      }
    }
  },
  watch: {
    name (oldVal, val) {
      console.log(oldVal, val) //值类型,可正常拿到
    },
    info: {
      handler (oldVal, val) {
        console.log(oldVal, val) //引用类型,拿不到
      },
      deep: true //深度监听
    }
  }
}

</script>

Computed 和 Watch 的区别

参考 参考视频

对于Computed:

  • 它支持缓存,只有依赖的数据发生了变化,才会重新计算
  • 不支持异步,当Computed中有异步操作时,无法监听数据的变化
  • computed的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
  • 如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用computed
  • 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。

对于Watch:

  • 它不支持缓存,数据变化时,它就会触发相应的操作
  • 支持异步监听
  • 监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
  • 当一个属性发生变化时,就需要执行相应的操作
  • 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
    • immediate:组件加载立即触发回调函数
    • deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。

当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。

总结:

  • computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
  • watch 侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

运用场景:

  • 当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
  • 当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

class和style

使用动态属性,或者使用驼峰式写法

<template>
  <div>
    <p :class="{black: isBlack, yellow: isYellow}">使用class</p>
    <p :class="{black, yellow}">使用class(数组)</p>
    <p :Style="styleData">使用style</p>
  </div>
</template>

<script>
export default {
  data () {
    return {
      isBlack: true,
      isYellow: true,
      balck: 'black',
      yelllow: 'yellow',
      styleData: {
        fontSize: '40px', //驼峰写法
      }
    }
  }
}

</script>

vuex

参考 参考 Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。 image.png

state

State提供唯一的公共数据源,所有共享的数据都要统一放到Store的State中进行存储。

//全局中
import store from './store'
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
//store.js
//创建store数据源,提高唯一公共数据
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutaitons: {

  },
  actions: {

  }
})

export default store

访问state中数据

第一种方式:this.$store.state.全局数据名称

this.$store.state.count

第二种方式:从vuex中按需导入mapState函数

import { mapState } form 'vuex'

通过刚才导入的mapState函数,将当前组件需要的全局数据,映射为当前组件的computed计算属性

computed: {
    ...mapState({['count']})
}

mutation

mutation用于变更store中的数据,只能通过Mutation变更Store数据,不可以直接操作Store中的数据,通过这种方式虽然操作起来稍微有些繁琐,但是可以集中监控所有数据的变化。

如果发现state中数据的变更有问题,可以直接通过Mutation查找原因,当项目很大的时候利于维护。

//定义Mutation
const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: { //state是固定的代表全局的数据对象state, step是触发时额外的参数
        add(state, step) {
            state.count += step
        }
    }
})

第一种触发方式:this.$store.commit()是触发mutations的第一种方式

//触发mutation
methos: {
    handle() {
    //触发mutations时携带参数
        this.$store.commit('add', 3)
    }
}

第二种触发方式:通过刚才导入的mapMutations函数,将需要的mutations函数,映射为当前组件的methods方法

// 从vuex中按需导入mapMutations函数
import { mapMutations } from 'vuex'
//将指定的mutations函数,映射为当前组件的methods函数
methods: {
    ...mapMutations(['add', 'addN']),
    btnHandler1() {
        this.add(3) //传参
    }
}

action

Action用于处理异步任务,如果通过异步操作变更数据,必须通过Action,而且不能使用Mutation,但是再Action中还是要通过触发Mutation的方式间接变更数据。

//定义Action
const store = new Vuex.Store({
    //...省略其他代码
    mutations: {
        add(state, step) {
            state.count += step
        }
    },
    actions: {
        addAsync(context, step) {
           setTimeout(() => {
           //在action中, 不能直接修改state中的数据
           //必须通过context.commit()触发某个mutation才行
                context.commit('add', step)   
           }, 1000)     
         }
    }
})

第一种触发方式

//触发
methods: {
    handel() {
        this.$store.dispatch('addAsync',5)
    }
}

第二种触发方式:通过刚才导入的mapActions函数,将需要的actions函数,映射为当前组件的methods方法

import { mapActions } from 'vuex'
methods: {
    ...mapActions(['addASync','addNASync'])
    btnHandler3() {
        this.subAsync()
    }
}

getters

  • Getter可以对Store中已有的数据加工处理之后形成新的数据,类似Vue的计算属性
  • Store中数据发生变化,Getter的数据也会跟着变化
//定义Getter
const store = new Vuex.Store({
    state: {
        count: 0
    },
    getters: {
        showNum: state => {
            return '当前最新的数量是【'+ state.count +'】'
        }
    }
})

使用第一种方式

this.$store.getters.名称
//例如
{{this.$store.getters.showNum}}

使用第二中方式

import { mapGetters } from 'vuex'
computed: {
   ...mapGetters(['showNum']) 
}

总结

(1)核心流程中的主要功能:

  • Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;
  • 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;
  • 然后 Mutations 就去改变(Mutate)State 中的数据;
  • 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。

(2)各模块在核心流程中的主要功能:

  • Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
  • dispatch∶操作行为触发方法,是唯一能执行action的方法。
  • actions∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
  • commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
  • mutations∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。操作之中会有一些hook暴露出来,以进行state的监控等。
  • state∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,利用Vue的细粒度数据响应机制来进行高效的状态更新。
  • getters∶ state对象读取方法。图中没有单独列出该模块,应该被包含在了render中,Vue Components通过该方法读取全局state对象。 (3)Vuex中action和mutation的区别:
  • Mutation专注于修改State,理论上是修改State的唯一途径;Action业务代码、异步请求。
  • Mutation:必须同步执行;Action:可以异步,但不能直接操作State。
  • 在视图更新时,先触发actions,actions再触发mutation
  • mutation的参数是state,它包含store中的数据;store的参数是context,它是 state 的父级,包含 state、getters

vue原理

组件化

数据驱动视图

传统组件:只是静态渲染,更新还要依赖于操作DOM。 数据驱动视图:vue和react会根据数据的变化重新渲染视图 数据驱动视图: Vue MVVM; React setState;

Vue MVVM

mvvm.png

MVVM 是 Model-View-ViewModel 的缩写

  • Model 代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑。
  • View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来。
  • ViewModel 监听模型数据的改变和控制视图⾏为、处理⽤户交互,简单理解就是⼀个同 步View 和 Model 的对象,连接 Model 和 View
  • 在 MVVM 架构下, View 和 Model 之间并没有直接的联系,⽽是通过 ViewModel 进⾏交互, Model 和 ViewModel 之间的交互是双向的, 因 此 View 数据的变化会同步到Model中,⽽Model 数据的变化也会⽴即反 应到 View 上。
  • ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,⽽ View 和 Model 之间的同步⼯作完全是⾃动的,⽆需⼈为⼲涉,因此开 发者只需关注业务逻辑,不需要⼿动操作DOM,不需要关注数据状态的同 步问题,复杂的数据状态维护完全由 MVVM 来统⼀管理

Vue响应式

参考 概念:组件data的数据一旦变化,立刻触发视图的更新

vue2.0响应式

基本使用

参考 定义:Object.defineProperty()  方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
参数:obj 要定义属性的对象; prop要定义或修改的属性的名称或 Symbol; descriptor要定义或修改的属性描述符。
可选键值:

  • configurable 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false

  • enumerable 当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。默认为 false

  • value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
    默认为 undefined

  • writable 当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 (en-US)改变。默认为 false

      var obj = {}
      Object.defineProperty(obj, 'a', {
        // 赋值
        value: 3,
        // 是否可以被改写
        writable: false,
        // 是否可以被枚举遍历
        enumerable: true
      });
      obj.a = 4
      document.write(obj.a)

set和get

  • get

  • 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。注意value和get不能同时使用
    默认为 undefined

  • set

  • 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入set函数里。
    默认为 undefined

      Object.defineProperty(obj, 'b', {
        get() {
          document.write('你试图访问obj的a属性')
        },
        set() {
          document.write('你试图改变obj的a属性')
        }
      })
      console.log(obj.b) //你试图访问obj的a属性
      obj.b = 10 //你试图改变obj的a属性

改变值 由于属性的值为get函数的返回值,所以如果想改变该属性的值,需要在set函数中将接收到的改变值赋给一个临时变量,然后再访问时return临时变量。这也是vue的监听原理

      var obj = {}
      var temp;
      Object.defineProperty(obj, 'b', {
        get() {
          console.log('你试图访问obj的a属性')
          return temp //访问临时变量
        },
        set(newValue) {
          console.log('你试图改变obj的a属性')
          temp = newValue //将改变的值赋给临时变量
        }
      })
      obj.b = 9;
      console.log(obj.b)

深度监听

利用递归直到递归到非引用类型的值为止,类似深拷贝。并将他们都监听起来,注意再赋新值的时候也需要observer,深度监听新赋值的每个值

订阅者Dep

收集依赖需要为依赖找一个存储依赖的地方,为此我们创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。

于是我们先来实现一个订阅者 Dep 类,用于解耦属性的依赖收集和派发更新操作,说得具体点,它的主要作用是用来存放 Watcher 观察者对象。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
Dep的简单实现

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }
    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }
    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

观察者 Watcher

1.为什么引入Watcher

Vue 中定义一个 Watcher 类来表示观察订阅依赖。至于为啥引入Watcher,《深入浅出vue.js》给出了很好的解释:

当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。

依赖收集的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中

2.Watcher的简单实现
class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
   // 我们定义一个 cb 函数,这个函数用来模拟视图更新,调用它即代表更新视图
    this.cb(this.value)
  }
}

以上就是 Watcher 的简单实现,在执行构造函数的时候将 Dep.target 指向自身,从而使得收集到了对应的 Watcher,在派发更新的时候取出对应的 Watcher ,然后执行 update 函数。

Dep类和Watcher类

  • 把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例,成员中都有一个Dep的实例
  • Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件
  • 依赖就是Watcher。只有Watcher触发的getter才会收集依赖,那个Watcher触发了getter,就把哪个Watcher收集到Dep中。
  • Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍
  • 代码实现的巧妙之处:Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中。

Object.defineProperty缺点

  • 深度监听,需要递归到底,一次性计算量大,如果data中定义的对象层级很深的话,需要一直递归下去,如果这个层级非常深,再页面初始化的时候就会卡死
  • 无法监听新增属性/删除属性(vue.set vue.delete)
  • 无法原生监听数组,需要特殊处理

完整代码实现

//触发更新视图
function updateView() {
  console.log('视图更新')
}

// 重新定义数组原型
const oldArrayPropty = Array.prototype
// 创建新对象, 原型指向oldArrayProperty, 再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayPropty);
['push','pop','shift','unshift','splice'].forEach(methodName => {
  arrProto[methodName] = function () {
    updateView() //触发视图更新
    oldArrayPropty[methodName].call(this, ...arguments)
  }
})

//重新定义属性,监听
function defineReactive(target, key, value) {
  //深度监听递归
  observer(value)
  //创建dp实例
  let dp = new Dep() 
  //核心API
  Object.defineProperty(target, key, {
    get() {
    //将Watcher添加到订阅
      if(Dep.target) {
          dp.addSub(Dep.target)
      }
      return value
    },
    set(newValue) {
      if(newValue !== value) {
        //深度监听
        observer(newValue)
        //设置新值
        value = newValue
        // 执行watcher的upadate方法
        dp.notify()
        //触发更新视图
        updateView()
      }
    }
  })
}

// 监听对象属性
function observer(target) {
  if(typeof target !== 'object' || target === null) {
    // 不是对象或数组
    return target
  }

  if (Array.isArray(target)) {
    target.__proto__ = arrProto
  }

  //重新定义各个属性(for in 也可以遍历数组) 
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

//准备数据
const data = {
  name: 'zhangsan',
  age: 20,
  info: {
    address: '北京'
  },
  nums: [10, 20, 30]
}

//监听数据
observer(data)

//测试
// data.name = 'list'
// data.age = 21
// data.info.address = '上海' //深度监听
// data.x = '100' //新增属性,监听不到
// delete data.name //删除属性,监听不到  需要vue.set
data.nums.push(4)

vue3.0响应式

参考
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于Object.defineProperty(),其有以下特点:

  1. Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
  2. Proxy 可以监听数组的变化。
 const proxyData = new Proxy(data, {
   get(target,key,receive){ 
     // 只处理本身(非原型)的属性
     const ownKeys = Reflect.ownKeys(target)
     if(ownKeys.includes(key)){
       console.log('get',key) // 监听
     }
     const result = Reflect.get(target,key,receive)
     return result
   },
   set(target, key, val, reveive){
     // 重复的数据,不处理
     const oldVal = target[key]
     if(val == oldVal){
       return true
     }
     const result = Reflect.set(target, key, val,reveive)
     console.log('set', key, val)
     return result
   },
   deleteProperty(target, key){
     const result = Reflect.deleteProperty(target,key)
     console.log('delete property', key)
     console.log('result',result)
     return result
   }
 })

  // 声明要响应式的对象,Proxy会自动代理
 const data = {
   name: "zhangsan",
   age: 20,
   info: {
     address: "北京" // 需要深度监听
   },
   nums: [10, 20, 30]
 };

vue响应式总结

参考
整体思路是数据劫持结合观察者模式

内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性(订阅addsub,发布notify),存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。

如何监测数组的变化
数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写

所以在 Vue 中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新

vue2.0和vue3.0实现响应式的区别

Vue2.0
  • 基于Object.defineProperty,不具备监听数组的能力,需要重新定义数组的原型来达到响应式。
  • Object.defineProperty 无法检测到对象属性的添加和删除 。
  • 由于Vue会在初始化实例时对属性执行getter/setter转化,所有属性必须在data对象上存在才能让Vue将它转换为响应式。
  • 深度监听需要一次性递归,对性能影响比较大。兼容到ie8
Vue3.0
  • 基于ProxyReflect,可以原生监听数组,可以监听对象属性的添加和删除。
  • 不需要一次性遍历data的属性,可以显著提高性能。
  • 因为Proxy是ES6新增的属性,有些浏览器还不支持,只能兼容到IE11 。

虚拟DOM(vdom)和diff

背景:DOM操作非常耗费性能, 用JS模拟DOM结构,计算出最小的变更,操作DOM

用JS模拟DOM结构

image.png

diff算法概述

diff 即对比,是一个广泛的概念,如linux diff命令、git diff等,两个js对象也可以做diff,两棵树做diff,如这里的vodm diff

树diff的时间复杂度o(n^3)

  • 第一,遍历tree1,第二,遍历tree2
  • 第三,排序
  • 1000个节点,要计算1亿次,算法不可用

优化时间复杂度到O(n)

  • 只比较同一层级,不跨级比较
  • tag不相同,则直接删掉重建,不再深度比较
  • tag和key,两者都相同,则认为是相同节点,不在深度比较

image.png

image.png

虚拟DOM总结

由于在浏览器中操作 DOM 是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。

优点:

  1. 保证性能下限: 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  2. 无需手动操作 DOM: 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  3. 跨平台: 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  1. 无法进行极致优化: 虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
  2. 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。 进阶可以参考

Vue中key的作用

参考 vue 中 key 值的作用可以分为两种情况来考虑:

  • 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
  • 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速

  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

为什么不建议用index作为key?

使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。

模板编译

vodm

渲染过程

image.png

  • 解析模板为render函数(或在开发环境已完成,vue-loader)
  • 触发响应式,监听data属性getter setter
  • 执行render函数,生成vnode, patch(elem, vonde)
  • 修改data,触发setter(此前在getter中已被监听)
  • 重新执行render函数,生成newVnode
  • patch(vonde, newVnode)

异步渲染

  • 汇总data的修改,一次性更新视图
  • 减少DOM操作次数,提高性能

前端路由原理

image.png

hash的特点

  • hash变化会触发网页跳转,即浏览器的前进、后退
  • hash变化不会刷新页面,SPA必需的特点
  • hash永远不会提交到server端
// 监听hash变化
window.onhashchange = (event) => {
    console.log('old url', event.oldURL)
    console.log('new url', event.newURL)
    console.log('hash:', location,hash)
}
//页面初次加载,获取hash
document.addEventListener('DOMContentLoaded', () => {
    console.log('hash:', location.hash)
})
//JS修改url
document.getElementById('btn1').addEventListener('click', () => {
    location.href = '#/user'
})

h5 history模式

  • 用url规范的路由,但跳转时不刷新页面
  • history.pushState
  • window.onpopstate
//页面初次加载,获取path
document.addEventListener('DOMContentLoaded', () => {
    console.log('load', location.pathname)
})
//点击按钮打开一个新的路由
document.getElementById('btn1').addEventListener('click', () => {
    const state = {name: 'page1'}
    console.log('切换路由到' 'page1')
    history.pushState(state, '', 'page1')// 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => {//重要!!
    console.log('onpopstate', event.state, location.pathname)
}
// 需要server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html

总结

  • hash - window.onhashchange 方式监听
  • H5 history - history.pushState 和 window.onpopstate 方式监听
  • H5 history 需要后端支持 两者选择
  • to B 的系统推荐用hash, 简单易用, 对url规范不敏感
  • to C 的系统,可以考虑选择H5 history, 但需要服务端支持(seo 搜索引擎优化需要选择h5 history)
  • 能选择简单的,就用简单的,要考虑成本和收益

vue原理-总结

内容小结

  • 组件化:组件化历史, 数据驱动视图, MVVM
  • 响应式:Object.defineProperty, 监听对象(深度), 监听数组, Object.defineProperty的缺点(vue3用proxy)
  • vdom和diff:应用背景,vnode结构,snabbdom使用:vnode h patch
  • 模板编译:with语法,模板编译为render函数,执行render函数生成vonde
  • 渲染过程:初次渲染过程,更新过程,异步渲染
  • 前端路由:hash,H5 history,两者对比

面试真题

v-show 和 v-if 的区别

  • v-show通过css display 控制显示和隐藏
  • v-if组件真正的渲染和销毁,而不是显示和隐藏
  • 频繁切换显示状态用v-show,否则用v-if

为何在v-for中用key

参考
vue 中 key 值的作用可以分为两种情况来考虑:

  • 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
  • 第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,如果没有key值它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。

key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速

  • 更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快

描述Vue组件生命周期(父子组件)重要

  • 单组件生命周期 beforeCreate(创建前)
    在实例初始化之后,数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。

created(创建后)
实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom

beforeMount(挂载前)
在挂载开始之前被调用:相关的 render 函数首次被调用。模板已经在内存中编译好了,还没有渲染到页面上

mounted(挂载后)
在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点

beforeUpdate(更新前)
data数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。此时数据已经被更新,但是页面尚未和最新的数据同步,可以在此修改数据

updated(更新后)
发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。页面和data数据保持同步

beforeDestroy(销毁前)
实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。

destroyed(销毁后)
Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

activated keep-alive 专属,组件被激活时调用 deactivated keep-alive 专属,组件被销毁时调用
用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。 可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。

如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:

  • 能更快获取到服务端数据,减少页面 loading 时间;
  • ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;

父子组件生命周期关系

原则:创建是从外到内,渲染是从内到外

  • 加载渲染过程

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

  • 子组件更新过程

父 beforeUpdate->子 beforeUpdate->子 updated->父 updated

  • 父组件更新过程

父 beforeUpdate->父 updated

  • 销毁过程

父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

Vue组件如何通讯(常见)

参考
组件通信的方式如下:

(1) props / $emit

父组件通过props向子组件传递数据,子组件通过$emit和父组件通信

1. 父组件向子组件传值
  • props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
  • props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。
  • props属性名规则:若在props中使用驼峰形式,模板中需要使用短横线的形式
// 父组件
<template>
    <div id="father">
        <son :msg="msgData" :fn="myFunction"></son>
    </div>
</template>

<script>
import son from "./son.vue";
export default {
    name: father,
    data() {
        msgData: "父组件数据";
    },
    methods: {
        myFunction() {
            console.log("vue");
        }
    },
    components: {
        son
    }
};
</script>
// 子组件
<template>
    <div id="son">
        <p>{{msg}}</p>
        <button @click="fn">按钮</button>
    </div>
</template>
<script>
export default {
    name: "son",
    props: ["msg", "fn"]
};
</script>
2. 子组件向父组件传值
  • $emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on监听并接收参数。
// 父组件
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
  name: 'comArticle',
  components: { comArticle },
  data() {
    return {
      currentIndex: -1,
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  },
  methods: {
    onEmitIndex(idx) {
      this.currentIndex = idx
    }
  }
}
</script>
// 父组件
<template>
  <div class="section">
    <com-article :articles="articleList" @onEmitIndex="onEmitIndex"></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from './test/article.vue'
export default {
  name: 'comArticle',
  components: { comArticle },
  data() {
    return {
      currentIndex: -1,
      articleList: ['红楼梦', '西游记', '三国演义']
    }
  },
  methods: {
    onEmitIndex(idx) {
      this.currentIndex = idx
    }
  }
}
</script>
//子组件
<template>
  <div>
    <div v-for="(item, index) in articles" :key="index" @click="emitIndex(index)">{{item}}</div>
  </div>
</template>

<script>
export default {
  props: ['articles'],
  methods: {
    emitIndex(index) {
      this.$emit('onEmitIndex', index) // 触发父组件的方法,并传递参数index
    }
  }
}
</script>

(2)eventBus事件总线($emit / $on)

vm.$on( event, callback ):监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数。
vm.$emit( eventName, […args] ):触发当前实例上的事件。附加参数都会传给监听器回调。

vm.$off( [event, callback] )

  • 用法: 移除自定义事件监听器。
    • 如果没有提供参数,则移除所有的事件监听器;
    • 如果只提供了事件,则移除该事件所有的监听器;
    • 如果同时提供了事件与回调,则只移除这个回调的监听器。

eventBus事件总线适用于父子组件非父子组件等之间的通信,使用步骤如下:

(1)创建事件中心管理组件之间的通信

  1. 新建一个event-bus.js文件
// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()
  1. 将new Vue()赋值到Vue.prototype上
Vue.prototype.$bus = new Vue()

使用时

//使用时不需要再引入文件直接使用,接收事件
    this.$bus.$on("refreshEvent", fn)
//发送事件
    this.$bus.$emit("refreshEvent")

(2)发送事件

假设有两个兄弟组件firstComsecondCom

<template>
  <div>
    <first-com></first-com>
    <second-com></second-com>
  </div>
</template>

<script>
import firstCom from './firstCom.vue'
import secondCom from './secondCom.vue'
export default {
  components: { firstCom, secondCom }
}
</script>

firstCom组件中发送事件:

<template>
  <div>
    <button @click="add">加法</button>    
  </div>
</template>

<script>
import {EventBus} from './event-bus.js' // 引入事件中心

export default {
  data(){
    return{
      num:0
    }
  },
  methods:{
    add(){
      EventBus.$emit('addition', {
        num:this.num++
      })
    }
  }
}
</script>

(3)接收事件

secondCom组件中发送事件:

<template>
  <div>求和: {{count}}</div>
</template>

<script>
import { EventBus } from './event-bus.js'
export default {
  data() {
    return {
      count: 0
    }
  },
  mounted() {
    EventBus.$on('addition', add(param))
  },
  methods: {
    add (param)  {
      this.count = this.count + param.num;
    }
  }
}
</script>

(4) 解绑事件
如果不及时取消订阅,则回调函数仍会执行,更严重的是,如果在事件处理回调函数中引用了外部变量形成了闭包,则会导致内存泄漏。

beforeDestory () {
    //及时销毁,避免内存泄漏
    EventBus.$off('addtion', this.add)
}

在上述代码中,这就相当于将num值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。

虽然看起来比较简单,但是这种方法也有不便之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。

(3)依赖注入(project / inject)

这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。

project / inject是Vue提供的两个钩子,和datamethods是同级的。并且project的书写形式和data一样。

  • project 钩子用来发送数据或方法
  • inject钩子用来接收数据或方法

在父组件中:

provide() {
 return {
    num: this.num
  };
}

在子组件中:

inject: ['num']

还可以这样写,这样写就可以访问父组件中的所有属性:

provide() {
 return {
    app: this
  };
}
data() {
 return {
    num: 1
  };
}

inject: ['app']
console.log(this.app.num)

注意:  依赖注入所提供的属性是非响应式的。

(3)ref / $refs

这种方式也是实现父子组件之间的通信。

ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。

在子组件中:

export default {
  data () {
    return {
      name: 'JavaScript'
    }
  },
  methods: {
    sayHello () {
      console.log('hello')
    }
  }
}

在父组件中:

<template>
  <child ref="child"></component-a>
</template>
<script>
  import child from './child.vue'
  export default {
    components: { child },
    mounted () {
      console.log(this.$refs.child.name);  // JavaScript
      this.$refs.child.sayHello();  // hello
    }
  }
</script>

(4)$parent / $children

  • 使用$parent可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法)
  • 使用$children可以让组件访问子组件的实例,但是,$children并不能保证顺序,并且访问的数据也不是响应式的。

在子组件中:

<template>
  <div>
    <span>{{message}}</span>
    <p>获取父组件的值为:  {{parentVal}}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Vue'
    }
  },
  computed:{
    parentVal(){
      return this.$parent.msg;
    }
  }
}
</script>

在父组件中:

// 父组件中
<template>
  <div class="hello_world">
    <div>{{msg}}</div>
    <child></child>
    <button @click="change">点击改变子组件值</button>
  </div>
</template>

<script>
import child from './child.vue'
export default {
  components: { child },
  data() {
    return {
      msg: 'Welcome'
    }
  },
  methods: {
    change() {
      // 获取到子组件
      this.$children[0].message = 'JavaScript'
    }
  }
}
</script>

在上面的代码中,子组件获取到了父组件的parentVal值,父组件改变了子组件中message的值。

需要注意:

  • 通过$parent访问到的是上一级父组件的实例,可以使用$root来访问根组件的实例
  • 在组件中使用$children拿到的是所有的子组件的实例,它是一个数组,并且是无序的
  • 在根组件#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组
  • $children 的值是数组,而$parent是个对象

(5)$attrs / $listeners

考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?

如果是用props/$emit来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。

针对上述情况,Vue引入了$attrs / $listeners,实现组件之间的跨代通信。

先来看一下inheritAttrs,它的默认值true,继承所有的父组件属性除props之外的所有属性;inheritAttrs:false 只继承class属性 。

  • $attrs:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上
  • $listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合 v-on="$listeners" 将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)

A组件(APP.vue):

<template>
    <div id="app">
        //此处监听了两个事件,可以在B组件或者C组件中直接触发 
        <child1 :p-child1="child1" :p-child2="child2" @test1="onTest1" @test2="onTest2"></child1>
    </div>
</template>
<script>
import Child1 from './Child1.vue';
export default {
    components: { Child1 },
    methods: {
        onTest1() {
            console.log('test1 running');
        },
        onTest2() {
            console.log('test2 running');
        }
    }
};
</script>

B组件(Child1.vue):

<template>
    <div class="child-1">
        <p>props: {{pChild1}}</p>
        <p>$attrs: {{$attrs}}</p>
        <child2 v-bind="$attrs" v-on="$listeners"></child2>
    </div>
</template>
<script>
import Child2 from './Child2.vue';
export default {
    props: ['pChild1'],
    components: { Child2 },
    inheritAttrs: false,
    mounted() {
        this.$emit('test1'); // 触发APP.vue中的test1方法
    }
};
</script>

C 组件 (Child2.vue):

<template>
    <div class="child-2">
        <p>props: {{pChild2}}</p>
        <p>$attrs: {{$attrs}}</p>
    </div>
</template>
<script>
export default {
    props: ['pChild2'],
    inheritAttrs: false,
    mounted() {
        this.$emit('test2');// 触发APP.vue中的test2方法
    }
};
</script>

在上述代码中:

  • C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了$listeners 属性
  • 在B组件中通过v-bind 绑定$attrs属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)

(6)总结

(1)父子组件间通信

  • 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
  • 通过 ref 属性给子组件设置一个名字。父组件通过 refs组件名来获得子组件,子组件通过refs 组件名来获得子组件,子组件通过 parent 获得父组件,这样也可以实现通信。
  • 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。

(2)兄弟组件间通信

  • 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
  • 通过 $parent/$refs 来获取到兄弟组件,也可以进行通信。

(3)任意组件之间

  • 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。

如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。

描述组件渲染和更新的过程

image.png

双向数据绑定v-model的实现原理

参考 参考 v-model 是一个语法糖 动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为目标值:

<input v-model="sth" />
//  等同于
<input 
    v-bind:value="message" 
    v-on:input="message=$event.target.value"
>
//$event 指代当前触发的事件对象;
//$event.target 指代当前触发的事件对象的dom;
//$event.target.value 就是当前dom的value值;
//在@input方法中,value => sth;
//在:value中,sth => value;

在自定义组件中,v-model 默认会利用名为 value 的 prop和名为 input 的事件

<currency-input v-model="price"></currentcy-input>
<!--上行代码是下行的语法糖
 <currency-input :value="price" @input="price = arguments[0]"></currency-input>
-->

<!-- 子组件定义 -->
Vue.component('currency-input', {
 template: `
  <span>
   <input
    ref="input"
    :value="value"
    @input="$emit('input', $event.target.value)"
   >
  </span>
 `,
 props: ['value'],
})

v-if和v-for能不能同时使用?

因为v-for比v-if优先级高,意味着如果两个指令同时使用,会先循环后判断,造成资源浪费;
如果遇到需要同时使用时可以考虑写成计算属性的方式或者外层加一个template;