【vue3系列】4. 通过实例轻松掌握vue3.0新增的effect、依赖收集和触发视图更新

687 阅读3分钟

上篇文章为了简化整体流程,视图更新直接调用的实例app的update方法,这篇文章增加依赖收集和触发视图更新过程将实例app进行解耦。 vue3.0响应式新增了副作用effect函数,结合依赖收集和视图更新完成数据响应式。

1. 副作用函数

所谓副作用函数其实就是一个函数包裹器,他用来收集要更新视图的函数,当调用effect时,会自动执行更新函数然后立即释放。代码如下:

   // 副作用函数存储栈
   const effectStatck = [];
   // 副作用函数
   function effect(fn) {
       const eff = () => {
           try {
               // 添加待执行的函数
               effectStatck.push(fn); 
               // 执行函数
               fn();
           } finally {
               // 执行完释放
               effectStatck.pop();
           }
       }
       // 先执行一次
       eff();
       return eff;
   }

基于上篇文章的代码改动如下:

82c1a63354ae7d47ae39e1312f210c8b.png

2. 依赖收集

依赖收集就是利用proxy的get方法将对象的属性与副作用函数通过某种关系进行绑定,保证属性数据变化时执行对应的更新函数。

   // 依赖收集
   const targetMap = {};
   function track(target, key) {
       // 获取副作用函数
       const effect = effectStatck[effectStatck.length - 1];
       if (effect) {
           let map = targetMap[target];
           if (!map) {
               map = targetMap[target] = {};
           }
           let deps = map[key];
           if (!deps) {
               deps = map[key] = [];
           }
           if (deps.indexOf(effect) === -1) {
               deps.push(effect);
           }
       }
   }

基于上篇文章的代码改动如下: dfded5d5ee251d69190490d4070c34eb.png

注:如果对象的属性类型依然是对象,要继续返回一个proxy,否则不能深度监听

3. 触发更新

当属性的数据发生变化时,会触发proxy的set执行trigger,trigger拿出依赖收集对应的副作用函数遍历执行

// 触发副作用
function trigger(target, key) {
    const map = targetMap[target];
    console.log(map, targetMap);
    if (map) {
        const deps = map[key];
        (deps || []).forEach(dep => {
            dep()
        });
    }
}

基于上篇文章的代码改动如下:

d0e0270b92142d5f73de6ea9ae279e76.png

使用Reflect为了保证更好的兼容性,一般它和proxy配对使用。

4. 实例验证

