VUE3浅析---响应式

VUE3中setup语法糖解决响应式的方案,所有的只要被ref或者reactive包裹的变量,都会转变成响应式。而在VUE2中,要想做成响应式,必须将变量定义在data函数中。



1、ref:将一个属性或者对象定义成ref对象,也就是将一个属性或者对象变成响应式,修改值必须.value才能处理对应的值。

  • 以下代码定义了三个User对象,并且都是使用ref做成了响应式,当点击按钮改变User对象的值的时候,页面上的值也会被改变,这就是响应式的作用。
  • 使用ref获取dom元素。
import {
    
     ref, onMounted } from 'vue'
import type {
    
     Ref } from 'vue' // Ref是一个类型定义,类型定义导入的时候必须使用type关键字

// 定义User的各个属性的字段类型
type UserType = {
    
    
	name: string
	age: number
}

// 三种不同的User对象的定义
const User = ref<UserType>({
    
     name: '小明', age: 12 })
const User1: Ref<UserType> = ref({
    
     name: '小明', age: 12 })
const User2 = ref({
    
     name: '小明', age: 12 })

const refTest = () => {
    
    	
	User.value.age = 18
	User1.value.age = 18
	User2.value.age = 18
}

// 使用ref获取dom元素
const dom = ref<HTMLElement>()
onMounted(() => {
    
    
	console.log(dom.value?.innerHTML) // onMounted结束之后,才能获取到dom元素,所以需要放在onMounted中才能获取到dom
})

<button @click="refTest" style="height: 100px; width: 100px; background: green">refTest</button>
<div>
	<p>User: {
    
    {
    
     User }}</p>
	<p>User1: {
    
    {
    
     User1 }}</p>
	<p>User2: {
    
    {
    
     User2 }}</p>
</div>

<div ref="dom">通过ref获取dom</div>

2、isRef:用来判断一个属性或者对象是不是ref对象。

isRef实际上在项目中很少使用,然而在ref源码中很多地方都在使用

import {
    
     ref, isRef } from 'vue'
const a = ref<number>(1)
const b = 1
console.log('a是ref对象:', isRef(a))
console.log('b是ref对象:', isRef(b))

a是ref对象: true
b是ref对象: false

3、shallowRef:和ref的作用相似,但是shallowRef只能用来做浅层响应式

shallowRef只能用来做浅层响应式,也就是说他只能做到修改到.value的这一层,.value后边的数据他不能响应式的修改。

  • 当我们点击shallowRefTest按钮时,UserE2在页面上的输出是没有任何变化的,但是如果我们在控制台上去看UserE2对象的时候会发现,实际上他的值已经改变了,但是不能渲染到页面上。
  • 当我们点击shallowRefTest1按钮时,UserE2在页面上的输出发生了变化,这是因为UserE1的处理是正确的,触发了整个ref的机制,导致UserE2的值也被改变了,并正确的渲染到了界面上。
  • 当我们点击shallowRefTest2按钮时,UserE2、UserE1在页面上的输出发生了变化,这是因为UserE1的处理是正确的,shallowRef的响应式处理只能从.value后边修改。
  • 当我们点击shallowRefTest3按钮时,UserE2、UserE1在页面上的输出发生了变化,这是因为ref的改变的会影响shallowRef的改变,也就是说,shallowRef和ref的处理放一起的时候会被ref影响,shallowRef也会变成深响应式,原因是ref底层会调用triggerRef,而triggerRef会强制收集所有的改变,进而导致shallowRef深层次的改变。
import {
    
     ref, shallowRef, triggerRef } from 'vue'
const a = ref<number>(1)
const UserE1 = shallowRef({
    
     name: "小明", age: 12 });
const UserE2 = shallowRef({
    
     name: "小明", age: 12 });
const shallowRefTest = () => {
    
    
    UserE2.value.age = 18;
};

