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

1,046 阅读4分钟

所谓数据响应式就是当数据发生变化时页面视图会自动更新

1. vue2.0数据响应

1.1 Object.defineProperty

vue2.0实现方式是利用原生的方法Object.defineProperty实现对象属性的读写劫持。举个例子:

    let data = {
        name: 'mengmeng'
    }
    Object.defineProperty(data, name, {
        get(target, key) {
            // 收集依赖
            return target[key]
        },
        set(target, key, val) {
            target[key] = val;
            // 值发生改变时,通知更新
        }
    });
    data.name = 'meng'

vue2.0主要是使用Object.defineProperty把data的对象的属性全部转为getter/setter,让Vue能够追踪依赖,在属性被访问和修改时通知触发视图更新

1.2 存在问题

vue2.0数据响应式主要存在如下问题:

  • 不能监听数组变化

Object.defineProperty本身是可以监听数组变化的,但是由于性能问题,数组变化没有直接用Object.defineProperty进行监听。

我们开发时认为可以更新是因为vue2.0对数组常用的7个方法(push、pop、unshift、shift、sort、reverse和splice)通过原型链进行了拦截修改。

但是对于数组长度变化下标值修改内容是无法监听的,vue提供了Vue.set(要修改的值,索引值,修改后的值)进行响应式。不得不说大神考虑就是周到,原来是性能和用户体验做了权衡才这么处理的。

  • 不能监听对象属性的新增和删除 因为vue2.0是提前遍历数据监听响应,动态的属性没有被监听所以不能响应式。
  • 嵌套层次较深时,性能消耗大 因为Object.defineProperty会一开始就会遍历data、methods、props、computed、watch、mixins… 里的一系列变量全都绑定在this上,当嵌套层次比较深时会影响性能和占内存比较大。

基于以上问题,vue3.0放弃了这种方式,采用了proxy代理方式进行监听,可以优雅的解决响应式。

2. vue3.0数据响应

2.1 Proxy代理

先来看下MND描述:

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 使用方法如下:

new Proxy(data, {
   // 拦截对象的属性读取
   get(target, key) {
       // 为了更好的兼容性,源码使用Reflect.get(target[key])
       return target[key]
   },
   // 拦截对象的属性新增、修改
   set(target, key, val) {
       target[key] = val;
       // todo 检测到变化时通知更新
   },
   // 拦截对象的属性删除
   deleteProperty(target, key) {
       delete target[key]
   },
   ...
}) 

2.2 实例验证

接着上篇文章,我们继续为Vue添加一个reactive函数,主要用来处理引用类型,基本类型还需要一个函数ref,基本就是reactive的基础上封装的,相当于整体包裹为一个对象,增加了一个value属性。

 const Vue = {
       // 数据响应
       reactive(data) {
           return new Proxy(data, {
               // 拦截对象的属性读取
               get(target, key) {
                   return target[key]
               },
               // 拦截对象的属性新增、修改
               set(target, key, val) {
                   target[key] = val;
                   // 检测到变化时通知更新
               },
               // 拦截对象的属性删除
               deleteProperty(target, key) {
                   delete target[key]
               }
           }) 
       },
       ...
   }

通过测试用例验证数组通过下标改变内容和改变数组长度、对象嵌套的属性、新增属性和删除属性是否可以监听。

 <script>
   const {createApp, reactive} = Vue;
   const app = createApp({
       setup() {
           const state = reactive({
               title: 'hi, vue3',
           });
           setTimeout(() => {
               // 删除对象属性
                delete state.title
                // 验证是否可以监听新增属性
                state.name = 'you da da~~~'
           }, 1000);
           // 注意这里的对象return时不能解构 ...state,否则会失去响应式
           return {
               state
           }
       }
   });
   app.mount('#app');
</script>

附上完整html

<html>
    <body>
        <div id="app">
        </div>
        <script>
            const Vue = {
                // 数据响应(引用类型)
                reactive(data) {
                    return new Proxy(data, {
                        // 拦截对象的属性读取
                        get(target, key) {
                            return target[key]
                        },
                        // 拦截对象的属性新增、修改
                        set(target, key, val) {
                            target[key] = val;
                            // 检测到变化时通知更新,为了简化先直接调用实例的更新方法
                            app.update();
                        },
                        // 拦截对象的属性删除
                        deleteProperty(target, key) {
                            delete target[key]
                        }
                    }) 
                },
                // 创建应用程序实例
                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 = function() {
                            // 通过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='', name=''} = this.state;
                                div.innerHTML = title +'<br/>' + name;
                                return div
                            }
                        }
                    }
                }
            }
        </script>
        <script>
            const {createApp, reactive} = Vue;
            const app = createApp({
                setup() {
                    const state = reactive({
                        title: 'hi, vue3',
                    });
                    setTimeout(() => {
                        // 删除对象属性
                        delete state.title
                        // 验证是否可以监听新增属性
                        state.name = 'you da da~~~'
                    }, 1000);
                    return {
                        state,
                        list
                    }
                }
            });
            app.mount('#app');
        </script>
    </body>
</html>

浏览器运行,proxy可以实现对象新增属性和删除属性的监听,是不是很香~ 对于嵌套对象和数组监听的变化还需要特殊处理,下篇文章再细说

对于源码还涉及依赖收集触发副作用函数的逻辑,我们这里为了理解整体流程做了简化。

3. 总结

vue2.0对于数组和对象发生变化时的处理方式不同,使用vue3后发现再也不需要对某些场景数据变化视图没更新烦恼了,所以还是要拥抱变化。

感谢点赞支持~

附上历史文章

  • vue3系列

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

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

  • webpack系列

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

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

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

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

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

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