vue学习整理

171 阅读6分钟

0. vue的diff算法

参考 juejin.cn/post/684490…

  • diff算法比较:
  1. 只进行同层级比较执行patch(不同则替换),
  2. 如果节点不相同(主要根据标签和key等是否一致),则直接生成新节点(不复用)替换原节点dom。
  3. 如果节点相同(复用),执行patchVnode,根据新节点对旧节点修改属性,文本和子节点children(删除或者新增children的子节点dom)
  4. 相同节点更新时,如果新旧vnode都有子节点,则对应的子节点需要执行updateChildren( 四种比较方式+key查询在此过程),来更新子节点的属性和data等,并将此节点移动到对应的位置。在updateChildren中,如果没找到可复用的节点,则是生成新节点dom后插入到oldStartVnode前面,** 不是替换 **
  5. 最后在updateChildren中,如果原虚拟节点先比较完,说明新虚拟节点都是新增的直接新建插入,如果新虚拟节点先比较完毕,说明剩下的旧虚拟节点都应该删除。

image.png

// 是否是相同节点的判断函数
function sameVnode (a, b) {
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 标签名
    a.isComment === b.isComment &&  // 是否为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}

image.png

diff过程时,都没找到该newVnode则新建并插入,newStart++,oldStart并不发生改变 image.png

1. Vue中列表组件写key的作用

主要用于diff时,快速找到要复用的节点,找不到就新生成,没有加key永远都认为是一个相同的节点,会直接修改当前节点(tag一致时), diff算法中,新旧虚拟dom首先进行头头,尾尾,头尾,尾头对比(交叉对比),四种比较如果都没找到相同节点,则会1:如果有key,则使用map(map是key=>节点位置的index)映射,去看看节点列表中是否有要复用的节点(如果有则直接复用该节点)。2:如果上面都没有则直接根据新的虚拟节点列表的头指针位置的vnode生成新的节点,并插入Dom对应位置。

// vue项目  src/core/vdom/patch.js  -488行
// 以下是为了阅读性进行格式化后的代码

// oldCh 是一个旧虚拟节点数组
if (isUndef(oldKeyToIdx)) {
  oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
if(isDef(newStartVnode.key)) {
  // map 方式获取原节点,进行服用
  idxInOld = oldKeyToIdx[newStartVnode.key]
} else {
  // 新建节点并插入
}

2. 父子组件通信的方式

props和$emit

this.$refs.child 和 this.$parent

this.$ref和this.$parent(this.$child获取子组件数组,效果跟this.$refs一样,vue3中废弃)都能获取实例,直接访问data和method

eventBus

主要是使用一个新的Vue的实例的 $on 和 $emit方法,只监听一次用$once,$off移出监听(或者自己实现EventEmitter)

  • EventBus.$off('事件名', callback),只移除这个回调的监听器。

  • EventBus.$off('事件名'),移除该事件所有的监听器。

  • EventBus.$off(), 移除所有的事件监听器,注意不需要添加任何参数。 缺点

  • 大家都知道vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。

  • A组件向事件总线发送了一个事件aMsg并传递了参数MsgA,然后B组件对该事件进行了监听,并获取了传递过来的参数。但是,这时如果我们离开B组件,然后再次进入B组件时,又会触发一次对事件aMsg的监听,这时时间总线里就有两个监听。通常会用到,在vue页面销毁时,同时移除EventBus事件监听。

优点

  • 解决了多层组件之间繁琐的事件传播。
  • 使用原理十分简单,代码量少。
// 两种引入方式:全局和局部
// 局部:创建一个EventBus.js文件
import Vue from 'vue' // 引入vue
const EventBus = new Vue() // 创建实例
export default EventBus // 导出

// 全局:
import Vue from 'vue' // 引入vue
Vue.prototype.$EventBus = new Vue();

// a.vue,监听
<template>
  <div>
    <h3>页面A</h3>
    <router-link to="/b">
      跳转B页面
    </router-link>
  </div>
</template>

<script>
export default {
  created () {
    console.log('----A页面监听事件----')
    // 使用Vue原型链引入
    this.$EventBus.$on('getNum'(num) => {
      console.log('num', num)
    })
  },
  // a.vue 添加$off方法,防止内存泄露
 beforeDestroy () {
    console.log('----A页面销毁监听事件----')
    this.$EventBus.$off('getNum')
  }
}
</script>


// b.vue,触发
<template>
  <div>
    <h3>页面B</h3>
    <router-link to="/a">
      跳转A页面
    </router-link>
  </div>
</template>

<script>
// import { EventBus } from "../Bus.js"; 局部使用
export default {
  created () {
    console.log('----B页面触发事件----')
    // 使用Vue原型链引入
    this.$EventBus.$emit('getNum', num)
    })
  }
}
</script>

