浅谈Vue响应式原理

1,253 阅读2分钟

Vue.js 的核心包括一套“响应式系统”。

“响应式”,是指当数据改变后,Vue 会通知到使用该数据的代码。例如,视图渲染中使用了数据,数据改变后,视图也会自动更新。

对于官网上关于响应式数据的描述,并不能让人短时间内明白其原理。下面我将按照我的理解分析一下Vue2.0响应式核心代码实现。

Vue中响应式数据分为:对象类型{}和数组类型[]

对象类型 {}

我们若想要实现响应式,需要以下类和方法:

  • 数据data
  • 数据监听器defineReactive
  • 订阅者(更新视图)watcher
  • 维护订阅者dep
  • 状态active

实现原理: 对象内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性)。

假设页面上有容器appdata存放响应式变量,当data中的值改变时,容器内的数值也会发生变化。

    <div id="app"></div>
    
    <script>
        let data = {
            count:0
        }
        app.innerHTML = data.count
    </script>

1.添加视图更新watcher

    <div id="app"></div>
    <script>
        let data = {
            count:0
        }
        // 定义watcher函数,传入参数为函数,且立即执行
        let watcher = (fn)=>{
            fn();
        }

        watcher(()=>{
            // 更换app内容
            app.innerHTML = data.count;
        })
    </script>

watcher所执行的操作 将页面上的内容更新=>视图更新

2.实现数据监听器

将data中的属性依次增加get()set()方法,这样当用户取值的时候,当作模版收集起来。待数据变化通知模版数据更新。

    <script>
        let data = {
            count:0
        }
        // 数据监听器
        function defineReactive(obj) {
            //每一个属性都重新定义get、set
            for(let key in obj){
            	let value = obj[key]
                Object.defineProperty(obj,key,{
                    // 当data中的值“出现”的时候,执行get()
                    get(){
                    	//将获取的原始值返回
                        return value;
                    },
                     // 当data中的值“改变”的时候,执行get()
                    set(newValue){
                        value = newValue
                    }
                })
            }
        }
        //劫持data中的数据
        defineReactive(data)

        //此时的a没有被数据监听器监测到,属于“后来者”不受劫持
        data.a = 10;

        // 定义watcher函数,传入参数为函数,且立即执行
        let watcher = (fn)=>{
            fn();
        }

        watcher(()=>{
            // 取值
            app.innerHTML = data.count;
        })
    </script>

3. 当数据改变时更新视图

   <div id="app"></div>
   <script>
       let data = {
           count:0
       }
       //需要执行的视图内容
       let active;
       // 数据监听器
       function defineReactive(obj) {
           for(let key in obj){
               let value = obj[key];
               //存放当前属性相关的所有方法
               let dep = [];
               Object.defineProperty(obj,key,{
                   // 当data中的值“出现”的时候,执行get()
                   get(){
                       //(3)
                       if(active){
                           dep.push(active)
                       }
                       return value
                   },
                    // 当data中的值“改变”的时候,执行get()
                   set(newValue){
                       value = newValue
                       dep.forEach(active=>active())
                   }
               })
           }
       }
       //劫持data中的数据
       defineReactive(data)


       // 定义watcher函数,传入参数为函数,且立即执行
       let watcher = (fn)=>{
           active = fn;
           //(1)
           fn();
           //(4)
           active = null;
       }

       watcher(()=>{
           // 更换app内容
           //(2)
           app.innerHTML = data.count;
       })
   </script>

当定义watcher时,会依次执行(1)=>(2)=>(3)=>(4)。 每个属性都拥有自己的dep属性,存放它所存放的watcher,当属性变化后会同志自己对应的watcher去更新。

Vue2.0响应式用的是Object.defineProperty Vue3.0响应式用的是proxy

data中的数据存在多层嵌套的时候,如果用Object.defineProperty,内部会进行递归,影响性能。proxy提升性能,但是不兼容ie11。

数组类型 []

数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择对数组原型上的方法进行重写(push,pop,shift,unshift,splice,sort,reverse)只有这7种方法会重写数组

    <div id="app"></div>
    <script>
        let data = [1,2,3];
        // 获取数组所有方法-原型链
        let originArray = Array.prototype;
        // 浅拷贝
        let arrayMethods = Object.create(originArray);
        // 数据监听器
        function defineReactive(obj) {
            // 函数劫持,重写方法。可以添加自己想要执行的内容
            arrayMethods.push = function (...args) {
                // 更改this指向
                originArray.push.call(this,...args);
                render();
            }
            // 挂载到原型上
            obj.__proto__ = arrayMethods
        }
        defineReactive(data)

        // 视图渲染
        function render() {
            app.innerHTML = data;
        }
        render();
       
    </script>

在Vue中修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新。数组中如果是对象类型也会进行递归劫持。

如果想要更改索引,可以通过Vue.$set来进行处理,内部核心代码是splice方法