effect 和 effectScope
使用场景: 点击按钮实现数字加一以及double操作, 以及点击终止按钮,取消下面watch,watchEffect,effect的监听操作
import {effect, effectScope, watch ,watchEffect, computed} from 'vue'
const counter = ref(0)
const cleanUp = [] // 用于存副作用函数的数组
const stopWatch1 = watchEffect(() => {counterEl.textContent = counter.value})
【watch ,watchEffect 会返回一个终止的函数,用于去取消监听!!!】
cosnt stopWatch2 = watch(counter,() => {console.log('监听了counter的变化'})
//重名了,重命名为ef,effect 返回一个对象,对象里面有个effect,解构出来,有个effect.stop函数
const {effect:ef} = effect(() => {doubleEl.textcontent = counter.value * 2})
cleanUp.push(stopWatch1,stopWatch2, ef.stop.bind(ef))
incrementBtn.addEventListener('click', () => {counter.value++})
decrementBtn.addEventListener('click', () => {counter.value--})
stopBtn.addEventListener('click',() => {cleanUp.forEach((cp) => {cp()})})
// 到这里是不借助于effectScope这个工厂函数实现的取消监听的效果。
// 借助于effectScope, 主要是用于收集作用域里面用到的副作用函数,做一个统一的处理,这个工厂函数返回一个对象, 在run方法的会掉里面,直接使用 这三个监听方法,run函数会自动收集这些副作用!!!
const scope = effectScope();
scope.run(() => {
watchEffect(() => {counterEl.textContent = counter.value})
watch(counter,() => {console.log('监听了counter的变化'})
watch(threetimes,() => {console.log('监听了counter的变化'})
effect(() => {doubleEl.textcontent = counter.value * 2})
//这里面computed 也是可以用的
const threetimes = computed(() => {counter.value * 3})
const _scope = effectScope(true/false) // 这里有一个参数,表示是否自动绑定上下级的关系,为true则表示不绑定,最外层的scope stop,不会影响内部的_scope, 不传或者为false就表示一起跟随着符级停止!!!
// 里面有嵌套的effectScope 也是可以统一处理的!!!
})
然后再点击stopBtn的时候,调用scope.stop() 方法,会自动去将在run 里面收集到的副作用给停止!!!
stopBtn.addEventListener('click',() => {scope.stop()})
//这里stop的逻辑其实就是拿到一个个的副作用函数,依次执行 effect.stop()!!!
watchEffect 的2个场景
这个情况下watcheffect第一次执行的时候, clicked 由于为false,导致第一次其实是没有访问到这个obj.value的,所以导致依赖没有被收集到,导致后续更新的时候,由于依赖项数组里没有收集到这个更新函数的逻辑,导致后续更新不会触发视图的更新。
这种情况就是前面是一个异步,由于后面的依赖收集,需要等到await 执行完才会被执行到,此时访问obj.value会被放到微任务队列里,等到watchEffect传入的函数执行完,才会执行这个访问语句,但是此时 watcheffect里的依赖收集函数已经被重置为null了。 等从微任务队列里面拿出这段代码,去访问属性手机依赖的时候,此时effect 已经是null,就不会被收集到了。
【总结就是类似于这种操作,用于收集依赖的对于属性的访问,应当放到异步函数之前或者在有标字符判断之前执行!!!】
关于异步更新
Vue的视图更新是异步的。 props的更新也是异步的,因为props的更新是在render函数中生成的的副作用!!!
所以一般在父组件更新props, 调用子组件的方法获取最近props的时候,需要在nextTick中去获取,或者在子组件中监听props的变化,监听到更新了在进行下一步操作!!!
关于动态绑定样式: 加入div是v-for动态渲染的,并且搭配使用了tailwindcss,就可以使用如下方法:
<div v-for="(a, index) in arr" :key="index" class="flex flex-col flex-wrap" :class="`h-1/arr.length`">{{a.xxx}}</div>
这样就是相当于将div的高度动态设定为 容器的 n分之一,
Vue2 加上属性,导致样式丢失的bug
场景:就是有一个组件,里面的内容是默认显示loading,然后定时器一秒后,加载正文。
代入如图:CommonLayout 只要里面的加上a:1 这个属性,那么这个正文内容 empty 这个class类就无法命中,去掉就可以命中。
排查思路: 首先第一感覺就是這個樣式沒有命中這個正文元素, 去f12 排查 ,發現元素缺少了一格scopeID,手動編輯元素,加上这个09512f3f 这个样式就有了
然后进一步排查: 确定了应该是vue 底层的 diff算法 虚拟节点vnode 复用问题, 应该是加上了:a=1属性的时候,在这个v-if v-else 切换的时候,正文的span标签的父级元素的vnode,直接复用隐藏起来的loading的这个div。 后面测试,直接在 empty 这个div上加一个:key=“any str” 就发现样式命中了。那么到此排查完毕,确实是因为虚拟节点复用的问题。
为什么? 原因在哪里? 要清楚原理就得需要从源码入手 首先确定vue2的版本,这里是2.6.14. 然后就是找到虚拟节点更新的逻辑代码路径:这里是在是src/core/vdom/patch.js , 更新的函数在patch() 这个函数中。
然后随便在代码中找一句代码,认为是整个源码中唯一的代码或者注释之类的。 去到浏览器,f12 控制台/ network 下面搜索(ctrl+f [windows系统]) 搜索,这里找的一句注释
然后搜索,找到浏览器加载的源代码在哪个位置后,就可以打断点调试了。
这里直接说结果: 是因为在patchVnode 这函数里面,有一个逻辑是判断新旧vnode是否是同一个节点, 加了:a=1的这种情况 在patchVode 里面的有个逻辑就是在比较是否是同一个vnode的时候,这个sameVode返回的true,如果去掉这个:a=1的属性,那么走到这个逻辑,sameVnode就返回false,之后单步跳过,发现会走到一个createEl的方法,新建一个node,并且这个方法里面有个setScope(Vnode)方法,可以加上scoped,这就是为什么去掉之后样式命中的原因。sameVnode就返回false,导致正文span的父级的div是复用了组件里的那个template下的div,由于组件是没有写scoped ,所以就导致正文的div没有scopeId,导致无法命中样式。 【总结就是加上:a=1属性,或者任意属性后,页面和组件之间产生了虚拟节点的共用,导致切换后的div没有scoped,如果页面的样式是基于scoepd的,那么样式就不会命中!!!】
解决办法: 1. 在组件div那里加上:key 2. 手动在更新Vnode的时候手动调用这个setScope(Vnode)这个方法。
vue3 proxy proxy(obj,{
get(){}
set(){}
})
到底比
vue2 defineProperty(obj,'property', {
get(){}
set(){}
})
vue2 中 的监听都是对于具体某一个属性的监听, 所以对于vue2 中源码有一个 observe(){} 就用于递归处理一个待监听对象的所有属性,去深度遍历这个对象的属性。 这个过程叫做观察
vue2 响应式做法
function isObject(target) {
return type of target === 'object' && target !== null
} // 判断传入的类型是否是对象
function observe(obj) {
for(let k in obj) {
let v = obj[k]
if(isObject(v)) {
observe(v)
}
defineProperty(obj,k, {
get(){
return v
}
set(val){
if(val !== v) {
v = val
}
}
})
}
}
这种做法缺陷:
1. 需要深入遍历对象里面的所有属性,这造成了效率的损失
2.在观察observer(obj) 时,当下的obj里面所有的属性都是可以被观察到(created 钩子之前),但是后面对obj新增的属性都无法被观察到的。这个就是为什么在vue2中 如果对data里面的对象,动态新增一个属性后,这个新增属性的值得变化不会触发视图更新(mounted 后),就是这么个原理 -
vue3 中是对整个对象进项监听观察
const proxy = new Proxy(obj, {
get(target, key) {
let val = target[key]
cnosole.log('读取属性',k)
return val
}
set(target,key. val) {
if(target[key] !== val) {
target[key] = val
console.log(k,'更改')
}
}
})
对比vue2 vue3的代理监听,好处在于效率的提升,还有对于新增属性一就可以实现监听