「你不知道的vue3.0」盘点vue3.0的提升与CompositionAPI原理

460 阅读12分钟
  • diff算法的优化

      在vue2.0中,diff算法是通过对新旧虚拟DOM所有节点进行遍历比较的形式,来判断哪些节点是有更新的,那么就存在一个问题,有些DOM节点,内容始终就是一个字符串,或者有些节点就是作为容器存在的,没有比较的意义,这种全比较的方式有些冗余;如图 ( 图片来自李南江的教学视频,这个当时对我的学习挺有帮助的): image

      而在vue3.0中,生成虚拟DOM的时候,会针对绑定了变量的节点附加一个flag值,而在diff算法针对新旧虚拟DOM进行比较的时候,只针对flag值不为0的节点进行两两比较(flag的值会根据变量绑定属性的不同而不同,例如变量绑定class,style等),优化了diff算法的性能;如图: image

  • 静态提升以及监听缓存(待补)

(待补充)

  • Composition组合式API

1.CompositionAPI解决了什么问题?

   在vue2.0中,比如需要新增一个A业务逻辑,我们需要把所用到的变量写在data中,绑定的事件写在methods中,同时监听写在watch中等等等等,这样就造成了一个情况,A业务逻辑的代码很分散,不利于维护和阅读。而vue3.0的组合式API则解决了这个问题;

2.CompositionAPI如何使用?

vue3.0中提供了一个setup的语法糖; setup入参: setup接收两个参数,分别是( props, context ) setup回参: setup可以自定义返回值,一般是一个对象,如果自定义的返回值是一个对象的话,vue会帮我们将这个对象添加到renderContext中去,从而实现数据渲染。

// 代码案例
<template>
  <div class="hello">
    <h1>{{ count }}</h1>
    <button @click="myFn">点击弹窗</button>
  </div>
</template>

<script lang="ts">
import { ref } from "vue";

export default {
  setup() {
  // 一定要通过return暴露出去
    return {
      ...businessFn()
    };
  },
};
// 我们可以将A业务逻辑写在一个函数中,这个函数返回一个对象,B业务逻辑写在另一个函数中;
// 这样实现了业务逻辑的代码分割,易于维护;
function businessFn() {
  let count = ref(0);
  let myFn = () => {
    setInterval(() => {
      count.value += 1;
    }, 1000);
  };
  return {
    count,
    myFn
  }
}
</script>
3.CompositionAPI中setup语法糖的注意点
  1. 没有this:在执行setup的时候组件实例还未创建成功,故获取this是undefined
  2. setup执行时机:setup => beforeCreate(组件刚刚被创建,并且data和methods还未初始化) => created(组件刚刚被创建,并且data和methods刚刚初始化好); 故在setup中是无法访问data和methods的,且vue为了防止我们错误使用,在setup中直接将this修改为了undefined;
  3. 两个入参:props, context
4.CompositionAPI的本质

   CompositionAPI也叫组合式API,也可以叫他注入式API,其实很简单,setup中所有的返回值,在编译的时候,vue会帮助我们将setup中定义的变量注入到data中,定义的函数注入到methods中,等等...这就是setup的本质,在开发阶段帮助我们很大程度上优化了代码结构。

5.ref和reactive基本使用

在setup中,如果直接用let、const定义一个基本数据类型的变量后直接赋值,这个变量是无法做到响应式的,也就是变量更新时无法通知视图更新,所以我们需要用ref和reactive方法来在变量赋值的时候进行处理;

export default {
  setup() {
   const count = ref(0);// 定义基本数据类型的变量用ref
   const dataList = reactive({// 定义引用数据类型变量用reactive
    list: []
  })
   let myFn = () => {
    setInterval(() => {
      count.value += 1;
    }, 1000);
  };
    return {
      count,
      dataList
    };
  },
};
6.ref和reactive知识点
  1. 通过ref赋值的变量,虽然是基本数据类型,但是在虚拟DOM中是一个对象,所以如果需要修改这个值的话,需要通过xxx.value来进行修改; 其实ref本质上也是一个reactive,只是vue为了方便写法,定义了一个ref供我们使用,即:ref(0) === reactive({ value: 0 });   所以我们访问ref定义的变量的时候,需要用xxx.value来访问,咦,其实在我看到这里的时候就就衍生出一个疑惑,因为ref定义的变量在template中是可以直接访问的,并不需要xxx.value来访问;其实这也是因为vue帮我们处理好了,即通过ref定义的变量,在template中,vue会自动帮我们加上.value

  2. 通过reactive赋值的变量,如果传入的数据是json/array的话,reactive会帮助我们将传入的json/array包装成一个proxy对象,既然成为了proxy对象,那么我们直接对里面的属性进行修改是可以被监听从而更新视图的的。但是如果传入的是非json/array类型的对象的话,想要更新视图,只能通过重新赋值的方式进行修改,才可以更新视图

  3. ref和reactive的区别:虽然ref的本质上也是reactive的,但是还是有一些区别; 在template中,通过ref赋值的变量,是不用通过xxx.value来访问的,而reactive是需要的,因为vue在解析模板的时候会去判断该变量是否是通过ref创建的;   那么问题来了,vue是通过什么来判断这个变量是否是通过ref创建的呢?其实vue在模板解析的时候是通过ref变量中的__v_isRef这个私有属性来判断该变量是否为ref创建的, 虽然这是一个私有属性,外界无法访问,但是vue有提供一个isRef()isReactive() 的方法来判断

  4. 不管是ref还是reactive在默认情况下都是递归监听 ,递归监听性能消耗较大,因为所有基本数据类型都会被包装成一个proxy对象,如果想要阻止的对象的所有数据被包装成proxy,只包装第一层的话,可以使用shallowRef以及shallowReactive;

 setup() {
   const dataList = reactive({
    list: ['姓名''年龄''籍贯']
  })
   return {
    count,
    dataList
   };
  }, 
  
