Vue - 原理总结

7/28/2022

# vue与MVVM

# Vue的两大核心

  • 数据驱动:数据劫持+依赖收集+发布订阅。getter手记依赖,setter依赖更新。
    • 数据劫持:Vue2.0用的是Object.defineProperty把对象里的property全部转成getter和setter。
    • 发布订阅:监听器Observer,订阅者Watcher,解析器Compile
  • 组件化开发
    • 模板template
    • 初始数据data
    • props
    • 方法methods
    • 生命周期钩子
    • 私有资源assets

# 为什么vue里的data是函数

  • 解释:大概意思就是,data是函数的话就解决了对象之间会相互影响的问题。因为在函数内定义的变量是有作用域的,就相当于一个对象开辟一个新的空间存储, 但如果不是函数,那么变量就会对全局进行污染。

# vue2的双向绑定原理

  • Vue2是通过数据劫持+发布订阅模式实现的。Vue2是Object.defineProperty(), Vue3是Proxy。(但proxy更像是代理而不是劫持)。
  • Object.defineProperty是干什么的呢?它的作用就是控制一个对象属性的一些特殊操作,比如读写权、是否可枚举。Vue里主要是利用它设置对象的 某个属性,用get和set进行读写操作。
  • vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布 消息给订阅者,触发相应的监听回调来渲染视图。
  • 流程图

# $nextTick

# 定义

  • 定义:在下次 DOM 更新循环结束之后执行延迟回调。说人话就是nextTick(),是将回调函数延迟在下一次dom更新数据后调用

# 使用场景

  • 需要在视图更新之后,基于新的视图进行操作。说得更具体一点就是比如我想把一个data中的数据渲染成promise中请求后的。但是

# 原理

  • 原因是,Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据 变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计 算和Dom操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。 当你设置 vm.someData = 'new value',DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的 DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立 即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

# 注意

  • Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。$nextTick 是在下次 DOM 更新循环结束之后 执行延迟回调,在修改数据之后使用 $nextTick,则可以在回调中获取更新后的 DOM

  • code in Vue nextTick

# 组件传值

# 父子传值

  • 父传子,子用props接收。
  • $parent/$children
  • 依赖注入(父provide / 子inject)

# 子传父

  • $emit,要通过绑定父组件事件触发,$emit('fn', value)
  • ref/ $refs

# 综合

  • eventBus()。先创建vue实例,用event.$emit()传递,event.$on()接收
  • Vuex。 store, getters, mutations, action, modules五大属性。
  • 发布订阅。pub发sub订,有插件,也可以自己造。

# vuex

# 五大属性

  • state, getters, mutations, action, modules.
  • 为什么mutations不能支持异步?
    • 如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪 ,给调试带来困难(无法追踪状态)。写了之后,state里的值vue devtool无法捕捉到变化。

# keepalive

  • 概念
    • keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 transition 相似, keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中。
  • 作用
    • keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性include/exclude。
    • ka会把数据保存在内存this.cache中,在render渲染时,如果VNode的name符合缓存条件,会从this.cache中取出之前缓存的VNode进行渲染。
  • 生命周期
    • activated组件激活时调用,获取数据,承担原来created钩子中的数据任务。
    • deactivated组件停用时调用,
  • 使用

# v-model

# 概念

  • v-model本质上是一个v-bind和@input的语法糖。如下面的代码,两行的效果是一样的
<input v-model="sth" />
<input :value="sth" @input="sth = $event.target.value" />
1
2

# 组件上的应用

  • v-model也可以用在组件上,做到实时数据更新。具体实现及原理如下:
<div id="demo">
 <currency-input v-model="price"></currency-input>
<!-- 下面问题的答案在这里:-->
<!-- <currency-input :value="price" @input="price = arguments[0]"></currency-input>-->
 <span>{{price}}</span>
</div>
<script>
Vue.component('currency-input', {
 template: `
  <span>
   <input
    ref="input"
    :value="value"
    <-! 为什么这里把 'input' 作为触发事件的事件名?input在哪定义的?-->
    @input="$emit('input', $event.target.value)
  </span>
 `,
 props: ['value'],// 为什么这里要用 value 属性,value在哪里定义的?貌似没找到啊?
})
 
