欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Vue3学习记录(三)

程序员文章站 2022-05-17 19:54:25
...

原理

双向绑定

当修改数据时视图发生改变;在视图发生改变时去修改对应的数据,这就是双向绑定。

  • 发布-订阅: 可以使用各种监听事件,当事件去触发时修改数据;
  • 数据劫持: 访问或修改属性时,会触发相应的事件。

即双向绑定的实现原理就是 发布-订阅 + 数据劫持。

vue2的双向绑定实现

Object.defineProperty(obj, prop, descriptor)

通过Object.defineProperty为对象绑定gettersetter方法。在__render中会读取数据。于是将会触发gettergetter会将当前的watcher订阅到这个数据持有的dep中,完成了依赖收集;而我们修改数据之后会触发setter方法,触发依赖过程中订阅的所有watcherupdate,完成了派发更新。

不是所有的对象的改变都可以触发视图改变的:

var vm = new Vue({
  data:{
    obj: {
        a:1,
    },
    test1:1
  }
})
// vm.test2是非响应的
vm.test2 = 1
// vm.obj.b 是非响应的
vm.obj.b = 2
// vm.obj.b 是响应式的
Vue.$set(this.obj,'b', 2)
  • 对于已经创建的实例,不能动态增加响应式属性,即添加的属性不是响应式的,可以使用$set解决;
  • 对于数组,vue封装了push、pop、shift、unshift、splice、sort、reverse,其中push、unshift、splice会将数据转为响应式数据,所以有些数组的操作并不一定会触发视图更新。

vue3的双向绑定实现

Object.defineProperty只能一个属性一个属性监听,如果对象的属性嵌套的很深,则需要深度遍历;当修改数据的时候,对于这样的数据又得重新遍历,相当的耗性能。

proxy可以监听一整个对象,且只有数据被用到的时候才会去监听它,性能得到了提高。

var proxy = new Proxy(target, handler);

主要API

响应性基础API

reactive 创建响应式对象

<template>
  <div>
    <p>{{obj.count}}</p>
    <button @click="add">加1</button>
  </div>
</template>

<script>
import { reactive } from 'vue';

export default {
  name: 'App',
  setup(){
   const obj = reactive({count: 0});

    function add(){
      obj.count += 1;
    }

   return { obj,add };
  }
}
</script>

  1. 通过reactive传入一个对象,返回该对象的副本,实现响应式。当响应对象发生改变时,原对象的值也会发生改变:
setup(){
   const tmp = {count: 0};
   const obj = reactive(tmp);

    function add(){
      obj.count += 1;
      console.log(obj, tmp)  // Proxy {count: 1} {count: 1}
    }

   return { obj, add };
  }
  1. 之前在vue2中直接根据下标修改数组的值,UI界面不会发生改变,但是在vue3中是可以做到的。
setup(){
    let arr = reactive([1,2,3]);

    function handleClick(){
      arr[1] = 0
    }

    return { arr, handleClick }
  }

readonly创建只读对象

setup(){
   const obj = readonly({ count: 0});
   const obj2 = readonly({ 
     f: {
       count: 0,
       son: {
         count: 0
       }
     }
   });

    function add(){
      obj.count += 1;
      obj2.f.count += 1;
      obj2.f.son.count += 1;
      console.log(obj, obj2);   // 0 0 0
    }

   return { obj, add };
  }

通过readonly传入一个对象,只会对这个对象的属性(含更深层次的属性)只有读的权限,没有修改的权限。

shallowReadonly创建第一层只读对象

readonlyshallowReadonly不一样,前者是所有属性都是只读的,而shallowReadonly只有第一层属性是只读的

setup(){
    const obj = shallowReadonly({ count: 0});
    const obj2 = shallowReadonly({ 
      count: 0,
      f: {
        son: {
          count: 0
        }
      }
    });

      function add(){
        obj.count += 1;
        obj2.f.count += 1;
        obj2.f.son.count += 1;
        console.log(obj.count, obj2.count, obj2.f.son.count);  // 0 0 1  obj2.f.son.count在UI层并没有更新,仍然是0
      }

    return { obj, obj2, add }; 
 }

isProxy检测对象是被代理过

