从零手写Vue3 中异步更新策略

一.watchAPI

watchAPI 的核心就是监控值的变化,值发生变化后调用对应的回调函数

1.同步watch

const state = reactive({ name: 'zf' })
watch(() => state.name, (newValue, oldValue) => {
    console.log(newValue, oldValue)
}, { flush: 'sync' }); // 同步watcher
setTimeout(() => {
    state.name = 'jw'
    state.name = 'zf'
}, 1000);
1
2
3
4
5
6
7
8

watchAPI根据传入的参数不同,有不同的调用方式

export function watch(source, cb, options:any = {}) {
    dowatch(source, cb, options)
}
function dowatch(source, cb, {flush,immediate}) {
    let getter = () => source.call(currentInstance); // 保证函数中的this 是当前实例
    let oldValue;
    const job = () => {
        if(cb){
            const newValue = runner(); // 获取新值
            if(hasChanged(newValue,oldValue)){ // 如果有变化,调用对应的callback
                cb(newValue,oldValue);
                oldValue = newValue; // 更新值
            }
        }
    }
    let scheduler;
    if(flush == 'sync') {
        scheduler = job
    }else if(flush === 'post'){

    }else {
        // flush === 'pre'
    }
    const runner = effect(getter, {
        lazy: true,
        scheduler
    })
    if(cb){
        if(immediate){
            job(); // 立即让cb执行
        }else{
            oldValue = runner(); // 仅执行不调用 cb
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

2.异步watch

多次进行更改操作,最终仅仅执行一次

const state = reactive({ name: 'zf' })
watch(() => state.name, (newValue, oldValue) => {
    console.log(newValue, oldValue); // xxx zf
});
setTimeout(() => {
    state.name = 'jw'
    state.name = 'xxx'
}, 1000);
1
2
3
4
5
6
7
8

根据参数不同,将任务放到不同的队列中

let pendingPreFlushCbs = [] // preCallback
let pendingPostFlushCbs = [] // postCallback

export function queuePreFlushCb(cb) {
    queueCb(cb, pendingPreFlushCbs)
}
export function queuePostFlushCb(cb) {
    queueCb(cb, pendingPostFlushCbs)
}

function queueCb(cb, pendingQueue) {
    pendingQueue.push(cb);
    queueFlush();
}
let isFlushPending = false
function queueFlush() {
    if (!isFlushPending) { //保证queueFlush方法只能调用一次
        isFlushPending = true;
        Promise.resolve().then(flushJobs)
    }
}
function flushJobs() {
    isFlushPending = false;
    flushPreFlushCbs(); // 刷新队列
    flushPostFlushCbs();
}
function flushPreFlushCbs() {
    if(pendingPreFlushCbs.length) {
        const deduped: any = [...new Set(pendingPreFlushCbs)];
        pendingPreFlushCbs.length = 0;
        console.log(deduped)
        for (let i = 0; i < deduped.length; i++) {
            deduped[i]();
        }
        flushPreFlushCbs(); // 递归直到用尽
    }
}
function flushPostFlushCbs() {
    if(pendingPostFlushCbs.length) {
        const deduped: any = [...new Set(pendingPostFlushCbs)];
        pendingPostFlushCbs.length = 0;
        for (let i = 0; i < deduped.length; i++) {
            deduped[i]();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

二.watchEffect

watchEffect是没有cb的watch,当数据变化后会重新执行source函数

const state = reactive({ name: 'zf' })
watchEffect(()=>console.log(state.name));
state.name = 'jw'
1
2
3

watchEffect实现

export function watchEffect(source,options){
    dowatch(source, null, options)
}
1
2
3
function dowatch(source, cb, {flush,immediate}) {
    const job = () => {
        if(cb){
            // ....
        }else{ // watchEffect 不需要新旧对比
            runner()
        }
    }
    if(cb){
       // ...
    }else{ // watchEffect 默认会执行一次
        runner();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

三.组件异步更新原理

const App = {
    setup() {
        const state = reactive({ name: 'zf' });
        setTimeout(() => { // 多次更新状态
            state.name = 'jw';
            state.name = 'zf';
            state.name = 'zry';
        }, 1000);
        return {
            state
        }
    },
    render: (r) => {
        console.log('render~~~'); // 造成多次渲染
        return h('div', r.state.name)
    }
}
createApp(App).mount('#app');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

给组件更新提供scheduler函数

const setupRenderEffect = (instance, initialVNode, container) => {
    instance.update = effect(function componentEffect() {
        // ....
    }, {
        scheduler: queueJob
    })
}
1
2
3
4
5
6
7

queueJob的原理也是将渲染函数维护到队列中

let queue: any = []
export function queueJob(job) {
    if (!queue.includes(job)) {
        queue.push(job);
        queueFlush();
    }
}
1
2
3
4
5
6
7
function flushJobs() {
    isFlushPending = false;
    flushPreFlushCbs(); // 渲染之前

    queue.sort((a, b) => a.id - b.id);
    for (let i = 0; i < queue.length; i++) {
        const job = queue[i]; // 渲染
        job();
    }
    queue.length = 0;

    flushPostFlushCbs(); // 渲染之后
}
1
2
3
4
5
6
7
8
9
10
11
12
13