深入vue3学习

Posted lin_fightin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入vue3学习相关的知识,希望对你有一定的参考价值。

setup

setup有两个参数

 props: {
    data: String,
  },
 setup(props, context) {
    console.log(props);
  },

第一个参数是props,顾名思义就是获取外界传进的值,因为setup里面不能用this,因为setup的调用是在组件创建之前的,这时候组件实例还没创建生成,故不存在this。
而第二个则是context参数,有三个属性

attrs: 所有非props的attribute,比如父亲调用子组件时传入的属性而子组件的props未定义。

slots: 父组件传递过来的插槽,用作渲染函数返回有作用,后续再讲。

emit 当组件内部需要发出事件如this.$emit,因没this,故用这个代替

返回值

setup的返回值可以直接在template中使用。
比如

setup() {
    //i18n hooks
    //store hooks
    const store = useStore();

    //router hooks
    const roter = useRouter();

    const root = toRef(store.state, "root");
    const changeRoot = () => {
      store.dispatch("setRoot", "改变了");
    };
    const { about } = {
      ...mapState({ about: (state) => (state as any).about }),
    };

    const change = (type: string) => {
      window.localStorage.setItem("i18nKey", type);
      roter.go(0);
    };

    console.log("about", about.call({ $store: store }));

    return { root, changeRoot, change };
  },

这些root changeRoot change,方法,变量都可以在template中使用。跟vue2的data和methods一样。

那如果定义了data又定义了setup呢?那肯定用setup,毕竟setup是vue3的灵魂,源码做了判断,优先从setup里面获取。

setup不能使用this。

官方描述:setup不会找到组件实例,(但其实是组件实例创建出来的了)且在调用的时候,data,computed,methods等都没有被解析。
原码:

  const instance: ComponentInternalInstance =
      compatMountInstance ||
      (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ))

先创建了
然后

 setupComponent(instance)

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}

再调用并把Instace传进去,所以说setup不能用this是因为组件还没创建这个点应该是误解的。
这个就是执行setup的源码,


    //执行setup,并且获取结果
    //内部实现就是执行setup,并把instacn.props,setupContext作为参数给setup
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    )
export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

可以看到fn在执行的时候压根就没有call apply去绑定this。

深入响应式Api ref reactive 学习

我们正常使用的data的变量是有响应性的,而我们自己在setup定义的变量却没有响应性,因为data里面的对象会被reactive()函数所包裹,劫持处理。原理是Es6的Proxy代理vue2是通过Obejct.definedproty来劫持。

所以我们也用reactive函数包裹我们定义的变量

import { defineComponent, reactive } from "vue";

export default defineComponent({
  name: "AA",
  components: {},
  props: {
    data: String,
  },

  setup(props, context) {

      const state = reactive({
          counter: 100
      })
    
  },
});

这样state就拥有了响应性。

ref使用

ref与reactive的区别就是一个是基本数据类型,一个是Object。所以reactive是不能接受一个基本数据类型滴,会报错。

const counter = ref(0)

这样我们的counter就是一个响应式对象了,为什么说是对象,因为ref返回的是一个可变的响应式对象,它内部的值counter,是在ref的value属性中被维护的,也就是说我们要在setup里面用的话应该

  console.log(counter.value);

加上value属性使用。
但是在template却又不用value

  <h2>{{counter}}</h2>

因为外部的template使用ref对象时,实际上帮助我们做了.value处理啦(自动进行解包(浅层解包,就是外层若包裹了一个普通对象则不会解包,只有包裹了reactive对象才会继续解包)),看似是counter,内部是counter.value。

readonly的认识

reactvie和ref获取一个响应式对象,加上readonly会返回原生对象的只读代理,原理就是劫持这些对象的proxy代理的set方法,一旦修改就做对应处理,让其不能修改。

   const readonlyCounter = readonly<Ref<number>>(counter)

一些简单的api

isProxy

检查对象是否是由reactive或者readonly创建的proxy。

isReactive

检查对象是否由reactive创建的。但如果是readonlu创建的,但是包裹的是reactive创建的,则会返回true。

