响应式介绍与原理

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-09-24

reactive

reactive 是 Vue3 中提供的实现响应式数据的方法,在 Vue2.0 中响应式数据是通过 defineProperty 来实现的,而在 Vue3.0 中响应式数据是通过 ES6 的 Proxy 来实现的。

需要注意的是:

  • reactive 参数必须是对象,会被包装成一个 Proxy 对象。

  • reactive 传递了值类型,不会被包装后才能一个 Proxy 对象,仍是一个值类型,也就不会是响应式数据。

  • 如果给 reactive 传递是一个如果不是json或者array,而是一个其他对象,那么直接修改这个对象界面是不会更新的,而要重新赋值才行,如下例中的 time

demo 如下:

<template>
  <div>
    <p>{{ state }}</p>
    <button @click="handleClick">click</button>

    <p>{{ obj.time }}</p>
    <button @click="handleTime">加一天</button>
  </div>
</template>
<script>
import { reactive } from 'vue';
export default {
  name: 'App',
  setup() {
    let state = reactive(123); // 传值的情况

    function handleClick() {
      state = 'hello';
      console.log(state); // hello 值被改变了,但是界面不会更新
    }

    let obj = reactive({
      time: new Date()
    })

    function handleTime() {
      obj.time.setDate(obj.time.getDate() + 1); // 日期加一天
      console.log(obj.time); // 值确实被加了一天,但是界面不会更新

      // 需要这样才行
      const newTime = new Date(obj.time);
      obj.time = newTime;
    }

    return { state, handleClick, obj, handleTime };
  }
}
</script>

ref

为了解决 reactive 只能接受一个对象的问题,Vue3 提供 ref 来处理值类型的响应式。 ref 的本质就是:ref(3) -> reactive({value: 3})

在Vue模板中使用,可以直接使用 const a = ref(3) 处理过后的变量 {{ a }}。但是在 js 业务代码中,必须 a.value 来访问。

需要注意的是,在 template 里使用的是ref 类型的数据,那么 Vue 会自动渲染为 .value 的值,但是如果是 reactive 类型的数据,是不会有这一步操作的。内部通过当前数据的 a.__v_ref 这个属性值来判断是否是 ref,同时也暴露了isRefisReactive 两个方法来供使用者判断。

示例:

<template>
  <div>
    <!-- 显示 3 -->
    <p>{{ age1 }}</p>
    <!-- 显示  { value : 3 } -->
    <p>{{ age2 }}</p>
  </div>
</template>
<script>
import { reactive, ref, isRef } from 'vue';
export default {
  name: 'App',
  setup() {
    let age1 = ref(3);

    let age2 = reactive({ value: 3 })

    console.log(isRef(age1)); // true

    console.log(isRef(age2)); // false

    return { age1, age2 };
  }
}
</script>

ref 也能接受一个对象,同时也可以像 reactive 进行递归监听,但是需要注意的是即使传入ref的是一个对象,更改的时候还是要使用 .value

export default {
  setup() {
    let state = ref({
      a: {
        b: {
          c: 2
        }
      }
    })

    function handle() {
      state.value.a.b.c = 1; // 要这样更改
    }
  }
}

shallowReactive 和 shallowRef

递归监听是比较消耗性能的,可以使用 shallowReactiveshallowRef 进行非递归监听,只监听第一层。

<template>
  <div>
    <button @click="handleState">state is: {{ state.a.b.c }}</button>
  </div>
</template>
<script>
import { shallowReactive, shallowRef } from 'vue';
export default {
  name: 'App',
  setup() {
    const state = shallowReactive({
      a: {
        b: {
          c: 1
        }
      }
    })

    function handleState() {
      // 界面是不会发生变化的 因为 shallowReactive 没有监听到 state.a.b.c 只监听到了 state.a
      state.a.b.c = 2;

      // 这样赋值是可以的,界面也会被更新为3
      // state.a = {
      //   b: {
      //     c: 3
      //   }
      // }
    }

    return { state, handleState };
  }
}
</script>

需要注意的是 shallowRef 进行非递归监听一个对象的时候,监听的是 .value。因为 shallowRef(a) 近似于 shallowReactive({ value: a }),所以 .value 才是第一层。

const state = shallowRef({
  a: {
    b: {
      c: 1
    }
  }
})

function handleState() {
  // state.a = 1; // 这样是不行的

  // 这样赋值是可以的,界面也会被更新为3
  state.value = {
    a: {
      b: {
        c: 3
      }
    }
  }
}