const shallowRefTest1 = () => {
    
    
    UserE2.value.age = 18;
    UserE1.value = {
    
    
        name: "小明1",
        age: 121,
    };
};

const shallowRefTest2 = () => {
    
    
    UserE2.value = 18;
    UserE1.value = {
    
    
        name: "小明1",
        age: 121,
    };
};

const shallowRefTest3 = () => {
    
    
    a.value = 10;
    UserE2.value = 18;
};

<button @click="shallowRefTest" style="height: 100px; width: 100px; background: green">shallowRefTest</button>
<button @click="shallowRefTest1" style="height: 100px; width: 100px; background: green">shallowRefTest1</button>
<button @click="shallowRefTest2" style="height: 100px; width: 100px; background: green">shallowRefTest2</button>
<button @click="shallowRefTest3" style="height: 100px; width: 100px; background: green">shallowRefTest3</button>
<div>
	<p>UserE1: {
    
    {
    
     UserE1 }}</p>
	<p>UserE2: {
    
    {
    
     UserE2 }}</p>
</div>

4、triggerRef:强制收集所有的改变,和shallowRef一起使用,将shallowRef也变成深层次相应,即得到和ref一样的效果,但是ref和shallowRef不要一起使用,因为ref底层会调用triggerRef,会导致shallowRef的值也会被强制更新

  • 当我们点击shallowRefTest4按钮时,如果我们不加triggerRef(UserE2),UserE2在页面上的输出不会发生变化,如果我们加triggerRef(UserE2),UserE2在页面上的输出会发生变化,shallowRef也会变成深响应式,原因是triggerRef会强制收集所有的改变,进而导致shallowRef深层次的改变。
import {
    
     shallowRef, triggerRef } from 'vue'
const UserE2 = shallowRef({
    
     name: "小明", age: 12 });

const shallowRefTest4 = () => {
    
    
	UserE2.value.age = 18
	triggerRef(UserE2) // 主动调用,触发更新操作
}

<button @click="shallowRefTest4" style="height: 100px; width: 100px; background: green">shallowRefTest4</button>
<div>
	<p>UserE2: {
    
    {
    
     UserE2 }}</p>
</div>

5、customRef:自己实现ref的逻辑,在实现的过程中可以自己增加其他额外的逻辑处理。

customRef允许我们自己实现ref的逻辑,并增加一些额外的处理,它的实现逻辑主要依赖于get和set方法。比如我们在set的时候需要从后台获取的,假设我们一次调用了一百次后台,但是获取的都是同一个值,那这样我们可以在set方法中使用setTimeOut进行防抖处理,避免服务器压力过大。

import {
    
     customRef } from 'vue'
const myRefTest = MyRef<string>('myRefTest')
const myRefChange = () => {
    
    
	myRefTest.value = 'myRefTest:我自己实现了ref'
}

function MyRef<T>(value: T) {
    
    
    return customRef((track, triggeer) => {
    
    
        return {
    
    
            get() {
    
    
                track();
                return value;
            },

            set(newValue) {
    
    
                value = newValue;
                triggeer();
            },
        };
    });
}

<button @click="myRefChange" style="height: 100px; width: 100px; background: green">myRefChange</button>
<div>
	<p>myRefTest: {
    
    {
    
     myRefTest }}</p>
</div>

6、reactive:将一个对象变成响应式,修改值必须.属性即可处理对应的值

  • 以下代码定义了一个UserE3对象,并且都是使用reactive做成了响应式,当点击按钮改变UserE3对象的值的时候,页面上的值也会被改变,也达到了响应式的作用。
  • reactive定义对象,因为在reactive的定义中,约束了传入的值只能为Object
import {
    
     reactive } from 'vue'
// 定义属性约束
type UserType = {
    
    
    name: string;
    age: number;
};
// 定义一个reactive的对象
const UserE3 = reactive<UserType>({
    
     name: "小明", age: 12 });
const shallowRefTest5 = () => {
    
    
    UserE3.age = 18;
};