isReadonly

检查是否由readonly创建的制度代理。

toRow

返回reactive或者readonly代理的原始对象。(谨慎使用)

shallowReactive

创建一个响应式代理,只跟踪其自身property的响应性。深层的不会响应式。

shallowReadonly

创建一个只使其自身的property为只读,子属性的子属性还是可读可写的。

toRefs

我们知道,当reactive包裹的对象返回的时候若是解构,如


      const { counter } = reactive({
          counter: 100
      })
      

这样counter就是去了响应式。

const state = reactive({
          counter: 100
      })
let  { counter } = toRefs(state)

但是通过toRefs来处理后,内部就会创建一个ref(counter),变成响应式的。
官网是这样回答的:

Vue提供了一个toRefs的函数,可以将reactvie返回的对象中的属性转成ref。而这种做法相当于已经在state.xxx与xxx之间建立一个连接,任何一个修改都会引起另外一个变化。

就是说counter修改,会影响state.counter改变,state.counter修改会引起counter改变。

toRef

toRef是对一个对象中的一个属性转成ref。

跟toRefs的区别就是 toRefs会将对象中的所有属性都变成ref,建立连接。返回一个对象

而toRef可以对其中一个属性转成ref,建立连接。返回一个由ref转化的对象

  const state = reactive({
          counter: 100
      })
      let   counter  = toRef(state, 'counter')

Ref其他的api

unRef

判断这个值是不是ref包裹的,是的话返回value,不是的话返回本身。
相当于: val = isRef(val) ? val.vlaue : value的语法糖。

isRef

判断是否是ref镀锡

shallowRef

创建一个浅层ref对象

triggerRef

手动触发与shallowRef的副作用。triggerRef会触发shallowRef的副作用,比如改变深层的值不会响应式,调用triggerRef(值)就可以产生一次副作用,改变ui的值。

customRef(了解)

创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显示控制。就是自己定义一个类似于ref的,然后规则自己订。

他需要一个工厂函数,接受track和trigger函数尾参数,并且返回一个带有get和set的对象。

import { customRef } from "vue";


//自定义ref
export default function(value: any) {
    //track收集依赖, trigger触发副作用
  return customRef((track, trigger) => {
    return {
      //返回get set给proxy代理
      get() {
        //收集依赖 需要用到改值得时候就表明有依赖,收集
        track(); 
        return value

      },
      set(newValue) {
        setTimeout(()=>{
          value = newValue
          trigger()
        }, 1000)
        //值改变后,触发副作用
      },
    };
  });
}

这样我们的自定义ref就好了,每次赋值的时候i都会慢一秒再赋值。

   const a = customRef(1)

使用跟ref一样。

computed

用法1,传入getter函数

  const aa = computed(()=>a.value*2)

返回的aa而是ref包裹的对象。
用法2 传入对象

 const aa = computed({
      get:()=>a.value*2,
      set(newvalue){
        //aa.value = newvalue  错误做法
        a.value = newvalue //改变源数据才是正确做法,由他再去引起aa得变化
      }
    })

watch & watchEffect

watchEffect自动收集响应式数据得依赖
watch需要手动侦听数据源

使用

watchEffect

  watchEffect(()=>{
      console.log(a.value, 'a改变');
    })
export declare function watchEffect(effect: WatchEffect, options?: WatchOptionsBase): WatchStopHandle;
export declare type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void;

通过watchEffect得ts定义可以看出,回调函数没n和o这些新值旧值,只有一个onInvalidate回调函数。而watchEffect会在一开始立即执行一次,关键也是这一次执行,在第一次执行的时候,他会收集函数里面的可响应式得对象,比如a,收集到依赖去,然后一改变就会执行回调函数。所以第一次执行就是在检查了(为的是收集依赖)

停止侦听器

通过onInvalidate,

const stop1 = watchEffect((aa)=>{
      console.log(a.value, 'a改变');
       aa(()=>{
        //在这里清楚副作用
      })
    })