<html>
    <body>
        <div id="app">
        </div>
        <script>
            // 副作用函数
            const effectStatck = [];
            function effect(fn) {
                const eff = () => {
                    try {
                        effectStatck.push(fn); 
                        fn();
                    } finally {
                        effectStatck.pop();
                    }
                }
                // 先执行一次
                eff();
                return eff;
            }
            // 依赖收集
            const targetMap = {};
            function track(target, key) {
                // 获取副作用函数
                const effect = effectStatck[effectStatck.length - 1];
                if (effect) {
                    let map = targetMap[target];
                    if (!map) {
                        map = targetMap[target] = {};
                    }
                    let deps = map[key];
                    if (!deps) {
                        deps = map[key] = [];
                    }
                    if (deps.indexOf(effect) === -1) {
                        deps.push(effect);
                    }
                }
            }
            // 触发副作用
            function trigger(target, key) {
                const map = targetMap[target];
                console.log(map, targetMap);
                if (map) {
                    const deps = map[key];
                    (deps || []).forEach(dep => {
                        dep()
                    });
                }
            }
            const Vue = {
                // 数据响应
                reactive(data) {
                    let handler = {
                        // 拦截对象的属性读取
                        get(target, key) {
                            // 收集依赖
                            if (typeof target[key] === 'object' && typeof target[key] !== null) {
                                // 如果是对象,继续返回proxy,否则不能深度监听
                                return new Proxy(target[key], handler);
                            }
                            track(target, key);
                            return Reflect.get(target, key)
                        
                        },
                        // 拦截对象的属性新增、修改
                        set(target, key, val) {
                            console.log('set');
                            Reflect.set(target, key, val)
                            // 检测到变化时通知更新,为了简化先直接修改
                            // app.update();
                            trigger(target, key);
                        },
                        // 拦截对象的属性删除
                        deleteProperty(target, key) {
                            delete target[key];
                            trigger(target, key);
                        }
                    }
                    return new Proxy(data, handler) 
                },
                // 创建应用程序实例
                createApp(options) {
                    return {
                        mount(selector) {
                            const parent = document.querySelector(selector);
                            if (!options.render) {
                                // 如果没有配置渲染函数,使用自定义的编译函数得到渲染函数
                                options.render = this.compile(parent.innerHTML);
                            }
                            // 
                            this.setupData = options.setup();
                            // 给app实例增加一个更新方法,为了更好利用parent,通过变量声明函数
                            this.update = effect(() => {
                                // 通过call执行函数,并将setup的返回值作为this
                                const el = options.render.call(this.setupData);
                                // 清空宿主元素的内容
                                parent.innerHTML = '';
                                // 将el追加到宿主元素
                                parent.appendChild(el);
                            })
                            this.update();
                        },
                        compile(template){
                            // template暂时不处理
                            return function render() {
                                const div = document.createElement('div');
                                // 这里的this就是setup函数的返回值,可以通过解构方式获取值
                                let {title='', info, name='', list, listobj} = this.state;
                                div.innerHTML = title +'<br/>' + info.msg +'<br/>' +  name + listobj[0].name;
                                return div
                            }
                        }
                    }
                }
            }
        </script>
        <script>
            const {createApp, reactive} = Vue;
            const app = createApp({
                setup() {
                    const state = reactive({
                        title: 'hi, vue3',
                        // 验证是否可以深度监听
                        info: {
                            msg: 'hello'
                        },
                        listobj: [{name: 'hh',  kk: {ll: '3434'}}],
                    });
                    setTimeout(() => {
                        // 改变属性名
                        state.title = '3434'
                        // 嵌套对象修改
                        state.info.msg = 'hei~~~~'
                        // 改变数组对象下标
                        state.listobj[0].name = 'change--name';
                        // 删除属性
                        delete state.title;
                        // 验证是否可以监听新增属性
                        state.name = 'you da da~~~'
                    }, 2000);
                    return {
                        state
                    }
                }
            });
            app.mount('#app');
        </script>
    </body>
</html>

整个事件流转如下:

  1. reactive(obj):可以监听对象属性的增删改查

  2. effect(fn):可以添加副作用,组件更新函数,fn会立刻执行一次

  3. fn执行时会访问属性,触发proxy的get

  4. 通过track(target, key)进行依赖收集(target, key和fn 关系存储)

  5. 当改变属性时触发proxy的set,从而调用trigger,拿出对应的fn执行

5. 总结

响应式整体流程是通过proxy的get进行依赖收集,通过map结构将要更新的函数与属性进行关联,利用proxy的set检测属性数据变化触发tirgger执行收集的对应的属性副作用包裹的函数,从而实现视图更新。

8400cedf9f1369c040e0a7a4570cf91f.png

9aa7e3033e237287c895786604356f65.png

感谢点赞支持~

附上历史文章

  • vue3系列:

【vue3系列】快速入门vue3,通过实例对比vue3和vue2区别

【vue3系列】一步步实现vue3.0简易版的createApp功能

【vue3系列】通过实例轻松理解vue3.0和vue2.0数据响应式原理和区别,并实现一个reactive

  • webpack系列:

【webpack系列】从源码角度分析webpack打包产出及核心流程

【webpack系列】从源码角度分析loader是如何触发和执行的

【webpack系列】webpack是如何解析模块的

【webpack系列】webpack的plugin插件是如何运行的

【webpack系列】从源码角度分析webpack热更新流程

【webpack系列】从源码角度深度剖析html-webpack-plugin执行过程