/* 
为了验证我们的说法,将打印数据结果贴一下
  console.log(dataList.list)
  Proxy {0: '姓名', 1: '年龄', 2: '籍贯'}
  
  console.log(count)
  {
    dep: Set(1) {ReactiveEffect}
    __v_isRef: true // 私有属性,用于判断是否为ref创建
    __v_isShallow: false
    _rawValue: 1
    _value: 1
    value: 6
    [[Prototype]]: Object
  }
*/
  1. 经过ref或reactive生成的proxy对象与原对象的关系:proxy对象与原对象是一种引用关系,原对象经过ref或reactive的处理,生成了一个proxy对象,而这个proxy对象里面保存了一个地址,这个地址指向的就是原对象, 如图:
const obj = {age: 18};
const proxyObj = reactive(obj);
classDiagram
proxyObj --> obj
class proxyObj {
    0ffc2
}
class obj {
    age: 18
}
7.shallowRef和shallowReactive以及triggerRef

  shallowRef和shallowReactive是用来做非递归监听的,它们都是只监听最外层的数据,如果对象中的属性值也是一个对象的话,不会再进行递归监听; 注意点: shallowRef:监听的不是第一层的数据,而是.value的数据 shallowReactive: 如果对象的属性值还是一个对象的话,不会再进行包装;

const count = shallowRef(0);
const dataList = shallowReactive({
  list: {
      a:{
        b:1
      }
    }
  })

  那么如果我想要修改比如第二层或者第三层的数据,又不想通过递归监听的方式来实现的,那么可以使用triggerRef();调用这个方法并且将更新的变量传入的话,vue会主动的去对比该变量,并将更新的部分通知视图更新;

const dataList = shallowRef({
  list: {
      a:{
        b:1
      }
    }
  })
  datalist.value.list.a.b = 2;
  triggerRef(dataList)
8. toRaw方法

   toRaw方法用于从被ref/reactive方法包装成的proxy对象中获取原数据,因为一个proxy对象的每次操作都会触发监听,那么当我们的一些操作不需要被跟踪的时候,就可以通过toRaw()方法来获取原对象;直接修改原对象是不会被监听到的,如果之后对proxy对象进行了修改,那么之前对原对象的修改也会更新; 注意:如果proxy对象是通过ref包装成的,那么我们需要明确的告知toRaw,我们要获取的是.value中的数据,即 toRaw(xxx.value);

const obj = {age: 18};
const proxyObjA = reactive(obj);
const proxyObjB = ref(obj);
const objA = toRaw(proxyObjA); // objA === obj;
const objB = toRaw(proxyObjB.value); // objB === obj;

9. markRaw方法

  经过markRaw()方法包装的对象,相当于用于告诉vue,这个对象的所有更改不需要被追踪,也就是说,一个对象一旦经过markdown定义,之后这个对象所做的所有更改vue都会忽略监听。   经过markRaw包装的对象,里面会有一个私有属性值 _v_skip:true而这个值为true的时候,vue就会忽略该对象的监听。

10. toRef方法

  这个方法可以单独将对象里面某个属性变成响应式数据,接受两个参数(对象,属性名); ref和toRef的区别:ref将对象中某属性包装成响应式数据之后,修改该属性会触发视图更新,而toRef包装后的属性修改不会触发视图更新 如果想要用toRef方法一次包装同一个对象内的多个属性,可以使用 toRefs( ), 直接传入一个对象,toRefs这个方法会遍历该对象下面的所有属性并执行toRef方法;

  let obj = {
    name: 'yln',
  }
  const dataList = ref(obj.name);
  const toDataList = toRef(obj,'name');

使用场景: 我们通过reactive包装了一个对象,我们在页面中总是需要xxx.xxx来使用,较为繁琐,这时toRefs就派上用场了

toRefs文档:将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。

<template>
  <div class="hello">
    <h1>{{name}}</h1>
    <h2>{{age}}</h2>
    <button @click="myFn">点击</button>
  </div>
</template>

<script>
import { reactive, toRefs } from "vue";
export default {
  setup() {
    const data = reactive({
      name: 'yyy',
      age: 18
    })
    const myFn = () => {
      data.age = 20;//由于toRef是对单个属性的监听,所以直接对对象属性修改,可以被监听
    }
    return {
    ...toRefs(data),
    myFn
    };
  },
};