参数aa可以执行,里面的cb就可以清除副作用。

export declare function watchEffect(effect: WatchEffect, options?: WatchOptionsBase): WatchStopHandle;

可以看到watchEffect是返回一个停止得函数。他的调用是在修改之前被调用,就是比watchEffect得其他代码执行的早。取消该次得副作用

const stop1 = watchEffect(()=>{
      console.log(a.value, 'a改变');
    })
    setTimeout(()=>{
      console.log('2s后停止');
      
      stop1()
    },2000)

只要调用即可停止。

停止副作用

比如在watchEffect发送网络请求想要取消,只需要

第一次执行等dom挂在完后再执行只需要

watchEffect(
      () => {
        console.log(title.value);
      },
      {
        //等dom更新完再执行watcheffect
        flush: "post",
      }
    );

传入第二个参数。flush设为post即可。默认是pre。还有async(变成同步,很低效)

watch

watch完全等同于vue2得watch。
特点:第一次不会执行,更具体的说明哪个状态发生变化才能触发侦听器执行,可以访问状态变化前后的值。
监听一个reactive对象

 const state = reactive({count: 100, value: 300})

    watch(state,(n,o)=>{
      console.log(n,o);
      
    })
export declare type WatchCallback<V = any, OV = any> = (value: V, oldValue: OV, onInvalidate: InvalidateCbRegistrator) => any;

从watch中的回调函数定义可以看到,有新值以及旧值两个参数。停止跟watchEffect一样。
监听一个reactvie对象的属性

  watch(()=>state.count,(n,o)=>{
      console.log(n,o);
      
    })

vue3不支持“state.count"这样去监听了,因为第一个参数必须是对象。

源码

 return doWatch(source as any, cb, options)
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
):
if (isRef(source)) {
    getter = () => source.value
    forceTrigger = !!source._shallow
  } else if (isReactive(source)) {
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(isReactive)
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
  } else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  } 

可以看到watch接受三个参数,一个source,一个cb,一个options,然后真正实行是doWatch函数,他会通过isRef,isReactvie,isArray等的判断判断你传得值是否符合规范。
我们注意到

reactvie对象监听的值,依然是proxy的,因为源码就是直接()=>source返回了,而ref对象则是返回其value值,()=>source.value。

希望reactvie拿到一个普通的对象。

需要这么做:

  watch(
      () => {
        return { ...state };
      },
      (n, o) => {
        console.log(n, o);
      }
    );

    return { title, state };

第一个参数传入一个函数并且返回一个解构的对象。

可以看下源码:

else if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } 

当我们传入的source是函数的时候,

export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

该函数直接返回了fn的调用。则调用了

 () => {
        return { ...state };
      },

并且返回。

可以看到还有一个判断就是isArray(source)、

else if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(isReactive)
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
  } 

也就是说我们还可以传入一个数组,表示可以监听多个属性。

 watch([state, title], (n, o) => {
      console.log(n, o);
    });

这样数组中任意一个值改变都会触发watch,参数则为数组,对应改变的值。

深度监听

 watch([state, title], (n, o) => {
      console.log(n, o);
    }, {
      deep: true,
      immediate: true
    });

watch可以传入第三个参数option,配置对象,deep表示进行深监听,immediate表示初始化就执行一次。
而reactvie默认是会深度监听的(往上看源码,有deep=true),而解构出来的话就失去了深度监听,所以就需要第三个参数deep:true了。

ref(不是创建响应对象那个ref哦,而是拿到dom元素的ref)

还是一样用ref,

    const title = ref(null);
    onMounted(() => {
      console.log("title", title.value);
    });
    return { title };

然后这个title绑定在dom上就行

  <div ref=

以上是关于深入vue3学习的主要内容,如果未能解决你的问题,请参考以下文章

深入 Vue3 源码,学习初始化流程

深入 Vue3 源码,学习初始化流程

深入 Vue3 源码,学习初始化流程

深入 Vue3 源码,学习初始化流程

深入 Vue3 源码,学习响应式原理

深入vue3学习