var demo = new Vue({
 el: '#demo',
 data: {
  price: 100,
 }
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 缺点及解决方案

  • 其实也算是使用场景,v-model在有多个复选框或者单选框的组件时,这个并不好用,因为它获取到的是checked属性,且点击单选框时 触发的是onChange事件而非onInput事件,所以对于这种办法的v-model并不好用。
  • 上述问题也是有解决办法的。如下
<input type="checkbox" :checked="status" @change="status = $event.target.checked" />
1

# vue性能优化(待完善)

  • 编程优化
  • 加载性能优化
  • 用户体验优化
  • SEO优化

# EventBus 事件总线

# 概念

  • 原理是发布订阅模式,解决的是Vue中组件的通信。也可以说是除了Vuex的另一个解决方案。

# 方法

  • 既然是发布订阅,就会有“发布”和“订阅”两个动作。
  • $emit 用于创建发送的事件。
  • $on 用于订阅监听事件。
  • $off 用于放在beforeDestroy钩子里对这个组件销毁,释放内存。(也就是说用eventbus并不会主动地被回收机制清除,解决这个的方法就是手动清除。)

# 用法

  • updateMessage组件,更新信息(在此创建监听事件)
<!-- UpdateMessage.vue -->
<template>
    <div class="form">
        <div class="form-control">
            <input v-model="message" >
            <button @click="updateMessage()">更新消息</button>
        </div>
    </div>
</template>
<script>
export default {
        name: "UpdateMessage",
        data() {
            return {
                message: "这是一条消息"
            };
        },
        methods: {
            updateMessage() {
                this.$bus.$emit("updateMessage", this.message);
            }
        },
        beforeDestroy () {
            $this.$bus.$off('updateMessage')
        }
    };
 </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • showMessage组件,监听updateMessage事件,并显示。
<!-- ShowMessage.vue -->
<template>
    <div class="message">
        <h1>{{ message }}</h1>
    </div>
</template>

<script> 
export default {
        name: "ShowMessage",
        data() {
            return {
                message: "我是一条消息"
            };
        },
        created() {
            var self = this
            this.$bus.$on('updateMessage', function(value) {
                self.updateMessage(value);
            })
        },
        methods: {
            updateMessage(value) {
                this.message = value
            }
        }
    }; 
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# Diff算法

# Vue2.0的diff

  • 利用头尾双指针,对新旧两棵树的结点进行一一比对,结点相同,两个头指针都向右移一位,一直到尾指针
  • 其中如果遇到旧树没有,新树有的情况,以新vNode子节点的头指针指向的结点为基准,遍历旧vNode子节点,如果没找到 相同,就创建并插入到真实dom里。
  • 如果上述情况遍历旧vNode结点找到了相同结点,就把对应结点插入相应的位置。
  • 直到遍历完new vNode,循环结束。

# diff里的函数

  • patch:比较新旧两个结点的key和tag,如果都相同,那这两个结点就是相等的。
  • patchVnode:更新的过程中会根据新旧vNode结点有子节点还是有文本来做不同的操作。
    1. 旧VNODE有子节点,新VNODE没有:直接删掉真实dom的子节点
    2. 旧VNODE没有子节点,新VNODE有子节点:创建新VNODE的子节点,添加到真实dom上
    3. 新/旧VNODE都有文本节点:如果文本内容不同,直接把真实dom的文本改成新VNODE的文本
    4. 新/旧VNODE都有子节点:把两者的children数组单独拎出来对比,对比的过程如下
  • updateChildren:对于两个自己诶单数组,声明两个头尾指针,在不错位的情况下,对比分成5种。
    • 分别:旧头和新头; 旧头和新尾; 旧尾和新头; 旧尾和新尾; 以上四种都不符合就是第五种。
    • 具体图解请看这里 (opens new window)

# Vue3.0和2.0的diff不同在哪

  • vue2在diff过程中,优先处理特殊场景的情况,入头头比,头尾比,尾头比,尾尾比这样。
  • 但是在vue3里,根据newIndexToOldIndexMap新老节点列表找到最长稳定序列,通过最长增长子序列算法 比对,找出新旧结点中不需要更改和移动的点就地复用,只对需要移动或者已经patch的结点操作,大幅提升了 替换效率。

# 补充:为什么v-for里的key值不推荐用index?

  • 其实原理还是diff算法,因为index值并不是稳定的,但diff算法比对新旧结点的时候是比较tag和key值,那么 index不稳定,就有可能造成新旧VNode无法匹配的情况,导致渲染dom错误。

# Vue-Router

# vue-router完整的导航解析流程

  1. 导航被触发
  2. 在即将离开的组件里调用beforeRouteLeave守卫
  3. 调用全局前置守卫beforeEach守卫
  4. 在重用的组件里调用beforeRouteUpdate守卫 / 调用路由配置的beforeEnter守卫
  5. 解析异步路由组件
  6. 在被激活的组件里调用beforeRouteEnter
  7. 调用全局的beforeResolve守卫
  8. 导航被确认
  9. 调用全局的 afterEach 钩子
  10. 触发DOM更新
  11. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。

# vue-router生命周期

  • 全局

    • beforeEach
    • afterEach
  • 路由独享

    • beforeEnter
  • 组件生命周期

    • beforeRouteEnter
    • beforeRouteUpdate
    • beforeRouteLeave

# v-show 和 v-if

# v-show

  • show隐藏元素的原理是display:none,控制隐藏,只会编译一次;

# v-if

  • if隐藏元素的原理是动态向DOM树添加或删除元素,如果初始值是false,就不会编译。
  • 如果要频繁切换节点,用show会比较节省开销。

# Vue2-自定义指令

# 主要应用场景

  • 需要对一些dom元素执行操作的时候,有可能要用到自定义指令。
  • ex:比如说我现在有一个表格,我希望点击表格中的文字可以获取到这个dom焦点并变成input框,失焦后自动保存内容。这种情况就需要用到自定义指令了。

# 钩子函数

  • 自定义指令提供了几个钩子函数:
    • bind: 只调用一次,指令第一次绑定到元素时调用,可以定义一个在绑定时执行一次的初始化动作。
    • inserted: 被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于 document 中)。
    • update: 被绑定元素所在的模板更新时调用,而不论绑定值是否变化。通过比较更新前后的绑定值。
    • componentUpdated: 被绑定元素所在模板完成一次更新周期时调用。
    • unbind: 只调用一次, 指令与元素解绑时调用。

# 常用的自定义

# npm run dev 的时候发生了什么?

# 脚手架Vue-cli

# vue-cli热更新

  • vue-hot-reload-api
  • 主要就是通过维护一个map映射对象,通过对component名称进行对比,这里主要维护了一个Ctor对象,通过hook 的方法在vue的生命周期中进行watch监听,然后更新后进行rerender以及reload
  • 和webpack的hmr对比:
    • 依赖:vue-cli热重载是强依赖于vue框架的,利用的是vue自身的Watcher监听,通过vue的生命周期函数进行名称模 块的变更的替换;而webpack则是不依赖于框架,利用的是sock.js进行浏览器端和本地服务端的通信,本地的watch监听 则是webpack和webpack-dev-server对模块名称的监听,替换过程用的则是jsonp/ajax的传递;
    • 粒度:vue-cli热重载主要是利用的vue自身框架的component粒度的更新,虽然vue-cli也用到了webpack,其主要是 打包和本地服务的用途;而webpack的热更新则是模块粒度的,其主要是模块名称的变化定位去更新,由于其自身是一个工具 应用,因而不能确定是哪个框架具体的生命周期,因而其监听内容变化就必须通过自身去实现一套类似的周期变化监听;
    • 定位:vue-cli定位就是vue框架的命令行工具,因而其并不需要特别大的考虑到双方通信以及自定义扩展性等;而webpack 本身定位在一个打包工具,或者说其实基于node.js运行时环境的应用,因而也就决定了它必须有更方便、更个性化的扩展和抽象性