<button @click="shallowRefTest5" style="height: 100px; width: 100px; background: green">shallowRefTest5</button>
<div>
     <p>UserE3: {
    
    {
    
     UserE3 }}</p>
</div>

7、shallowReactive:也是将一个对象变成浅层响应式,即只能处理对象的第二层属性的值,和shallowRef一样的特性。

import {
    
     shallowReactive } from 'vue'
const shallowReactiveE4 = shallowReactive<any>({
    
    
    foot: {
    
    
        bar: {
    
    
            name: "bar",
        },
    },
});
const shallowRefTest8 = () => {
    
    
    // shallowReactiveE4.foot.bar.name = 22 // 这里实际上不能修改name的值,只能处理对象的第二层属性的值,即foot这一层
    shallowReactiveE4.foot = 22; // 这里实际上能修改foot的值
};

<button
    @click="shallowRefTest8"
    style="height: 100px; width: 100px; background: green"
>
    shallowRefTest8
</button>
<div>
    <p>shallowReactiveE4: {
    
    {
    
     shallowReactiveE4 }}</p>
</div>

8、readonly:将reactive对象变成只读对象,该只读对象不允许再次被赋值等操作,但是该只读对象值依旧受原始对象值的影响。

import {
    
     reactive, readonly } from 'vue'
const readonlyUserE3 = readonly(UserE3);
const shallowRefTest7 = () => {
    
    
    readonlyUserE3.age = 22; // 这里直接对readonlyUserE3操作不会改变readonlyUserE3对象属性
    // UserE3.age = 18  // 如果这里改变的是原始对象,readonlyUserE3也会受影响
};

<button
    @click="shallowRefTest7"
    style="height: 100px; width: 100px; background: green"
>
    shallowRefTest7
</button>
<div>
    <p>readonlyUserE3: {
    
    {
    
     readonlyUserE3 }}</p>
</div>

9、reactive和ref的区别

  • reactive只能定义Object类型的值作为响应式,比如:Map,List等,而ref可以将任意值变成响应式,包括对象,字符串,数字等。
  • reactive在处理值的时候直接.属性即可处理对应的值,而ref需要.value才能处理
  • reactive在异步场景下不能直接赋值,否则会破坏他的proxy代理对象,从而无法变成响应式。解决办法为:
    • 将data解构并使用push方法进行添加
    • 定义类似于list1的对象,对象里面在放arr数组属性,然后在使用=将data直接复制给list1对象里面的arr数组属性,使用的时候也是list1.arr
let list = reactive<string[]>([]);
let list1 = reactive<{
      
      
    arr: string[];
}>({
    
    
    arr: [],
});
const shallowRefTest6 = () => {
    
    
    setTimeout(() => {
    
    
        const data = ["data1", "data2", "data3", "data4", "data5"];
        // list = data  // 直接用等号赋值会发现,list实际上已经有值了,并且在控制太也能看到,但是页面没有渲染,这就说明:=会破坏响应式
        list.push(...data); // 解决办法1:就是将data解构并使用push方法进行添加
        list1.arr = data // 解决办法2:使用对象,将数组变为对象的一个属性,并直接赋值。
        console.log(list);
    }, 1000);
};

<button @click="shallowRefTest6" style="height: 100px; width: 100px; background: green">shallowRefTest6</button>
<div>
	<ul>
		<li v-for="item in list" :key="item">{
    
    {
    
     item }}</li>
		<li v-for="item in list1.arr" :key="item">list1 + {
    
    {
    
     item }}</li>
	</ul>
</div>

10、响应式原理

Object.defineProperty和Proxy是VUE实现响应式的关键,而VUE2的响应式使用的是Object.defineProperty,而VUE3使用的是Proxy。

10.1 VUE2的响应式原理

a、Object.defineProperty解读:

Object.defineProerty是JS内置对象Object的原生的静态方法,主要用来在一个对象上定义或者修改一个属性并返回该对象。defineProerty有三个参数,他的源码定义如下:

/**
* 将属性添加到对象,或修改现有属性的属性。
* 
* @param o 要操作的对象。
* @param p 要操作的对象的属性
* @param attributes 对这个要操作的对象的这个属性的一些描述,比如该属性是不是可以被遍历等。
*/
defineProperty<T>(o: T, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): T;

// PropertyDescriptor的定义如下
interface PropertyDescriptor {
    
    
    configurable?: boolean; // 是否可以被删除属性或者再次去修改enumerable、writable这些特性,默认为false,即调用delete时无法删除该属性
    enumerable?: boolean; // 该属性是不是可以被枚举(使用fo……in或者Object.keys去循环该对象时,该这个属性可否可见,默认再循环时不允许读取该属性),默认为false。
    value?: any; // 该属性的值,默认为undefined,当使用set和get方法的时候,该属性不能被使用了,两者冲突
    writable?: boolean; // 该属性是不是可以重新赋值。即使用=号给该属性赋值的时候不生效,当使用set和get方法的时候,该属性不能被使用了,两者冲突
    get?(): any; // get方法,当获取该属性的值的时候触发这个方法。注意:不要在get中再次对属性进行获取,这样相当于递归调用,会引发栈溢出错误。
    set?(v: any): void; // set方法,当给这个属性设置值的时候触发这个方法。注意:不要在get中再次对属性赋值,这样相当于递归调用,会引发栈溢出错误。
}

简单是使用Object.defineProerty来实现一个数据响应式的案例,当使用User.name获取name的值的时候,get方法会被触发,当使用User.name = nValue给name的赋值的时候,set方法会被触发。

const User = {
    
     name: "zs", age: 19, sex: "男" };

let cache = User.name;
Object.defineProperty(User, "name", {
    
    
  configurable: true,
  enumerable: true,
  //   value: undefined, // 使用了set和get,该特性不能再使用了,冲突
  //   writable: true, // 使用了set和get,该特性不能再使用了,冲突
  get: function getter() {
    
    
    console.log("获取name属性的值"); // 当使用User.name获取name的值的时候,get方法会被触发,进而该语句会被打印
    // return User.name; // 不能再get方法执行 User.name取值操作,该操作会再次触发get方法,进而形成死循环,引发栈溢出错误。
    return cache;
  },
  set: function setter(nValue) {
    
    
    console.log("设置name属性的值,新值为:" + nValue + "旧值为:" + cache); // 当使用User.name = nValue给name的赋值的时候,set方法会被触发,进而该语句会被打印
    // User.name = nValue; // 不能再set方法执行 User.name = nValue赋值操作,该操作会再次触发set方法,进而形成死循环,引发栈溢出错误。
    cache = nValue;
  },
});

b、实现VUE2的响应式

VUE2的响应式使用的是Object.defineProperty,利用该特性手工实现VUE2的响应式

// 模拟VUE对象,需要使用new关键字初始化MyVue对象的实例
class MyVue {
    
    
  constructor(options) {
    
    
    this._data = options.data; // 将data对象挂载到MyVue对象的实例上
    this._options = options; // 将options对象挂载到MyVue对象的实例上,备用
    this.initData(); // 初始化MyVue对象的实例的时候,就去做拦截的实现
  }

