vue 响应式原理解析

311 阅读4分钟

vue2源码解析双向绑定

Vue.js是采用数据劫持结合发布-订阅模式,通过Object.defineproperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

Object.defineProperty()使用方法

Object.defineProperty(obj,prop,descriptor)

  • obj:要添加或者修改属性的对象
  • prop:要添加或者修改属性的名称或Symbol
  • descriptor:要添加或者修改的属性描述符
let person = {
     name:'张三',
     gender:'男'
}
Object.defineProperty(person,'age',{value:'12'})
console.log(person)//=>{name:'张三',gender:'男',age:12}

题外话

如果进入下面的if语句

if(a ===1 && a === 2 && a === 3){
   console.log('You win!')
}

解决方法:调用Object.defineProperty()

let default = 0 
Object.defineProperty(window,'a',{
   get(){
     return ++ default
   }
})
if(a ===1 && a === 2 && a === 3){
   console.log('You win!')
}

Object.defineProperty()大概思路及流程:

思路:

  • 首先对数据进行劫持监听,设置一个Observer函数,用来监听所有属性的变化

  • 属性发生变化,告诉订阅者watcher是否需要更新数据,如果订阅者有多个,则需要一个Dep收集订阅者,在监听器observer和watcher之间进行统一管理。

  • 需要一个指令解析器compile,对需要监听的节点和属性进行扫描和解析。

流程:

第一步:需要一个监听器Observer,用来劫持并监听所有属性,如果属性发生变化,就通知订阅者。
  • observe()方法的目的:循环遍历数据对象的每个数据(forEach),进行监听调用defineReactive()函数
  • defineReactive()方法:为数据添加检测(调用Object.defineProperty()的get()、set()属性)
/*
* 循环遍历数据对象的每个属性,进行监听
*/
function observe(data){
   if(!data || typeof data !== 'object'){
     return
   }
   let keys = Object.keys(data)
   keys.forEach((key) =>{
      defineReactive(data,key,data[key])
   })
}
/*
* 为数据添加检测
*/
function defineReactive(data,key,val){
 
   Object.defineProperty(obj,key,{
      get(){
         console.log(`${key}属性被读取。。。`)
      return val
      },
      set(newVal){
        console.log(`${key}属性被修改。。。`)
      val = newVal
      }
   })
}
第二步:实现一个订阅器Dep,用来收集订阅者,对监听器Observer和订阅者watcher进行统一管理

订阅器Dep主要负责收集订阅者,然后当数据变化时执行对应订阅者的更新函数。

/*
* 创建消息订阅器Dep
*/
function Dep(){
   this.subs = []
}
Dep.prototype = {
   addSub:function(sub){
      this.subs.push(sub)
   }
   notify:function(){
      this.subs.forEach(function(sub){
         sub.update()
      })
   }
}
Dep.target = null

有了订阅器,再对defineReactive函数改造,向其植入订阅器

defineReactive: function(data, key, val) {
    var dep = new Dep();
	Object.defineProperty(data, key, {
	       enumerable: true,
	       configurable: true,
	       get: function getter () {
		     if (Dep.target) {
		         dep.addSub(Dep.target);
		     }
		    return val;
		},
		set: function setter (newVal) {
		      if (newVal === val) return;
			val = newVal;
			dep.notify();
		}
	});
}

总结:设计了一个订阅器Dep类,该类里面定义了一些属性和方法,Dep.target是一个静态属性,是全局唯一的watcher,因为在统一时间只能有一个全局的watcher被计算,另外它的自身属性subs也是watcher的数组。

第三步:实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的方法,从而更新视图

只要在订阅者watcher初始化的时候处罚对应的get函数执行添加订阅者操作即可。

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // 将自己添加到订阅器的操作
}

Watcher.prototype = {
  update: function() {
    this.run();
  },
  run: function() {
    var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function() {
    Dep.target = this; // 缓存自己,用于判断是否添加watcher。
    var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
    Dep.target = null; // 释放自己
    return value;
  },
};


第四步: 实现一个解析器Compile,可以解析每个节点的相关指令,对模板数据和订阅器进行初始化。

解析模板指令,并替换模板数据,初始化视图 将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器

Object.defineProperty()的缺点

  • vue2是一次递归到底的来实现响应式的

  • Vue无法检测到对象属性的新增或删除的变化

    只能劫持对象的属性,需要对每个对象,每个属性进行遍历。如果属性值是对象,就需要深度遍历。

    • 对象新增属性的修改使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。
    • 对象删除属性,可以使用Vue.delete(obj,propertyName/index)或者vue.$delete(obj,propertyName/index)
    • 对象赋值多个新的属性,可以使用 Object.assign() 或 _.extend()。这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
this.someObject = Object.assign({}, 

this.someObject, { a: 1, b: 2 })
  • Vue不能检测数组的变化

    • 当你利用索引直接设置一个数组项时。Vue.set(vm.items, indexOfItem, newValue)
    • 当你修改数组的长度时。vm.items.splice(newLength)
    • vue2中是如何实现数组的响应式的?重写数组的部分方法实现响应式,限制在数组的push/pop/shift/unshift/splice/reverse七个方法

Proxy

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

const p = new Proxy(target, handler)

  • target 是要包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

  • handler 代理配置:带有钩子的对象,比如get钩子用于读取target属性,set钩子写入target属性等。

Proxy 源码解析

Proxy(data,{
   get(target,key){
     return target[key]
   },
   set(target,key,value){
      let val = Reflect.set(target,key,value)
      _that.$dep[key].forEach(item => item.update())
      return val
   }
})

proxy已知的两个问题

  • Proxy本身不支持对象内部的深度检测,需要自己实现
  • Proxy本身支持数组变化侦测,但会有很多次触发的风险

例子

const obj = {
   info:{
      name:'eason',
      blogs:['webpack','babel','postcss']
   }
}
function handler(){}
function createReactive(data,handler){
    let res = {}
    for(let key in data){
       if(typeof data[key] === 'object'){
         res[key] = createReactive(data[key],handler)
       }else {
          res[key] = data[key]
       }
    }
    return new Proxy(res,{
        get(target,key){
           return Reflect.get(target,key)
        },
        set(target,key,val){
           handler()
           return Reflect.set(target,key,value)
        }
    })
}
let proxy = createReactive(obj,handler)

解析后的结果:

image.png 可以看到,对象内部的对象和数组都已经被代理了,但是当object是一个非常大且复杂的对象时,性能就不好。 修改如下

const obj = {
   info:{
      name:'eason',
      blogs:['webpack','babel','postcss']
   }
}
const handler = {
    get(target,key,receiver){
       const res = Reflect.get(target,key,reveiver)
   
    // 创建Proxy并返回
    if(isObject(res)){
       return createReactiveObject(target[key],handler)
    }else {
       return res
    }
  },
  set(target,key,value,receiver){
     const res = Reflect.set(target,key,value,receiver)
     return res
  }
}
function createReactiveObject(target,handler){
   observed = new Proxy(target,handler)
   return observed
}
let proxy = createReactiveObject(obj,rawToReactive,reactiveToRaw,handler)
Proxy的优点
  • Proxy直接代理整个对象而非对象属性,返回一个新对象
  • Proxy可以监听数组的变化,多种拦截方法