provide/inject,$attrs和$listeners(父子,爷孙等组件)

provide/inject是vue 2.2.0 新出的api,主要用于父、子、孙及所有后代之间的通信。通过provide属性在父组件中提供指定属性,在需要的子孙组件(不论组件嵌套有多深)中通过inject注入所需属性。

// parent组件
<template>
  <div>
    <div>{{test}}</div>
    <button @click="changeTest">修改test的值</button>
    <son></son>
  </div>
</template>

<script>
import Son from "./Son";
export default {
  name: "parent",
  components: { Son },
  provide() {
    return {
      injectData: this.test
    };
  },
  data() {
    return {
      test: "测试",
    };
  },
  methods: {
    changeTest() {
      this.test = "测试后";
    }
  }
};
</script>


//son组件
<template>
  <div>{{injectData}}</div>
</template>

<script>
export default {
  name: "son",
  inject: ["injectData"],
  mounted() {
    // eslint-disable-next-line
    console.log(this.injectData)
  },
};
</script>

vuex(也可以非父子组件)

3. vue3的ref, reactive, toRef, toRefs

参考:juejin.cn/post/697667…

  • ref: 用于将基础类型(引用类型也行)的数据转成响应式,(除了模板中,其他地方都是.value获取和修改值)
<template>
  <div>
    <div>countRef:{{countRef}}</div>
    <div>objCountRef:{{objCountRef.count}}</div>
    <div>爱好:{{hobbyRef.join('---')}}</div>
  </div>
</template>

<script>
import { ref } from 'vue'
export default {
  name: 'refdemo',
  setup () {
    // 值类型
    const countRef = ref(1)
   
    // 对象
    const objCountRef = ref({ count: 1 })

    // 数组
    const hobbyRef = ref(['爬山', '游泳'])

    setTimeout(() => {
      // 通过value改变值
      countRef.value = 2
      objCountRef.value.count = 3
      hobbyRef.value.push('吃饭')
    }, 4000)

    return {
      countRef,
      objCountRef,
      hobbyRef
    }
  }
}
</script>

  • reactive: 用于将引用类型的数据转成响应式,解构时丢失响应式
<template>
    <p>ref demo {{ageRef}} {{state.name}}</p>
</template>

<script>
import { ref, reactive } from 'vue'

export default {
    name: 'Ref',
    setup(){
        const ageRef = ref(18)
        const nameRef = ref('monday')
        
        // ref的值,可以作为reactive的属性
        const state = reactive({
            name: nameRef
        })

        setTimeout(() => {
            console.log('ageRef', ageRef.value,'nameRef', nameRef.value)
            // .value
            ageRef.value = 20
            nameRef.value = 'mondaylab'
            console.log('ageRef', ageRef.value,'nameRef', nameRef.value)
        },1500)

        return{
            ageRef,
            state
        }
    }
}
</script>
  • toRef:用于将reactive之后的响应式对象的某个key的value单独拿出来(保持响应式),跟原值是引用关系
