vue百宝箱

146 阅读7分钟

image.png

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个场景

image.png

这个情况下watcheffect第一次执行的时候, clicked 由于为false,导致第一次其实是没有访问到这个obj.value的,所以导致依赖没有被收集到,导致后续更新的时候,由于依赖项数组里没有收集到这个更新函数的逻辑,导致后续更新不会触发视图的更新。

image.png

这种情况就是前面是一个异步,由于后面的依赖收集,需要等到await 执行完才会被执行到,此时访问obj.value会被放到微任务队列里,等到watchEffect传入的函数执行完,才会执行这个访问语句,但是此时 watcheffect里的依赖收集函数已经被重置为null了。 等从微任务队列里面拿出这段代码,去访问属性手机依赖的时候,此时effect 已经是null,就不会被收集到了。

image.png

【总结就是类似于这种操作,用于收集依赖的对于属性的访问,应当放到异步函数之前或者在有标字符判断之前执行!!!】

关于异步更新

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类就无法命中,去掉就可以命中。

image.png image.png

排查思路: 首先第一感覺就是這個樣式沒有命中這個正文元素, 去f12 排查 ,發現元素缺少了一格scopeID,手動編輯元素,加上这个09512f3f 这个样式就有了

image.png

然后进一步排查: 确定了应该是vue 底层的 diff算法 虚拟节点vnode 复用问题, 应该是加上了:a=1属性的时候,在这个v-if v-else 切换的时候,正文的span标签的父级元素的vnode,直接复用隐藏起来的loading的这个div。 后面测试,直接在 empty 这个div上加一个:key=“any str” 就发现样式命中了。那么到此排查完毕,确实是因为虚拟节点复用的问题。 image.png

为什么? 原因在哪里? 要清楚原理就得需要从源码入手 首先确定vue2的版本,这里是2.6.14. 然后就是找到虚拟节点更新的逻辑代码路径:这里是在是src/core/vdom/patch.js , 更新的函数在patch() 这个函数中。

然后随便在代码中找一句代码,认为是整个源码中唯一的代码或者注释之类的。 去到浏览器,f12 控制台/ network 下面搜索(ctrl+f [windows系统]) 搜索,这里找的一句注释

image.png 然后搜索,找到浏览器加载的源代码在哪个位置后,就可以打断点调试了。

这里直接说结果: 是因为在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的,那么样式就不会命中!!!】

image.png 解决办法: 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的代理监听,好处在于效率的提升,还有对于新增属性一就可以实现监听