Vue深度学习系列(一)响应式数据原理

466 阅读8分钟

Vue2.X实现

一、理论知识

Vue响应式数据原理:

Vue内部通过Object.defineProperty方法属性拦截的方式,把目标对象中的每个数据读写转换为getter/setter,当数据变化时通知视图更新。

二、基本流程

实现一个类似于vue响应式数据绑定的响应式系统,以下是简易流程图

image.png 由上面的图,关于监听器(Observer)、订阅器(Dep)、订阅者(Watcher)可能对没有学过设计模式的人有一点点抽象,那换个简单的例子说下他们的关系

小明要买房子,于是他加了一个售楼部销售员小红的微信,小红有很多关于住房的信息,小明想要的户型A恰好没有了,就拜托小红注意最新的消息,一旦这个户型又有了就立马通知小明,小明就马上付钱买下这个户型。小红有很多的客户其实都想买这个户型A,为了方便之后通知这些有意向的购买者,小红把这些人的微信记录在一个小本本上并且和户型A关联在一起。

在以上信息中,我们抽离出小明这个人他就是一个订阅者实例,小红手上的户型A信息就是数据,小红就是监听器实例,而这个记录这些A户型意向购买者信息的小本本就是Dep实例。当户型A有开始售卖了,小红就赶紧通知小本本上的人。

这里是举的一个小小的例子,可能不太恰当,详细具体的可以去看发布-订阅模式

接下来,我们一点一点的实现一个响应式系统

1.实现监听器Observer

在这里我们将通过Object.defineProperty方法实现数据劫持,也就是说我们能感知数据对象的读写,使得数据对象变得可观测。

  • 关于ObjectdefineProperty 这里就不展开细说了 详细的使用方法查看MDN。需要注意的就是属性描述符和存取描述符的使用

接下来是coding时间:

基本实现
  • defineReactive()方法实现属性的数据拦截
function defineReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    get: function getter() {
      console.log("获取")
      return value;
    },
    set: function setter(newValue) {
      if (newValue === value) return;
      value = newValue
      console.log("设置")
    }
  })
}

以上函数通过设置setter和getter方法来实现数据的监听

  • Observer类实现对象属性的遍历拦截
class Observer {
  constructor(obj) {
    this.value = obj
    this.trave()
  }
  trave() {
    Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key]))
  }
}

以上通过Object.keys方法遍历对象的所有属性,从而实现每个属性的数据拦截

优化提升

通过以上代码我们基本可以实现对一个对象obj所有属性的数据劫持,但是有一个小小的问题,如果对象属性是一个对象呢,也就是说obj内有嵌套属性,因此我们可以进行遍历,因此将代码优化为

class Observer {
  constructor(obj) {
    this.value = obj
    this.trave()
  }
  trave() {
    Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key]))
  }
}
function observe(obj){
  if(!obj||typeof obj!=='object'){
    return;
  }
  //监听器实例化
  new Observer(obj);
}
function defineReactive(obj, key, value) {
  //对每个属性值进行判断
  observe(value)
  Object.defineProperty(obj, key, {
    get: function getter() {
      console.log("获取")
      return value;
     
    },
    set: function setter(newValue) {
      if (newValue === value) return;
      value = newValue
      //对新设置的属性也需要进行判断
      observe(newValue)
      console.log("设置")
    }
  })
}

以上通过observe函数判断对象是否有嵌套属性,有的话就递归遍历,实现每一个属性的数据劫持

2.实现订阅者Watcher

先来了解Watcher需要实现的功能是什么?

  1. 当实例化一个订阅者的时候,意味着需要获取它订阅的属性
  2. 当订阅者收到数据变化后,需要去调用相应的回调函数

接下来coding时间:

基本实现
class Watcher{
  constructor(obj,exp,cb){
    this.obj = obj;
    //exp 为string类型 类似于'person.name'这样的string
    this.exp = exp;
    this.cb = cb;
    //实例化Watcher实例时获取它订阅的属性
    this.value = this.get()
  }
//订阅属性
  get(){
    var value = getValue(this.obj,this.exp);
    return value;
  }
//数据变化时,执行回调函数
  update(){
    var newVal = getValue(this.obj,this.exp);
    var oldVal = this.value;
    if(newVal!==oldVal){
      //数据更新
      this.value = newVal;
      //执行回调函数
      this.cb();
    }
  }
}
function getValue(obj,exp) {  
  var keys= exp.split(".");
  var value;
  for(let i of keys){
    if(!obj) return;
    value = obj[i];

  }
  return value;
}

关于Watcher的实例有很多地方需要去优化,先将订阅器Dep实现之后,我们再来探讨关于Watcher的优化

3.实现订阅器Dep

我们先来了解一下它需要实现的功能是什么?

  1. 每一个属性对应一个dep实例,dep实例存储每一个订阅该属性的Watcher
  2. 当数据发生变化时,对应的dep实例要去通知存储的每一个watcher
基本实现
class Dep{
  constructor(){
    this.subs = [];
  }
//将Watcher实例添加进subs数组
  addSub(sub){
    this.subs.push(sub);
  }
//派发消息 使得每个watcher实例调用update方法
  notify(){
    this.subs.forEach(function (sub) {  
      sub.update();
    })
  }  
}

以上只是实现了一个Dep类,有添加和通知watcher实例的方法

优化提升

经过上述的coding,我们只是进行了基本类的实现,而他们之间的联系还没有进行实现,具体是以下功能:

  1. 属性什么时候添加订阅器,属性改变时怎么通知对应的dep实例

通过改变数据劫持函数,在遍历每个属性的时候,添加对应的dep实例。

在setter方法更改属性值时,就需要去通知dep实例

