第四节:vue3-WatchAPI实现原理

watch的核心就是观测一个响应式数据,当数据变化时通知并执行回调 (那也就是说它本身就是一个effect)

watch 的本质就是effect 内部会对用户填写的数据进行依赖收集

watch监控对象的时候 对象的属性发生变化 是不会触发的因为 对象的引用地址没有发生变化

watch 等价于effect 内部会保存老值和新值 调用方法

使用场景

1、监控对象 可以监控数据变化 数据变化了就重新执行

监控对象无法区分前后的新值和老值

const state= reactive({flag: true, name: 'lyp',address:{num: 6}, age: 30})
// 监测一个响应式值的变化
watch(state,(oldValue,newValue)=>{ 
    console.log(oldValue,newValue)
})
setTimeout(() => {
    state.name ='jdlyp'
    // 也可以触发watch
    // state.address.num='10000'
}, 1000);
复制代码

2、可以去监控一个函数,函数的返回值就是老值 更新后获取新值

不能直接写成 state.address.num

const state= reactive({flag: true, name: 'lyp',address:{num: 6}, age: 30})
watch(()=>state.address.num,(oldValue,newValue)=>{ // 监测一个响应式值的变化
    console.log(oldValue,newValue)
})
setTimeout(() => {
    state.address.num=10000
}, 1000);
复制代码

3、连续触发watch时需要清理之前的watch操作 达到以最后一次返回结果为准的目的

当用户在输入框中输入的时候 我们根据输入的内容返回结果 (ajax等异步)

实现步骤

  • 1)第一次调用watch的时候 传入一个取消的回调
  • 2)第二次调用watch的时候执行上一次传入的回调

onCleanup 是 vue源码提供给用户的钩子

用户传递给onCleanup的函数会由vue源码自动调用

const state= reactive({flag: true, name: 'lyp',address:{num: 6}, age: 30})
let i = 2000;
// 模拟ajax  实现 第一次比第二次返回的晚
function getData(timer){ 
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve(timer)
        }, timer);
    })
}

// 每次数据变化 都会执行watch的回调函数 
// 每次都会形成一个私有作用域 传入的onCleanup函数 执行改变的是上一个私有作用域的clear值
// onCleanup 是 vue源码提供给用户的钩子
watch(()=>state.age,async (newValue,oldValue,onCleanup)=>{
    let clear = false;
    
    // 将 终止的调函数 给到vue源码中的cleanup(也就是传递给下一层)  终止函数的调用会有vue源码自动执行
    onCleanup(()=>{  
        clear = true; 
    })
    i-=1000;
    let r =  await getData(i); // 第一次执行1s后渲染1000, 第二次执行0s后渲染0, 最终应该是0
    if(!clear){document.body.innerHTML = r;} 
},{flush:'sync'}); // {flush:'sync'} 表示同步执行
state.age = 31;
state.age = 32;
复制代码

代码实现

  • 1、如果是响应式对象 循环一遍属性
  • 2、如果是函数则让函数作为fn即可
  • 3、创建effect 监控自己构造的函数 数据变化后重新执行job 然后获取新值
  • 4、运行保存老值 run 的时候 让getter执行 也就是source执行 或者循环source
  • 5、需要立即执行,则立刻执行任务
  • 6、回调时传入onCleanup函数 将钩子暴露给用户
  • 7、保存用户传入的参数为cleanup
export const enum ReactiveFlags {
    IS_REACTIVE = '__v_isReactive' 
}
export function isReactive(value){
    return !!(value && value[ReactiveFlags.IS_REACTIVE])
}
// 遍历的是对象的话  考虑对象中有循环引用的问题
function traverse(value,seen = new Set()){ 
    if(!isObject(value)){ // 不是对象就不再递归了
        return value
    }
    // 如果循环过就不再考虑 直接返回上次的对象就行 解决 object= {a:obj} 的问题
    if(seen.has(value)){ 
        return value;
    }
    seen.add(value);
    for(const k in value){ // 递归访问属性用于依赖收集
        traverse(value[k],seen)
    }
    return value
}


