深入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学习的主要内容,如果未能解决你的问题,请参考以下文章