  // 拦截的实现
  initData() {
    
    
    let data = this._data;
    let keys = Object.keys(data);
    for (const index in keys) {
    
     // 循环data中的每一个属性,准备对data中的每一个属性进行拦截,并将data中的每一个属性挂载MyVue对象的实例上
      Object.defineProperty(this, keys[index], {
    
    
        enumerable: true,
        configurable: true,
        get: function myGetter() {
    
    
          console.log("获取" + keys[index] + "值 :" + data[keys[index]]); //  myVue.name获取name的值的时候或者myVue.age获取age的值的时候,模拟拦截处理,这里做打印
          return data[keys[index]]; //在做返回data中该key的值,
        },
        set: function mySetter(nValue) {
    
    
          console.log(
            "设置" +
              keys[index] +
              "值, 新值:" +
              nValue +
              " 旧值:" +
              data[keys[index]]
          ); // myVue.name = 'lisi'设置name的值的时候或者myVue.age = 28设置age的值的时候,模拟拦截处理,这里做打印
          data[keys[index]] = nValue; // 将设置的新值保存到data中
          document.getElementById("div").innerText = JSON.stringify(data); // 模拟VUE中响应式,同步更新dom的值
        },
      });
    }
	// 处理data数据响应式,
    observer(data);
  }
}

// 将data数据响应式的入口,封装成为一个类调用
class Observer {
    
    
  constructor(data) {
    
    
    this.worker(data);
  }

  worker(data) {
    
    
    let keys = Object.keys(data);
    // 将data上的key做深层次的响应式处理,后边会用到递归处理,试想:data中的某一个key的数据依旧是一个复杂结构的对象
    keys.forEach((key) => {
    
    
      definedReactive(data, key, data[key]);
    });
  }
}

function observer(data) {
    
    
  // 如果data是一个基本数据类型,就返回
  const type = Object.prototype.toString.call(data); // 这样获取数据的类型相比于typeOf来说更加精确,
  if (type !== "[object Object]" && type !== "[object Array]") {
    
    
    return;
  }

  new Observer(data);
}

// data数据响应式处理的逻辑,实际上就是将data上的key深层次使用Object.defineProperty进行拦截处理
function definedReactive(target, key, value) {
    
    
  observer(target[key]);
  Object.defineProperty(target, key, {
    
    
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
    
    
      console.log("获取" + key + "值 :" + value);
      return value;
    },
    set: function reactiveSetter(nValue) {
    
    
      if (value === nValue) {
    
    
        return;[[readme]]
      }
      console.log("设置" + key + "值, 新值:" + nValue + " 旧值:" + value);
      document.getElementById("div").innerText = JSON.stringify(nValue);
      value = nValue;
    },
  });
}
<!DOCTYPE html>
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Object.defineProperty</Object></title>
</head>

<body>
    <div id="div"></div>
    <button onclick="change()" style="width: 100px; height: 30px; background: green;">改变值</button>
</body>

<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="index.js"></script>
<script>
    let myVue = new MyVue({
      
      
        data: {
      
      
            name: 'zs',
            age: 18,
            list: [1, 2, 3],
            class: {
      
      
                id: 001,
                leave: '一年级',
                dep: {
      
      
                    no: 001,
                    status: '开启'
                }
            }
        }
    }); // 模拟new VUE创建MyVue的实例
    document.getElementById('div').innerText = myVue.name + "----" + myVue.age;

	// 这里通过设置值,模拟VUE双向绑定的操作。界面上的值也会改变。同时保证myVue实例里面的值也会改变
    function change() {
      
      
        alert("改变值?");
        myVue.age = 28;
        myVue.name = 'lisi';
        myVue.class.id = 002;
        myVue.class.dep.no = 002;
    }

</script>

</html>

10.2、VUE3的响应式原理

注意:暂时没有实现深层次的响应式,也就是当Person对象中该有对象时,无法做到响应式。

a、Proxy解读

Proxy是JavaScript中的一个内置对象,用于生成对象的代理对象,用于对对象的操作进行拦截,例如:查找、删除、增加、修改、执行等。Proxy中接收两个参数

  • target: 要代理的对象。
  • handler: 要对target进行的操作,通常是target的各种操作,例如:查找、删除、增加、修改、执行等。
    • set:增加一个属性或者删除一个属性
    • deleteProperty: 删除某一个属性
    • get: 获取一个属性的值
const p = new Proxy(target, handler)

