Section 4: The principle of vue3-WatchAPI implementation

The core of watch is to observe a responsive data, notify and execute a callback when the data changes (that is to say, it is an effect itself)

The essence of watch is that the effect will rely on the data filled in by the user to collect

When the watch monitoring object changes, the object's properties will not be triggered because the reference address of the object has not changed.

watch is equivalent to the effect, which will save the old value and the new value and call the method

scenes to be used

1. The monitoring object can monitor data changes and re-execute when the data changes.

The monitoring object cannot distinguish between the new value and the old value before and after

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. You can monitor a function. The return value of the function is to get the new value after the old value is updated.

Cannot be written directly as 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. When the watch is triggered continuously, the previous watch operation needs to be cleaned up to achieve the purpose of the last returned result.

When the user enters in the input box, we return the result according to the input content (async such as ajax)

Implementation steps

  • 1) Pass in a cancel callback when calling watch for the first time
  • 2) When the watch is called for the second time, the last callback passed in is executed

onCleanup is a hook provided by the vue source code to the user

The function passed by the user to onCleanup will be automatically called by the vue source code

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;
复制代码

Code

  • 1. If it is a responsive object, loop over the properties
  • 2. If it is a function, let the function be used as fn
  • 3. Create an effect to monitor the changes of the function data constructed by yourself, re-execute the job and then get the new value
  • 4. When running to save the old value run, let the getter execute, that is, source execution or loop source
  • 5. If it needs to be executed immediately, execute the task immediately
  • 6. The onCleanup function is passed in the callback to expose the hook to the user
  • 7. Save the parameters passed in by the user as 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(); 
}
复制代码

step by step

Monitoring reactive objects

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(); // 运行保存老值
}
复制代码

Monitoring function

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

Callback execution timing in watch

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

cleanup implementation in watch

When the watch is triggered continuously, the previous watch operation needs to be cleaned up

onCleanup is a hook provided by the vue source code to the user

The function passed by the user to onCleanup will be automatically called by the vue source code

// 使用
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
}
复制代码

Guess you like

Origin juejin.im/post/7079322953174745125