针对 ref 还有一个方法就是 triggerRef,用来触发非递归监听属性的更新,但是没有提供 triggerRecative

import { triggerRef, shallowRef } from 'vue';

export default {
  setup() {
    const state = shallowRef({
      a: {
        b: {
          c: 1
        }
      }
    })

    function handleState() {
      state.value.a.b.c = 2; // 这样不会更新
      triggerRef(state); // 主动更新界面
    }
  }
}

toRaw

通过 toRaw 方法可以拿到响应式数据的原始对象。

import { reactive, toRaw } from 'vue';

export default {
  setup() {
    let obj = { name: 'jack' }
    let state = reactive(obj)

    console.log(obj === state); // false
    let obj2 = toRaw(state);
    console.log(obj === obj2); // true
  }
}

如果想通过 toRaw 拿到 ref 类型的原始数据(创建时传入的那个数据),那么就必须明确的告诉toRaw方法,要获取的是 .value 的值,因为经过 Vue 处理之后,.value 中保存的才是当初创建时传入的那个原始数据。

export default {
  setup() {
    let obj = { name: 'jack' }
    let state = ref(obj)
    let obj2 = toRaw(state.value);
    console.log(obj === obj2); // true
  }
}

markRaw

markRaw 方法用来标记一个对象永远都不要被追踪。

import { reactive, markRaw } from 'vue';

export default {
  setup() {
    let obj = { name: 'jack' }
    obj = markRaw(obj)
    let state = reactive(obj)

    function change() {
      state.name = 'andy' // 不会更新视图,因为被markRaw标记的对象,不会被追踪成为响应式数据
    }

    return {state, change }
  }
}

toRef

toRef某一个对象中的属性变成响应式的数据

export default {
  setup() {
    // 使用 `ref` 将某一个对象中的属性变成响应式的数据,我们修改响应式的数据是不会影响到原始数据的。
    let obj = { name: 'jack' }
    let state = ref(obj.name)
    state.value = 'andy';
    console.log(obj); // { name: 'jack' }

    // 当使用 `toRef` 将某一个对象中的属性变成响应式的数据,当我们修改响应式的数据时会影响到原始数据
    let obj2 = { name: 'jack' }
    let state2 = toRef(obj2, 'name'); // 要这么使用
    state2.value = 'andy';
    console.log(obj2); // { name: 'andy' }
  }
}

toRefs

使用toRefs 可以将某一个对象中的多个属性变成响应式对象。

export default {
  setup() {
    let obj = { name: 'jack', age: 18 }
    let state = toRefs(obj) // 要这么使用
    
    // 需要先访问.name 才访问 .value
    state.name.value = 'andy'; 
    state.age.value = 20;
  }
}

customRef

customRef 返回一个 ref 对象,可以显示地控制依赖追踪和触发响应。

import { customRef } from 'vue';

function myRef(value) {
  // customRef 用来自定义一个 ref
  // 接收一个回调函数,这个函数需要返回一个有 get 和 set 的对象,用来定制响应式方法的处理逻辑
  // 这个回调有两个参数, track 用来告诉Vue这个数据是需要追踪变化的(要与视图双向绑定),trigger 用来告诉Vue触发界面更新
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        console.log('get', value);
        return value;
      },
      set(newVal) {
        console.log('set', newVal);
        value = newVal;
        trigger();
      }
    }
  })
}

export default {
  setup() {
    let name = 'jack' 
    let state = myRef(name)

    function change() {
      state.value =  'andy';
    }
    return { state, change };
  }
}

setup 函数只能是同步的,不能是异步的,那么在 setup 中就不能使用 async/await,那么一些通过网络请求获取数据的逻辑如果放在 setup 中就得使用 .then 的方式来书写。 在某些场景下,可以将获取数据的方法放在customRef中,即获取到数据之后就进行响应式的追踪以及之后的触发。

<template>
  <div>
    <ul>
      <li v-for="item in state" :key="item.name">
        {{ item.name }}
      </li>
    </ul>
    <button @click="change">click</button>
  </div>
</template>

<script lang="ts">
import {customRef } from 'vue'

function myRef(dataPath) {
  return customRef((track, trigger) => {
    let val = [];

    fetch(dataPath)
    .then((res) => res.json())
    .then((data) => {
      console.log('获取到数据了');
      val = data; // get return的是什么,响应数据就是什么
      trigger();
    })
    .catch(console.log)
    return {
      get() {
        // 千万不能再get中获发送网络请求获取数据
        // 会变成死循环,渲染页面就要触发get发送网络请求,获得数据就更新页面,就重新渲染页面就又得发生网络请求。
        track();
        return val;
      },
      set(newVal) {
        val = newVal;
        trigger();
      }
    }
  })
}