setup(){
   const obj = reactive({ count: 1});
   const obj2 = readonly({ count: 1});
   const a = 1;

    function add(){
      const result = isProxy(obj);
      const result2 = isProxy(obj2);
      const result3 = isProxy(a); 
      console.log(result, result2, result3);   // true true false
    }

   return { obj, add };
  }

isReactive是否是 reactive创建的

setup(){
   const obj = reactive({ count: 1});
   const obj2 = readonly({ count: 1});
   const a = 1;

    function add(){
      const result = isReactive(obj);
      const result2 = isReactive(obj2);
      const result3 = isReactive(a);
      console.log(result, result2, result3);  // true false false
    }

   return { obj, add };
  }

isReadonly是否是只读

setup(){
   const obj = reactive({ count: 1});
   const obj2 = readonly({ count: 1});
   const obj3 = shallowReadonly({ count: 1});

    function add(){
      const result = isReadonly(obj);
      const result2 = isReadonly(obj2);
      const result3 = isReadonly(obj3);
      console.log(result, result2, result3);
    }

   return { obj, add };
}

toRaw返回 reactivereadonlyref代理的原始对象

ref/reactive的数据都又一个特点,就是每次修改都会被追踪,即更新界面,但是有的时候我们希望有些操作不要被追踪即,即不会更新UI界面,那我们可以使用toRawtoRaw可以返回reactive/ref的原始数据,我们可以通过修改原始数据的值进而修改到reactive/ref的值但不会触发UI界面的更新。

setup(){
    const value = {
      count: 0,
      f: {
        son: {
          count: 0
        }
      }
    }
    const obj = reactive(value);
    const obj2 = toRaw(obj);
    console.log(value === obj);      // false
    console.log(value === obj2);     // true
    
    function handleClick(){
        obj2.count = 1;
        console.log(obj);           // obj和obj2的值都发生了改变,但是UI界面不会发生更新
        console.log(obj2);
    }

    return { obj, handleClick }; 
  }

对于ref有点不同,由于ref会将变量包裹一层value,所以使用toRaw的时候应该是:

const obj = ref(value);
const obj2 = toRaw(obj.value);

markRaw

标记一个对象,使其永远不会转换为代理(即不可被追踪)。返回对象本身,一般在编写自己的第三方库时使用。

setup(){
    const value = {
      count: 0,
      f: {
        son: {
          count: 0
        }
      }
    }
    const markRawValue = markRaw(value);
    console.log(isReactive(reactive(markRaw))); // false

    const obj = reactive(value);
    console.log(isReactive(markRaw(obj))); // false
    
    function handleClick(){
      obj.count = 2;
      console.log(obj);                // 值更新,UI不更新
    }

    return { obj, handleClick }; 
  }

shallowReactive不对嵌套对象进行深度响应式(Proxy)转换

在默认情况下,reactive创建的对象都会进行递归监听,即每一层都会包装proxy:

setup(){
    let obj = reactive({
      a: 1,
      gf: {
        b: 2,
        f: {
          c: 3,
          s: {
            d: 4
          }
        }
      }
    });

    function handleClick(){
      console.log(obj);      
      console.log(obj.gf);
      console.log(obj.gf.f);
      console.log(obj.gf.f.s);
    }

    return { obj, handleClick }
  }

Vue3学习记录(三)

如果数据量特别大,或者数据的层级比较深,都会很消耗性能,vue3提供了shallowReactive,将obj改成shallowReactive:

let obj = shallowReactive({
      a: 1,
      gf: {
        b: 2,
        f: {
          c: 3,
          s: {
            d: 4
          }
        }
      }
 });

那么此时打印的内容会发现,只有第一次包裹了proxy

Vue3学习记录(三)

是否包裹了proxy决定了界面是否会更新:

function handleClick(){
      
      // obj.a = 'a';    // 如果改变obj.a即改变了第一层,则UI界面会发生改变
      obj.gf.b = 'b';
      obj.gf.f.c = 'c';
      obj.gf.f.s.d = 'd';

      console.log(obj.a);
      console.log(obj.gf.b);  
      console.log(obj.gf.f.c);
      console.log(obj.gf.f.s.d);
    }

由于第一层数据并没有发生改变,所以值虽然改变了,但是UI视图并不会发生改变:

Vue3学习记录(三)

Refs

ref返回一个响应式且可变的 ref 对象