// source 是用户传入的对象   cb就是对应的用户回调  immediate是否立即执行一次回调
export function watch(source,cb,{immediate} = {} as any){
    let getter;
    // 1、如果是响应式对象 循环一遍属性
    if(isReactive(source)){
        // 对用户传入的数据循环一遍来收集effect 只需要循环一遍就好
        //(递归循环,只要循环就会访问对象的每一个属性,在effect中 访问属性的时候 会进行依赖收集)
        // 包装成effect对应的fn, 函数内部进行遍历达到依赖收集的目的
        getter = () => traverse(source)
        console.log(getter)
    }else if(isFunction(source)){
        getter = source // 2、如果是函数则让函数作为fn即可
    }
    let oldValue;
    let cleanup;
    let onCleanup = (fn) =>{  
        cleanup = fn;// 7、保存用户传入的参数为cleanup
    }
    const job = () =>{
        // 值变化时再次运行effect函数,获取新值
        const newValue = effect.run(); 
        // 第一次没有 下次watch执行前调用上次注册的回调
        if(cleanup) cleanup(); 
        // 6、回调时传入onCleanup函数 将钩子暴露给用户
        cb(newValue,oldValue,onCleanup); 
        oldValue = newValue
    }
    // 3、创建effect 监控自己构造的函数 数据变化后重新执行job 然后获取新值
    const effect = new ReactiveEffect(getter,job) 
    if(immediate){ // 5、需要立即执行,则立刻执行任务
        job();
    }
    // 4、运行保存老值 run 的时候 让getter执行 也就是source执行 或者循环source
    oldValue = effect.run(); 
}
复制代码

分步讲解

监测响应式对象

function traverse(value,seen = new Set()){
    if(!isObject(value)){
        return value
    }
    if(seen.has(value)){
        return value;
    }
    seen.add(value);
    for(const k in value){ // 递归访问属性用于依赖收集
        traverse(value[k],seen)
    }
    return value
}
export function isReactive(value){
    return !!(value && value[ReactiveFlags.IS_REACTIVE])
}
export function watch(source,cb){
    let getter;
    if(isReactive(source)){ // 如果是响应式对象
        getter = () => traverse(source)// 包装成effect对应的fn, 函数内部进行遍历达到依赖收集的目的
    }
    let oldValue;
    const job = () =>{
        const newValue = effect.run(); // 值变化时再次运行effect函数,获取新值
        cb(newValue,oldValue);
        oldValue = newValue
    }
    const effect = new ReactiveEffect(getter,job) // 创建effect
    oldValue = effect.run(); // 运行保存老值
}
复制代码

监测函数

export function watch(source,cb){
    let getter;
    if(isReactive(source)){ // 如果是响应式对象
        getter = () => traverse(source)
    }else if(isFunction(source)){
        getter = source // 如果是函数则让函数作为fn即可
    }
    // ...
}
复制代码

watch中回调执行时机

export function watch(source,cb,{immediate} = {} as any){
    const effect = new ReactiveEffect(getter,job) // 创建effect
    if(immediate){ // 需要立即执行,则立刻执行任务
        job();
    }
    oldValue = effect.run(); 
}
复制代码

watch中cleanup实现

连续触发watch时需要清理之前的watch操作

onCleanup 是 vue源码提供给用户的钩子

用户传递给onCleanup的函数会由vue源码自动调用

// 使用
const state = reactive({ flag: true, name: 'lyp', age: 30 })
let i = 2000;
function getData(timer){
    return new Promise((resolve,reject)=>{
        setTimeout(() => {
            resolve(timer)
        }, timer);
    })
}
watch(()=>state.age,async (newValue,oldValue,onCleanup)=>{
    let clear = false;
    onCleanup(()=>{ // 利用钩子函数将 取消的回调传给下一层
        clear = true;
    })
    i-=1000;
    let r =  await getData(i); // 第一次执行1s后渲染1000, 第二次执行0s后渲染0, 最终应该是0
    if(!clear){document.body.innerHTML = r;}
},{flush:'sync'});
state.age = 31;
state.age = 32;
复制代码
// 源码实现
let cleanup;
let onCleanup = (fn) =>{
    cleanup = fn; // 保存用户的终止函数
}
const job = () =>{
    const newValue = effect.run(); 
    if(cleanup) cleanup(); // 第一次没有 下次watch执行前调用上次注册的回调
    cb(newValue,oldValue,onCleanup); // 调用用户回调传入onCleanup函数 暴露钩子
    oldValue = newValue
}
复制代码

猜你喜欢

转载自juejin.im/post/7079322953174745125
今日推荐