export default {
  setup: () => {
    let state = myRef('./data.json')

    console.log(state.value); // get return出来的 []

    function change() {
      state.value = [
        ...state.value,
        {name: 'zs'}
      ]
    }

    return { state, change };
  }
}
</script>

使用 ref 获取DOM元素

在 Vue2 中,可以使用 this.$refs 访问到使用了 ref 属性的DOM元素,而在 Vue3 中,访问方式变得不一样了。

<template>
  <div>
    <!-- 与Vue2写法一样 -->
    <p ref="myDom">this is p</p>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue';
export default {
  setup() {
    let myDom = ref(null);
    
    // 当页面挂载完成之后,会将这个Dom赋值给myDom.value
    onMounted(() => { // 在Vue3中,使用钩子函数,要这样注册
      console.log('myDom', myDom.value); // DOM 
    })

    console.log(myDom.value); // null

    return { myDom } // 要在这里暴露出去
  }
}
</script>

readonly

readonly 方法用于创建一个只读的数据(template中可以使用),并且是递归只读。

shallowReadonly 创建一个只读数据,但是不是递归只读,则只是让第一层为只读。

isReadonly 判断是否是只读的。

<template>
  <div>
    <p>{{ obj.name }}</p>
    <p>{{ obj2.attr.name }}</p>
    <button @click="change">click</button>
  </div>
</template>

<script>
import { readonly, shallowReadonly, isReadonly } from 'vue';
export default {
  setup() {

    const obj = readonly({ name: 'jack'})
    const obj2 = shallowReadonly({ name: 'andy', attr: { age: 18}})

    function change() {
      obj.name = 'andy'; // 修改无法生效,界面不会更新,会报警告
      obj2.attr.age = 20; // shallowReadonly 修改生效,但是界面也不会更新
    }

    console.log(isReadonly(obj)); // true
    console.log(isReadonly(obj2)); // true

    return { obj, obj2 } // 暴露出去template可以使用
  }
}
</script>

const 与 readonly 的区别:

  • const 是赋值保护,不能给变量重新赋值。(不可以改变引用,可以改变属性)
  • readonly 是属性保护,不能给属性重新赋值。(不可以改变属性,但是可以改变引用)

可以使用 const 声明一个 readonly,这样就既不能改变引用,也不能改变属性了。

import { readonly } from 'vue';

export default {
  setup() {
    const obj = readonly({ name: 'jack'}) // 不能改变引用,也不能改变属性
    let obj2 = readonly({ name: 'jack'}) // 能改变引用,但不能改变属性
  }
}

简单实现ref

function shallowReactive(obj) {
  return new Proxy(obj, {
    get(obj, key) {
      console.log('get');
      return obj[key]
    },
    set(obj, key, val) {
      console.log('set');
      obj[key] = val;
      return ture;
    }
  })
}

function shallowRef(val) {
  return shallowReactive({value: val});
}

function reactive(obj) {
  if (typeof obj === 'object') {
    if (Array.isArray(obj)) {
      obj.forEach((item, index) => {
        if (typeof item === 'object') {
          obj[index] = reactive(item)
        }
      })
    } else {
      for (const key in obj) {
        const item = obj[key];
        if (typeof item === 'object') {
          obj[key] = reactive(item)
        }
      }
    }

    return new Proxy(obj, {
      get(obj, key) {
        console.log('get');
        return obj[key]
      },
      set(obj, key, val) {
        console.log('set');
        obj[key] = val;
        return ture;
      }
    })
  } else {
    console.warn(`${obj} not an object`);
  }
}

function ref(val) {
  return reactive({value: val});
}

function shallowReadonly(obj) {
  return new Proxy(obj, {
    get(obj, key) {
      console.log('get');
      return obj[key]
    },
    set(obj, key, val) {
      // 禁止修改
      console.warn(`${obj} is readonly`);
    }
  })
}

//  isReactive isRef isReadonly 则是在对应的 Reactive Ref Readonly 中 修改 get
new Proxy(obj, {
  get(obj, key) {
    console.log('get');
    obj[key].__v_isRef = true
    return obj[key]
  },
})
function isRef(val) {
  return val.__v_isRef; // 访问val的时候就会触发get注册__v_isRef
}

isRef({__v_isRef: true}); // Vue3暴露isRef对于这个对象也返回true