之前使用reactive的时候我们传入的参数都是对象,如果传入某个普通变量也是可以,但是修改这个变量,UI界面并不会发生改变:

setup(){
    let count = reactive(123);

    function handleClick(){
      count++;
      console.log(count); // 124 但是UI界面并不会更新
    }

    return { count, handleClick }
  }

如果为了实现响应式,每次都把普通变量包装成对象再使用reactive,那并不好用。所以可以使用refref的底层实现就是reactive,每次传入普通变量xxx,就会处理为: {value: xxx}:

setup(){
    let count = ref(123);    // 这里会转为{value: 123}

    function handleClick(){
      count.value++;
      console.log(count.value); // 124 UI界面会更新
    }

    return { count, handleClick }
  }

js中我们读取/修改count需要使用count.value;而在vue template中直接使用count即可。

小结:Vue通过当前数据的__v_ref来判断,如果有这个私有的属性, 并且取值为true, 那么就代表是一个ref类型的数据,则会自动添加.value。另外在也可以给ref传递对象,同样会包裹一层value

在vue2中我们可以通过ref来获得元素,vue3也可以,但是需要改变写法:

<div ref="box">我是div</div>
setup() {
    // console.log(this.$refs.box);        // 在setup中无法读取this
    let box = ref(null); // reactive({value: null})

    onMounted(()=>{
      console.log('onMounted',box.value);
    });

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

    return {box};
}

isRef 判断是否是ref变量

setup(){
    let count = ref(123);
    let obj = reactive({
      name: 'sugarMei'
    });

    function handleClick(){
      console.log(isRef(count));   // true
      console.log(isRef(obj));     // false

      console.log(isReactive(count)); // false
      console.log(isReactive(obj));   // true
    }

    return { count, handleClick }
  }

shallowRef不会使其值成为响应式的

ReactiveRef也是递归监听的:

setup(){
    let count = ref(1);

    function handleClick(){

      console.log(count);     // proxy对象
      console.log(count.value); // 1
    }

    return { count, handleClick }
  }

Vue3学习记录(三)

之前提过ref的底层是reactive,每次会将xx改为value.xx,如果使用shallowRef,则第一层是value,而不是xx,那么怎么修改xx的值将不会引起视图的改变:

setup(){
    let count = shallowRef({
      count: 1
    });

    function handleClick(){
      count.value.count++;     
      console.log(count.value);  // count的值发生了改变,但是没有触发UI视图改变
    }

    return { count, handleClick }
  }

如果遇到shallowRef创建的对象,又很想修改除了value之外的内部值之后触发UI视图改变,可以使用triggerRef

triggerRef手动触发视图更新

function handleClick(){
      count.value.count++;
      triggerRef(count);
}

这样修改count值之后就会触发UI视图的改变了。

toRef创建一个引用对象,且更新之后不会修改UI界面

ref根据变量创建的对象,就是类似JS中的复制:

  • 即如果是普通变量,那么就是简单的复制,ref改变或者变量改变都不会互相影响,且ref的改变会引起视图变化;

    let objTemp = 2
    const obj = ref(objTemp);
    
    objTemp ++ ;             // 改变原始值,原始值会发生改变,但是ref值不会,UI界面不会发生更新
    console.log(obj.value);  // 2
    console.log(objTemp);    // 3
    
    obj.value ++ ;              // 改变ref值,原始值不会发生改变,但是ref值会,UI界面发生更新
    console.log(obj.value);     // 3
    console.log(objTemp);       // 2
    
  • 如果变量是对象,那么就是两者指向了同一个内存地址,两者之间变化一个都会互相影响,但是原始数据引起的数据改变不会更新UI视图,而ref的改变会让UI视图发生改变。

    let objTemp = {
      count: 2
    }
    const obj = ref(objTemp);
    
    objTemp.count ++ ;             // 改变原始值,原始值和ref值会发生改变,但UI界面不会发生更新
    console.log(obj.value.count);  // 3
    console.log(objTemp.count);    // 3
    
    obj.value.count ++ ;              // 改变ref值,原始值和ref值会发生改变,UI界面发生更新
    console.log(obj.value.count);     // 3
    console.log(objTemp.count);       // 3
    

这时候可以使用toRef,它创建处理的数据和以前的有关,但是数据变化不会自动更新界面。

