Vue数据响应式最终目标

236 阅读8分钟

Vue响应式原理是学习Vue的核心,了解其响应式原理,可以对Vue整体掌握更加熟练,学习Vue我们不仅要掌握其使用方式,更要掌握其源码,做到融会贯通,在面试中惊艳面试官

  1. 简述其原理:

Vue通过Object.definePerperty,根据其内部get(),set(),当读取数据时 执行get方法,当修改数据时执行set方法,以检测数据变化,当数据变化时,会通知观察者watcher,观察者会自动触发当前组件(子组件不会重新渲染),生成虚拟dom树,Vue会遍历旧的dom树和新的dom树中的每一个节点,并记录下来,将所有记录的不同点。局部修改到真实的dom树上。

  • 在具体的实现上 Vue用到了几个核心组件,接下来我们注意解析这些组件
  1. Observer:把普通对象变为响应式的对象
  2. Dep:记录依赖,派发更新
  3. Watcher:响应式数据改变,通过watcher来执行相应的函数 如render
  4. Scheduler:调度器:用来维护一个任务队列,防止数据改变 watcher重复执行

剖析响应式原理之Observer组件

  • 我们先简单概述一下Observer的功能
  1. Observer要实现的目标非常简单,就是把一个普通的对象转化为一个响应式的对象,为了实现这一点,Observer把对象的每个属性通过Object.defineproperty转化为带有getter 和setter属性,这样一来 当我们方位或者设置属性时,Vue就会有机会做一些别的事情

image.png 2. Observer是Vue中内部的构造器,我们可以使用Vue.observable(object)间接使用该功能,在组件的生命周期中,它发生在 beforeCreate 组件之后 created 之前 3. 具体实现上他会递归遍历对象的所有属性,以完成深度的属性转换。 4. 由于遍历只能遍历到对象的当前属性,所以无法检测到对象的增加和删除,因此Vue增加了setset 和delete两个实例方法,让开发者通过这两个实例方法增加和删除属性。 5. 对于数组 Vue会该改变他的隐式原型,之所以这样做,是因为vue需要监听哪些可能改变数组内容方法。

image.png 总之,Observer的目标,就是让一个对象,他的属性的读取,赋值,内部数组的变化都能被Vue感知到。

  • 手写Observer

1.了解Object.defineproperty

Object.defineproperty(data,key,descriptor)中有三个属性:1)参数data:为监听的对象,参数key为监听对象的key值,descriptor:是一个对象 对象中有get() set()方法,当读取对象的值时会执行get()方法,当修改数据时会执行set()方法。

  • 看代码
    const obj = {
        name:'zhangzhuo',
        age:18,
        love:{
            love1:"h1",
            love2:"h2",
            love3:"h3"
        },
        arr:[1,2,3]
    }
    /**
     *
     *
     **/
    function defineReactive(data,key,value){
        Object.defineProperty(data,key,{
            get(){//get()函数return的返回值是什么,读取的值就是什么,不写return返回为undefined
            console.log("数据被读取了");
            return "读取的值"
            },
            set(newVal){//newVal参数为改变后的值,即重新赋予的值
                console.log(newVal);
                console.log("数据被改变了");
            }
        })
    }
    defineReactive(obj,'name')
    console.log(obj.name);
    obj.name="zhangsan"

image.png

2.由于每一个value的值都是不一样的所以我们要再写一个observer函数 用来循环data对象

      const obj = {
        name: "zhangzhuo",
        age: 18,
        love: {
          love1: "h1",
          love2: "h2",
          love3: "h3",
        },
        arr: [1, 2, 3],
      };
      function defineReactive(data, key, value) {
        Object.defineProperty(data, key, {
          get() {
            //get()函数return的返回值是什么,读取的值就是什么,不写return返回为undefined
            console.log("数据被读取了");
            return value;
          },
          set(newVal) {
            //newVal参数为该变后的值
            if(value===newVal){//判断改变的值是否相等,如果相等不渲染,优化性能
                return
            }
            value = newVal;
            render()//值改变渲染页面 
          },
        });
      }
      
      function render(){
          console.log('页面渲染了');
      }
      
      function observer(data) {
        for (let key in data) {
          defineReactive(data, key, data[key]);
        }
      }
      observer(obj);

      console.log(obj.name);
      obj.name = "zhangsan";

3. 当我们要监听对象里面的深层对象时,需要进行递归观察观察

        name: "zhangzhuo",
        age: 18,
        love: {
          love1: "h1",
          love2: "h2",
          love3: "h3",
          love4: {
            like: "paly",
          },
        },
        arr: [1, 2, 3],
      };
      function defineReactive(data, key, value) {
        observer(value);
        Object.defineProperty(data, key, {
          get() {
            //get()函数return的返回值是什么,读取的值就是什么,不写return返回为undefined
            console.log("数据被读取了");
            return value;
          },
          set(newVal) {
            //newVal参数为该变后的值
            if (value === newVal) {
              //判断改变的值是否相等,如果相等不渲染,优化性能
              return;
            }
            value = newVal;
            render(); //值改变渲染页面
          },
        });
      }
      function render() {
        //该函数让页面渲染
        console.log("页面渲染了");
      }

      function observer(data) {
        if (typeof data === "object") {
          for (let key in data) {
            defineReactive(data, key, data[key]);
          }
        }
      }
      observer(obj);
      obj.love.love1="sss"
      console.log(obj.love.love1);

image.png