function defineReactive(obj, key, value) {
  //实例化dep
  var dep = new Dep();
  observe(value)
  Object.defineProperty(obj, key, {
    get: function getter() {
        
      return value;
     
    },
    set: function setter(newValue) {
      if (newValue === value) return;
      value = newValue;
      observe(newValue);
      //派发消息
        dep.notify();
    }
  })
}
  1. 怎么将实例化watcher实例时添加到对应属性的dep实例中?

Watcher的get方法 当实例化一个watcher,获取对应属性值时,同时也触发了defineReactive函数的getter方法,因此在getter方法中将watcher实例添加到对应的dep数组中。

  1. 怎么getter方法中获取watcher实例?

很显然 将watcher实例放在getter方法能访问的地方就行了 在这里我们将它放到Dep.target属性上。

  • 修改Watcher
get(){
  //将watcher实例挂靠在getter方法能访问到地方
    Dep.target = this
    var value = getValue(this.obj,this.exp);
  //需要重置Dep.target  
    Dep.target = null;
    return value;
  }

  • 修改defineReactive函数
    get: function getter() {
    //判断Dep.target是否有值,如果有的话 添加watcher实例
        if(Dep.target){
        dep.addSub(Dep.target);
         return value;
      }
    }

三、完整代码

class Observer {
  constructor(obj) {
    this.value = obj
    this.trave()
  }
  trave() {
    Object.keys(this.value).forEach((key) => defineReactive(this.value, key,this.value[key]))
  }
}
function observe(obj){
  if(!obj||typeof obj!=='object'){
    return;
  }
  new Observer(obj);
}
function defineReactive(obj, key, value) {
  observe(value)
  //实例化dep
  var dep = new Dep();
  Object.defineProperty(obj, key, {
    get: function getter() {
      if(Dep.target){
        dep.addSub(Dep.target);
      }
      return value;
     
    },
    set: function setter(newValue) {
      if (newValue === value) return;
      value = newValue
      observe(newValue);
      dep.notify()
    }
  })
}
//watcher
class Watcher{
  constructor(obj,exp,cb){
    this.obj = obj;
    this.exp = exp;
    this.cb = cb;
    //实例化Watcher实例时获取它订阅的属性
    this.value = this.get()
  }
//订阅属性
  get(){
    Dep.target = this;
    var value = getValue(this.obj,this.exp);
    Dep.target = null
    return value;

  }
//数据变化时
  update(){
    var newVal = getValue(this.obj,this.exp);
    var oldVal = this.value;
    if(newVal!==oldVal){
      //数据更新
      this.value = newVal;
      this.cb(newVal,oldVal);
    }
  }
}
function getValue(obj,exp) {  
  var keys= exp.split(".");
  console.log(keys)
  var value;
  for(let i of keys){
    if(!obj) return;
    value = obj[i];

  }
  return value;
}

class Dep{
  constructor(){
    this.subs = [];
  }
//将Watcher实例添加进subs数组
  addSub(sub){
    this.subs.push(sub);
  }
//派发消息 使得每个watcher实例调用update方法
  notify(){
    this.subs.forEach(function (sub) {  
      sub.update();
    })
  }  
}

以上是一个简易版本的数据响应,具体的双向数据绑定需要加上compile解析器

Vue3.X实现

一、理论知识

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

关于Proxy就不细说了,直接看MDN或者阮一峰老师的文档

vue3中响应式处理都放在reactive类中,因此接下来就模仿vue3来实现数据的响应式处理

二、基本流程

基于Proxy的强大,接下来的代码主要体现代码的封装性

import {reactive} from './vue3/reactivity';
const state = reactive({
  name:'beatrix',
  age:'21',
  hobby:{
    "a":1,
    "b":2
  }
})

很显然,接下来我们需要去实现这样一个reactive方法,使得我们传入的对象能够被监听

1.实现reactive


import {mutableHandler} from './mutableHandler'
function reactive(target) { 

  return createReactiveObject(target,mutableHandler);
}

function createReactiveObject(target,baseHandler) {  
  if(!isObject(target)){
    return target
  }

  const observer = new Proxy(target,baseHandler);
  //返回一个代理对象
  return observer;
}
function isObject(obj){
  return typeof obj ==='object' && obj!== null 
  
}
 export{
   reactive
 }

以上代码主要注意一点

  • mutableHandler 它是我们在执行各种操作时代理新创建对象的行为,由很多的函数作为属性的对象

2.实现mutableHandler.js

import { reactive } from "./reactive"

function hasOwnProperty(target,key){
  return Object.prototype.hasOwnProperty.call(target,key)
}
function isEqual(newVal,oldVal) {  
  return newVal === oldVal
}

const get = createGetter(), 
      set = createSetter(); 

function createGetter () {
  return function get (target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    // todo ......
    console.log('响应式获取:' + target[key]);
    
    if (isObject(res)) {
    //进行递归
      return reactive(res);
    }

    return res;
  } 
}

function createSetter () {
  return function set (target, key, value, receiver) {
    const isKeyExist = hasOwnProperty(target, key),
          oldValue = target[key],
          res = Reflect.set(target, key, value, receiver);
    // todo ......
    
    if (!isKeyExist) {
      console.log('响应式新增:' + value);
    } else if (!isEqual(value, oldValue)) {
      console.log('响应式修改:' + key + '=' + value);
    }

    return res;
  }
}

const mutableHandler = {
  get,
  set
}

export {
  mutableHandler
}

以上主要注意通过Reflect来实现对属性的操作。

  • Reflect 关于Reflect可以直接看MDN

总结

关于vue2/3响应式数据的原理在这里只是做了简单的实现。

比如关于Vue3的reactive它当然不会有那么简单,只是实现了一点点而已,具体感兴趣的可以去查看源码.