let objTemp = {
  count: 2
}
const obj = toRef(objTemp, 'count');   // => {value: count}
obj.value ++ ;              // 改变ref值,原始值和ref值会发生改变,但是UI界面不发生更新
console.log(obj.value);     // 3
console.log(objTemp.count); // 3

小结: 如果利用toRef将某一个对象中的属性变成响应式的数据,我们修改响应式的数据是会影响到原始数据的,但是如果响应式的数据是通过toRef创建的, 那么修改了数据并不会触发UI界面的更新。

toRef批量创建toRef对象

由于toRef每次只能传入一个对象属性,所以Vue提供了传入对象的toRefs,只是读取每个属性的时候需要使用xxx.value

setup(){
    let objTemp = {
      count: 2
    }
    const obj = toRefs(objTemp);   
    
    function handleClick(){
      obj.count.value ++ ;              // 改变ref值,原始值和ref值会发生改变,但是UI界面不发生更新
      console.log(obj.count.value);     // 3
      console.log(objTemp.count); // 3
    }

    return { obj, handleClick };
  }

customRef创建一个自定义的 ref

该函数接收 tracktrigger 函数作为参数,并应返回一个带有 getset 的对象。

import { customRef } from 'vue';

function myRef(value){
  return customRef((track, trigger) => {
    return {
      get(){
        track();                    // 告诉vue要追踪这个变量,如果不加,那么修改完之后,UI界面不会显示
        console.log('get', value);
        return value;
      },

      set(newVal){
        console.log('set', newVal);
        value = newVal;
        trigger();                  // 主动触发ui视图更新
      }
    }
  })
}

export default {
  name: 'App',
  setup(){
    let state = myRef(18);     // 使用了自定义ref
  
    function handleClick(){
      state.value += 1;
    }
    return { state, handleClick };
  }
}
</script>
get 18           # 界面读取的时候
get 18           # set之后读取了值
set 19           # set修改值,触发视图更新
get 18           # 界面更新,读取值

应用场景: 如果我们在setup中发起一个异步请求并更新数据时可以的,但是如果想使用async/await将异步请求变为同步请求是行不通的,UI 界面渲染不出来,但是如果将所有异步请求都写在setup中并不舒服,可以这样:

 function myRef(value){
  return customRef(async (track, trigger) => {
    await fetch(value)
    .then(res => {
      return res.json();
    })
    .then(data => {
      console.log(data);
      value = data;
      trigger();                 // 记得更新界面
    })
    .catch(err => {
      console.log(err);
    })
    return {
      get(){
        track();                    // 告诉vue要追踪这个变量,如果不加,那么修改完之后,UI界面不会显示
        console.log('get', value);
        return value;
      },

      set(newVal){
        console.log('set', newVal);
        value = newVal;
        trigger();                  // 主动触发ui视图更新
      }
    }
  })
}

export default {
  name: 'App',
  setup(){
    let state = myRef('../public/data.json');     // 使用了自定义ref
  
    function handleClick(){
      state.value += 1;
    }
    return { state, handleClick };
  }
}

unRef则返回内部值

如果参数为ref,则返回内部值,否则返回参数本身。即 val = isRef(val) ? val.value : val

ComputedWatch

conputed

setup(){
    const count = ref(1);
    const plusOne = computed({
      get: () => count.value + 1,
      set: val => {
        count.value = val + 1
      }
    })
  
    function handleClick(){
      plusOne.value = 1;
      console.log(count.value);      // 2
      console.log(plusOne.value);    // 3
    }
    return { count, handleClick };
  }

watchEffect

类似watch,但是是在setup中使用

setup(){
    const count = ref(0);

    watchEffect(() => {
      console.log(count.value);     // 先打印0 后打印1
    })

    function handleClick(){
      count.value = 1;
    }
    return { count, handleClick };
}

Watch

watchEffect比,有三个特点:

  • 惰性地执行副作用,即当值发生改变的时候才会执行watchEffect在初始化的时候也会打印);
  • 可以具体地说明触发监听器重新运行的状态,即可以指定是那个值改变就触发;
  • 访问侦听状态的先前值和当前值。
// 侦听一个getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

// 监听多个源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

watchEffect 共享行为

这个暂时没研究出来,晚点看完所有文档,可能可以连起来,到时候在补充~

相关标签: vue3 vue