这也体现了使用Object.defineProperty的劣势

  • 劣势:
  1. 需要进行递归观察:耗费性能
  2. 数组不能响应(使用数组的变异方法)
  3. 对象的增和删不能响应
  4. 通过索引该数组,当页面中用到了这个数据,那么页面渲染
  5. 即使能够增改数组索引存在的值,但是vue也不会那么做,再实际项目中,有大量的数据存在于数组中,当我们更改数组时,会执行循环遍历,耗费性能。性能的代价和用户体验不成正比,所以使用数组的变异方法改变数组。

4. 数组响应式的处理

Vue在数组的原型上重写了数组方法

1. 通过改变其原型,然后执行render函数来渲染数据
2. 利用数组的splice方法来书写$set $delete方法来增加和删除数据
const data = {
  name: "shanshan",
  age: 18,
  shan: {
    name: "shanshan",
    age: 18,
    obj: {},
  },
  arr: [1, 2, 3],
};
/**
 * 重写数组上的原型方法
 */
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
["push", "pop", "shift", "unshift", "sort", "splice", "reverse"].forEach(
  (method) => {
    arrayMethods[method] = function () {
      arrayProto[method].call(this, ...arguments);
      render();
    };
  }
);

function defineReactive(data, key, value) {
  observer(value);
  Object.defineProperty(data, key, {
    get() {
      return value;
    },
    set(newVal) {
      if (value === newVal) {
        return;
      }
      value = newVal;
      render();
    },
  });
}

function observer(data) {
  if (Array.isArray(data)) {
    data.__proto__ = arrayMethods;
    return;
  }

  if (typeof data === "object") {
    for (let key in data) {
      defineReactive(data, key, data[key]);
    }
  }
}

function render() {
  console.log("页面渲染啦");
}
/**
 * 判断数据是否是数组,如果是则执行其splice方法来更改数据
 * @param {*} data 监听的数据
 * @param {*} key 监听的key值
 * @param {*} value 监听的值
 * @returns 
 */
function $set(data, key, value) {
  if (Array.isArray(data)) {
    data.splice(key, 1, value);
    return value;
  }
  defineReactive(data, key, value);
  render();
  return value;
}
/**
 * 判断数据是否是数组,如果是则执行其splice方法来删除数据
 * @param {*} data 监听的数据
 * @param {*} key 监听的key值
 */
function $delete(data, key) {
  if (Array.isArray(data)) {
    data.splice(key, 1);
    return;
  }
  delete data[key];
  render();
}

observer(data);

刨析到这里远远还不够

剖析Dep

到这里我们还有两个问题没有解决,也就是说读取属性时我们要做什么事情,属性变化时我们要做什么事情。这个问题就要依赖Dep来解决。Dep含义是Dependency表示依赖的意思。 Vue中会为响应式中的每一个属性,对象本身创建一个Dep实例,每一个Dep实例都有能力做以下两件事情

  • 记录依赖:记录是谁在用我
  • 派发依赖:我变了,我要通知那些用到我的人 当读取响应式中的对象时,会进行依赖收集:有人用到了我 当改变了某个属性时,会派发更新,那些用到我的人听好了 我更新了

image.png 以下html里面的代码都是被render渲染出来的,那么就会执行getter,h1中用到了obj.a obj.b,它被渲染出来就会执行render函数,就也是对于属性obj.a obj.b来说 render函数用到了我,那么就会被记录依赖,k没有被渲染,不会被dep记录

image.png

image.png

image.png

Watcher

这里又出现了一个问题,就是Dep如何知道谁在用我?我该把任务派发给谁? 解决这个问题,需要依赖一个东西 就是Watcher。 当某个函数执行的过程中,用到了响应式中的数据,响应式数据无法知道是谁用到了我,因此Vue通过一招巧妙的方式来解决这个问题。

我们不是直接执行一个函数,而是把这个函数交给Watcher函数执行,Watcher是一个对象,每一个函数执行都应该创建一个Watcher,通过Watcher来执行。

Watcher会设置一个全局变量,让全局变量记录当前负责执行的Watcher等于自己,在执行函数中,如果发生了依赖记录Dep.depend(),那么Dep就会把这个全局变量记录下来,表示:有一个Watcher用到了我这个属性

当Dep进行派发更新时,他会通知之前记录的Watcher:我变了

image.png 每一个Vue组件实例,都至少记录一个Wachter,改Watcher中记录了该组件的render函数。

image.png Watcher首先会把render函数运行一次进行依赖收集,于是那些在render函数中用到的响应式数据就会记录这个watcher

当数据变化时,dep就会通知该watcher,而watcher将会重新渲染rendr函数,从而让页面重新渲染同时重新记录当前依赖。

Scheduler:调度器

现在还剩下最后一个问题,就是Dep通知Watcher之后,如果Watcher执行重新运行对应的函数,就有可能导致函数的频繁运行,从而导致效率低下,

试想:如果一个交给Watcher的函数,他里面用到了a,b,c,d 那么a,b,c,d属性都会被记录依赖,于是下面的代码就会被出发4次更新:

state.a = "new data"
state.b = "new data"
state.c = "new data"
state.d = "new data"

这样显然是不合适的,因此,watcher收到派发更新的通知后,实际上不是立即执行,而是把自己交给一个叫作调度器的东西

调度器维护一个执行队列,该队列同一个watcher仅会执行一次,队列中的watcher不是立即执行,而是通过一个叫nextTick的工具方法,把这些需要执行的watcher放到一个事件循环的微队列中 nextTick的具体方法是通过Promise完成的

nextTick(()=>{
Promise().resolove().then(fn)
})

也就是说数据变化时,render函数的执行是异步的,并且在微队列中

总体流程

image.png