</script>
11. customRef实现自定义ref

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显式控制。 它需要一个工厂函数,该函数接收 track 和 trigger函数作为参数,并且应该返回一个带有 get 和 set 的对象。

  在文档中我们可以知道,vue提供了一个customRef的方法,用来自定义ref,并且可以对自定义的这个ref进行显式控制。 那么,track 和 trigger有什么作用呢? track:调用后相当于告诉vue,这个方法所返回出去的数据在未来是需要追踪它的变化的,即响应式; trigger:调用后相当于告诉vue,此时此刻需要进行视图更新,与上文的triggerRef()类似

function useDebouncedRef(value) {
  return customRef((track, trigger) => {
    return {
      get() {
        track() // 告诉vue,return出去的数据需要追踪变化
        return value
      },
      set(newValue) {
          value = newValue
          trigger() // 新的数据成功更新后,更新视图
      }
    }
  })
}

  使用场景:由于我们不能对setup函数进行async,如果我们在setup中需要进行异步请求,需要写一堆的回调,如果用customRef的话,就可以封装在一个方法中了;不仅能够优化代码结构,同时也不需要重新对数据进行ref操作;(我想着这个函数应该应用挺少的吧,一般异步放在methods并且在created里面调用,常规想法更好一些?)

function getData(value) {
  return customRef((track, trigger) => {
  异步函数
    xxx.then(() => {
    ...// 逻辑
    trigger();
    },
    err => {...}
    )
    return {
      get() {
        track() // 告诉vue,return出去的数据需要追踪变化
        return value
      },
      set(newValue) {
          value = newValue
      }
    }
  })
}

setup(){
    const data = getData([]);
    return {
        data
    }
}
12.使用ref获取DOM元素

  在vue2.0中,我们是通过this.refs.refName的方式来获取DOM元素,但是在vue3中,取消了$refs这个东西,在setup中获取DOM,需要在setup中通过ref定义一个和所要获取DOM的ref同名的变量,然后将这个变量return出去,那么在这个DOM被初始化后,我们通过ref定义的这个变量,会自动获取到对应的ref的DOM;

<template>
  <div class="hello">
    <h1 ref="box">title</h1>
  </div>
</template>

<script lang="ts">
import { ref, onMounted } from "vue";
export default {
  setup() {
  const box = ref(null); //此处定义的变量名需和ref同名
  onMounted(() => {
    console.log('onMounted:',box.value);// 
  })
  return {
   box
  };
  },
};

</script>
13.readonly、shallowReadonly与isReadonly

readonly: 顾名思义,这是一个创建只读类型数据的方法,这个解释不由得让人想起const,没错readonly和const类似,但是还是有一些区别;有人说用这个方法创建变量和直接定义一个静态数据有什么区别,在我看来,这个方法使vue更加严谨,防止静态数据可能会出现的一些误操作的情况。 shallowReadonly: 只将对象的第一层数据包装成只读数据; isReadonly: 判断变量是否为只读数据,返回boolean值

<script>
import { ref, reactive, toRefs, readonly,isReadonly } from "vue";
export default {
  setup() {
    const data = readonly({
      name: 'yyy',
      age: 18
    })
    console.log(isReadonly(data));//true
    const myFn = () => {
      data.age = 20; //数据并未被改变
      //浏览器控制台报警告[Vue warn] Set operation on key "age" failed: target is readonly. 
    }
    return {
      name: data.name,
      age: data.age,
      myFn
    };
  },
};

</script>

readonly和const的区别:

  1. 针对引用数据类型 const:赋值保护,即变量不能够被重新赋值,但是如果对变量内的属性进行修改是可以的; readonly:属性保护,即变量通过readonly被定义后,属性是不可被修改的
  2. const会报错从而终止程序执行,而readonly报出的是警告,程序继续执行,对对象属性的修改不会生效
14.用Proxy实现一个数据监听

  在vue3中,尤大用Proxy替代了之前的defineProperty,而Proxy,顾名思义就是代理的意思,在我看来,相当于在对象外面套了一层,从而每次访问被Proxy包装的对象时候,都需要通过Proxy -> 变量,从而实现数据监听。下面手动实现一下用Proxy来包装对象

const makeProxyFn = function(obj){
   const proxyObj = new Proxy(obj, {
       get(currentObject, key){//接收两个参数,对象本身、属性名
        return currentObject[key];
       },
       set(currentObject, key, newValue){ //接收三个参数,当前对象,属性名,新值
        currentObject[key] = newValue;
        console.log('通知UI界面更新');
        return true // 很有必要
       },
   })
   return proxyObj
}

const data = makeProxyFn({
    name: 'yyy',
    age: 18
})

get新知识点:为什么在set中需要return一个true呢?目的是为了表示,本次set操作已经完成,可以进行下一次set操作了; 如果用Proxy代理的是一个数组,那么在对数组进行push()等操作的时候,set实际上是会触发两次的,第一次是往数组末尾的位置添加一个元素,第二次是将数组的length+1,如果不return true的话,就有可能会导致第二次操作无法执行。

未完待续....