/**
 * 生成一个对象的代理对象
 * 
 * @param {object} Person 这是一个对象,包含了一些可选的属性,即源数据
 * @param {string} handler 这是一个handle,通常是一个对象,或者函数,用于访问目标数据的方法
 */
const p = new Proxy(Person, {
    
    
    get(target, prop) {
    
     // 访问对象上的某一个属性

    },
    set(target, prop, value) {
    
     // 修改对象上的属性的值或者是新增一个属性
       
    },
    deleteProperty(target, prop) {
    
     // 删除对象上的一个属性
       
    }
});
b、Reflect解读

Reflect是一个内置对象,是不可构造的,它方法都是静态的,通常和Proxy一起联合使用,使用Reflect可以增强代码的可读性,使得代码更具编程式风格。目前,Reflect 具有Object的部分功能,某些情况下可以替换Object。以下列举几个常用的方法:

/**
 * 获取一个对象的属性的值
 * 
 * @param {object} target 这是一个对象,包含了一些可选的属性,即源数据
 * @param {string} prop 该对象中的某一个属性的名称
 * @param {string} receiver 可选
 */
Reflect.get(target, prop, receiver); // 获取值交给Reflect处理

/**
 * 设置对象的属性的值
 * 
 * @param {object} target 这是一个对象,包含了一些可选的属性,即源数据
 * @param {string} prop 该对象中的某一个属性的名称
 * @param {string} value 新值
 */
Reflect.set(target, prop, value); // 获取值交给Reflect处理

/**
 * 函数调用
 * 
 * @param {object} target 要调用函数名
 * @param {object} thisArguments this对象,可为空
 * @param {object} argumentsList 参数,可为空,多个参数是一个数组,无参可以传递任意空对象
 */
Relfect.apply(target,thisArguments,argumentsList)

c、实现VUE3中的响应式
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Proxy</Object></title>
</head>

<body>
    <div id="div">
        姓名:<h1 id="name"></h1>
        年龄:<h1 id="age"></h1>
        性别:<h2 id="gender"></h2>
    </div>

    <button onclick="change()" style="width: 100px; height: 30px; background: green;">改变值</button>
    <button onclick="deletePro()" style="width: 100px; height: 30px; background: green;">删除属性</button>
</body>

<script>

    const Person = {
    
    
        name: 'Jonah',
        age: 39
    }

    document.getElementById('name').innerHTML = Person.name
    document.getElementById('age').innerHTML = Person.age

    /**
     * 生成一个对象的代理对象
     * 
     * @param {object} Person 这是一个对象,包含了一些可选的属性,即源数据
     * @param {string} handler 这是一个handle,通常是一个对象,或者函数,用于访问目标数据的方法
     */
    const p = new Proxy(Person, {
    
    
        get(target, prop, receiver) {
    
    
            console.log('get', `获取${
      
      target}${
      
      prop}的值`);
            return Reflect.get(target, prop, receiver); // 获取值交给Reflect处理
        },
        set(target, prop, value) {
    
    
            console.log('set', `修改${
      
      target}${
      
      prop}的值或者新增一个${
      
      prop}属性`);
            document.getElementById(prop).innerHTML = value // 模拟更新dom
            return Reflect.set(target, prop, value); // 修改值交给Reflect处理
        },
        deleteProperty(target, prop) {
    
    
            console.log('deleteProperty', `删除${
      
      target}${
      
      prop}${
      
      prop}属性`);
            document.getElementById(prop).style.display = 'none' // 模拟更新dom
            return Reflect.deleteProperty(target, prop); // 删除值交给Reflect处理
        }
    });

    const change =
        () => {
    
    
            alert('修改age的值为')
            p.age += 1;
            console.log('change', p.age);
            alert('添加一个gender属性')
            p.gender = '女'

        }

    const deletePro =
        () => {
    
    
            alert('删除gender属性')
            delete p.gender

        }
        
</script>

</html>


猜你喜欢

转载自blog.csdn.net/qq_22610595/article/details/130959088