<template>
    <p>toRef demo - {{ageRef}} - {{state.name}} {{state.age}}</p>
</template>

<script>
import { ref, toRef, reactive, computed } from 'vue'

export default {
    name: 'ToRef',
    setup() {
        const state = reactive({
            age: 18,
            name: 'monday'
        })

        //实现某一个属性的数据响应式
        const ageRef = toRef(state, 'age')

        setTimeout(() => {
            // 由于引用关系,ageRef.value也变化
            state.age = 20
        }, 1500)

        setTimeout(() => {
            ageRef.value = 25 // .value 修改值
        }, 3000)

        return {
            state,
            ageRef
        }
    }
}
</script>
  • toRefs:用于将reactive之后的响应式对象的所有key的value拿出来,在return的时候可以解构
<template>
    <p>toRefs demo {{age}} {{name}}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive } from 'vue'

export default {
    name: 'ToRefs',
    setup() {
        const state = reactive({
            age: 18,
            name: 'monday'
        })

        const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象(理解:应该是对象变成普通对象,但是属性还是响应式的)

        setTimeout(() => {
            console.log('age', state.age, 'name', state.name)
            state.age = 20,
            state.name = '周一'
            console.log('age', state.age, 'name', state.name)
        }, 1500)
        // 这样之后,不用在写state.age等,直接写age
        return ...stateAsRefs
    }
}
</script>

4. vue2、3的数据劫持实现

  • Object.defineProperty
// 变成响应式数据
let oldArrayProto = Array.prototype
// 给新对象的__proto__设置为oldArrayProto
let newArrayProto = Object.create(oldArrayProto)
let methods = ['push', 'shift', 'unshift', 'pop', 'splice', 'sort', 'reverse']
methods.forEach(methodName => {
    newArrayProto[methodName] = function () {
        console.log('视图更新')
        oldArrayProto[methodName].call(this, ...arguments)
    }
})

function observer(target) {
    if (typeof target !== 'object' || typeof target === 'null') {
        return target
    }
    for (let key in target) {
        target.hasOwnProperty(key) && defineReactive(target, key, target[key]);
    }
}
function defineReactive(target, key, value) {
    // 深度监听
    observer(value);

    // 数组方法劫持
    if (Array.isArray(target)) {
        target.__proto__ = newArrayProto
        return;
    }

    // 以对象的key -> value劫持
    Object.defineProperty(target, key, {
        get() {
            // observer(value)
            console.log('get', key, value);
            return value
        },
        set(newValue) {
            // 新值是引用类型需要重新劫持
            observer(newValue)
            if (newValue !== value) {
                value = newValue
                console.log('视图更新')
            }
        }
    })
}

const data = {
    name: 'zhm',
    age: 10,
    friend: {
        friendName: '测试'
    },
    colors: [0, 1, 2]
}
// 把数据变成响应式
observer(data)

  • Proxy
const reactive = function(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    
    const observer = new Proxy(obj, {
        get(target, key) {
            console.log('get', key)
            const res = Reflect.get(target, key);
            // 深层代理,不然的话value为引用值时,没有被代理,虽然通过proxy.a.b能触发get
            // 但是相当于proxy.a触发代理的get, 返回的是未代理的对象a,然后再a.b访问属性b
            return typeof res === 'object' ? reactive(res) : res;
        },
        set(target, key, value) {
            console.log('set', key);
            Reflect.set(target, key, value)
        }
    })
    return observer;
}

const reactive2 = function(obj) {
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    let tmpValue;
    
}
const o = {
    name: 'sx',
    age: '122',
    obj: {
        h: 'aaa'
    }
}
const oo = reactive(o);

未深度劫持

深度劫持

5. v-if和v-show的区别

  1. 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
  2. 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中会销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
  3. 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载); v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;
  4. 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

6. vue2和3的区别

参考 juejin.cn/post/706337…