vue 数据响应式原理(偏响应式)

本文详细解析了Vue.js中数据响应化的实现原理,包括Object.defineProperty、包装原始数据劫持、递归遍历对象属性以及如何处理数组的响应式。文中介绍了通过数据劫持来监听属性的访问和修改,以及如何在setter外部实现依赖收集和监听器的更新,确保视图与数据的同步。同时,文章还讨论了数组操作方法的响应式改造,如.push、.pop等,确保数组变化时能够正确触发视图更新。
摘要由CSDN通过智能技术生成

数据变化:
在这里插入图片描述

在这里插入图片描述

1、Object.defineProperty数据劫持:

	每次访问或修改对象某个属性时,都能够被get或set捕捉到,从而能够进行响应式的处理
	Object.defineProperty(obj, 'a', {
	  // value: 3,	不能同时指定get当有value/writable属性,set同理
	  get() {		当访问obj对象的a属性时,会进入到getter中,相当于被劫持了	
	    return 4;
	  },
	  set(newValue){
	  	
	  }	  
	})
	弊端:
		虽然能数据劫持,但是修改属性时,set并不能完成对数据的修改,调用this.a来修改会因为循环进入set而死循环,最终返回的值依旧是get中设置的值,相当于修改无效
			解决方式:在外部定义一个变量,set中将新值赋给变量,get中返回变量

2、包装原始数据劫持:

使用闭包,将上述的变量保存起来
function defineReactive(obj,key,val) {
	  if(arguments.length==2){	未传入第三个参数,赋值属性本身的值
	  	val=obj[key];
	  }
	  	
	  Object.defineProperty(obj, key, {
		  get() {
		  	return val;
		  },
		  set(newVal) {
		  	if (newVal == val) {
			  return;
			}   
			val = newVal;
		    
		  },
		  enumerable: true,
		  configurable:true
	 })
}
	defineReactive(obj, 'a', 0);
	console.log(obj.a);
	obj.a = 5;
	console.log(obj.a);
	
	弊端:无法劫持更深层次的属性,如obj.a.b

在这里插入图片描述
3、相互引用来实现递归遍历对象每层属性,将所有属性转换成响应式(调用能够被劫持):

  循环调用顺序observe->Observer->defineReactive->observe...
  observe方法:将属性对象交给Observer,属性不为对象则返回空,为对象则返回对应的Observer实例
  Observer类:将属性对象添加__ob__属性并在__ob__上挂载一个Observer实例,并调用实例的walk方法遍历对象属性并调用defineReactive传递属性
  	def:Observer类内部调用,给属性添加__ob__属性并赋值当前Observer实例,并将__ob__变成不可枚举的一个方法
  defineReactive方法:若属性不为对象,将属性变为响应式,否则继续交给observe递归调用

observe.js:

import Observer from './observer';

//辅助判别函数,将传入的对象添加__ob__属性,并将对象每个层级属性转换成响应式的
export default function observe(value) {
  if (typeof value != 'object') { //属性值是基础类型直接返回
    return;
  }
  var ob;
  if (typeof value.__ob__!=='undefined') { //vue中, __ob__会指向一个Observer对象,每个被双向绑定的对象元素(数组也是对象)都会有一个__ob__ ,而且是单例的
    ob = value.__ob__;
  } else {
    ob = new Observer(value); //将传入的对象转换为每个层级都是响应式的(可以被数据劫持侦测的)的对象
  }

  return ob;
}

Observer类:

import { def } from './util'
import defineReactive from './defineReactive'

export default class Observer{ //将传入的对象转换为每个层级都是响应式的(可以被数据劫持侦测的)的对象
  constructor(value) {
    console.log('observer');
    //给传入的对象添加不可枚举属性__ob__,避免遍历时获取到,并将将__ob__属性值设置为当前实例this
    def(value, '__ob__', this, false);
    this.walk(value);
  }

  walk(value) { //遍历
    
    for (let k in value) {
      defineReactive(value, k); //将对象属性转换成可被数据劫持的对象
    }
  }
}

def方法:

export const def = function (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable,
    writable: true,
    configurable:true
  })
}

defineReactive:

import observe from './observe'; //辅助判别函数,将传入的对象添加__ob__属性,并将对象每个层级属性转换成响应式的

export default function defineReactive(obj, key, value) {
  if (arguments.length == 2) {
    value = obj[key];
  }
  
  //会产生递归的效果,会将当前属性值中的内层属性遍历,内层属性为了变成响应式又会调用defineReactive方法,直到当前属性内层的属性不是个对象为止,才会走后续的操作,然后递归进行返回走之后的操作
  //循环调用顺序observe->Observer->defineReactive->observe...
  //observe:将属性对象交给Observer,属性不为对象返回
  //Observer:将属性对象添加__ob__属性并在__ob__上挂载一个Observer实例,并调用实例的walk方法遍历对象属性调用defineReactive
  //defineReactive:若属性不为对象,将属性变为响应式,否则继续交给observe递归调用
  let childOb=observe(value); //返回属性对象对应的Observer实例

  Object.defineProperty(obj, key, {
    get() {
      console.log('访问obj的'+key+'属性')  
      return value;
    },
    set(newValue) {
      console.log('修改obj的' +key+'属性为'+newValue)
      if (newValue == value) {
        return;
      }  
      
      value = newValue;
      //如果赋的值是一个对象,则赋值后对象内部属性也应该是响应式的
      childOb=observe(newValue);
    },
    enumerable: true,
    configurable:true
  })
}

将传入的对象每层属性都变成响应式,index.js:

import observe from './observe';

var obj = {
  a: {
    m: {
      n: 5
    }
  },
  b:4
};


observe(obj);

console.log(obj);
obj.a.m.n++;	

将会打印:
在这里插入图片描述

实现数组的响应式:
1、通过上一部分,能够将数组变成响应式,即访问修改能够侦测到
2、但通过.push方法添加元素,因为并没有进行赋值的操作,所以没有触发set,不能响应式侦测

解决方法:
	修改数组的方法调用,使得调用时能够被侦测
	(1)定义一个方法,若属性是一个数组,则调用该方法,将修改后的'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'等方法挂载到数组原型上,使得数组调用对应方法时,调用这些方法
		1、通过Object.create可以创造一个原型指向原生数组原型的对象,即新对象具有原生数组的所有方法
		2、为新对象添加'push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice'对应的属性,并重写方法以及设置成不可枚举
			使用上一部分中的def方法
		3、因为新对象会被挂载到数组原型上,所以this会指向数组,数组调用上述方法时会优先调用新对象中重写的方法
			因此可通过arguments获取到调用时的参数,意味着可通过调用原生的对应方法实现和原生方法相同效果,且因为为自定义方法,使得调用时能被我们知道,即被侦测
		4、因为数组对象上绑定了__ob__属性,即Observer实例,就可调用实例上的observeArray方法,并将添加的参数传入,使得参数中的数组和对象变成响应式的
			observeArray方法:遍历数组的元素,将元素传入observe中,使得对象元素被侦测,数组元素的原型方法修改为自定义的七种方法,普通元素不处理,实现响应式
		
	(2)修改Observer类,添加如果传入的是数组判断,如果是数组则修改数组的原型为修改后的对象,并调用observeArray方法,遍历数组元素,将元素传入observe中,使得对象元素被侦测,数组元素的原型方法修改为自定义的七种方法,普通元素不处理,实现响应式
		Object.setPrototypeOf能够修改对象原型的指向
	
解释了为什么vue中,数组[下标]=值视图不能刷新,但若数组[下标]为对象,数组[下标]=值/或数组[下标].属性=值能够被响应,数组.push等七种指定方法能够被追踪刷新视图

array.js,暴露修改了七种方法的对象,将被挂载到数组对象的原型上:

import {def} from './util'

//被改写的七个数组方法
const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']

//获取原生数组原型链方法
const arrayPrototype = Array.prototype;

//创造一个原型链为原生数组原型链的对象,不能直接使用原型对象赋值,否则将直接修改原生原型链上的方法
//这样的目的是,如果使用改写的方法,则直接调用对象
//并将其暴露,在Obsever类中,修改数组的原型指向
export const arrayMethods = Object.create(arrayPrototype);


methods.forEach(method => {
  //备份原来的方法
  const original = arrayPrototype[method]; //直接执行原函数,会因为无上下文而将内部的this指向window而报错

  //取出数组身上的在observe方法调用后添加的__ob__属性,即Observer实例
  // console.log(this);


  //定义新的方法
  def(arrayMethods, method, function () {

    //因为this指向arrayMethods对象,而arrayMethods是数组对象的原型,在调用时是数组在调用该方法,所以this又指向了数组本身
    //取出数组身上的在observe方法调用后添加的__ob__属性,即Observer实例
    const ob = this.__ob__;
    
    //将类数组变成数组,使其具有数组的方法
    let transArguments = Array.from(arguments);
    

    //能够添加新元素的三种方法push、spilce、unshift,添加的元素都应该是响应式的
    let inserted = [];
    switch (method) {
      case "push":  //push、unshift参数都相同
      case "unshift":
        inserted = transArguments;
        break;
      case "splice":
        inserted = transArguments.slice(2); //splice从第二参数之后才是添加的元素
        break;
    }

    //将新元素变成响应式
    if (inserted.length) {
      ob.observeArray(inserted);
    }

    //arguments为调用该方法传入的参数
    let res=original.apply(this, arguments); //调用原数组方法,使得功能未改变

    console.log('new me')
    return res;
  }, false);

})

Observer类,添加了判断属性是数组的清空:
添加了能够将数组原型指向修改后的对象上,和遍历素组元素,递归检测的方法

import { def } from './util'
import defineReactive from './defineReactive'
import {arrayMethods} from './array'
import observe from './observe';

export default class Observer{ //将传入的对象转换为每个层级都是响应式的(可以被数据劫持侦测的)的对象
  constructor(value) {
    //给传入的对象添加不可枚举属性__ob__,避免遍历时获取到,并将将__ob__属性值设置为当前实例this
    def(value, '__ob__', this, false);

    //检查属性是否是数组
    if (Array.isArray(value)) {
      //是数组,修改数组的原型为修改后的对象
      Object.setPrototypeOf(value, arrayMethods); //使得调用数组的七种指定方法能够被侦测
      //遍历数组元素调用observe方法,只将数组中的对象变成响应式的,数组中的数组调用七种指定方法能够侦测,普通值不响应
      //解释了为什么vue中,数组[下标]=值视图不能刷新,但若数组[下标]为对象,数组[下标]=值/或数组[下标].属性=值能够被响应,数组.push等七种指定方法能够被追踪刷新视图
      this.observeArray(value);
      
    } else {  //如果是对象
      this.walk(value);  
    }
    
    
  }

  walk(value) { //遍历
    
    for (let k in value) {
      defineReactive(value, k); //将对象属性转换成可被数据劫持的对象
    }
  }

  observeArray(arr) {
    for (let i = 0,l=arr.length; i < l; i++){ //循环检测每一项是否是数组或对象,普通值会退出
      observe(arr[i]);
    }
  }
}

在setter外部实现依赖改变监听(依赖:状态数据),即Vue中的watch函数/计算属性等依赖监听实现
在这里插入图片描述在这里插入图片描述

收集监听器需要当监听器开启时(即 new Watcher后)再进行,所以需要一个全局标识,这里往Dep类上添加一个target属性,Dep.target默认为null,当new Watcher后赋值为Watcher实例作为标识

1、创建一个Dep类,用来保存watcher监听器实例,并提供一个方法来添加监听器,一个方法来通知监听器再次获取值
	当非数组的普通值元素(包括数组本身)进行修改和访问时,都会进入set和get内,所以在get中收集监听器依赖,在set中通知收集的监听器再次获取要监听的属性值
	
	当数组调用如.push方法时,因为不会进入get,而是进入之前修改后的自定义push中(在array.js中),所以在自定义的方法中通知收集的监听器再次获取要监听的属性值
		而因为自定义修改的方法能通过this.__ob__访问到Observer实例,而在set之前也会调用observe来返回对应的Observer实例,所以在set方法中,对于数组对象,将监听器存放在对应的Observer实例的dep属性上,dep属性也是一个Dep实例,
		因此就能通过调用Observer实例上的dep属性来通知存储的监听器来再次获取要监听的属性值
		
2、创建一个Watcher监听器类,传入要监听的对象、要监听的对象的值的属性链(如:"a.b.c",a为属性)、对象值修改后的回调
	当首次实例化监听器时,就会将对象上的值存在监听器实例上,又因为会根据属性链依次访问属性的get,所以每次访问都会将该监听器实例存储进dep中
	Watcher还会提供一个update方法,用来再次获取要监听的属性值,会在Dep通知监听器后触发

Dep类:

var uid = 0;
export default class Dep{
  constructor() {

    this.id = uid++; //唯一id

    //订阅的是watcher的实例
    this.subs = [];

  }

  addSub(sub) {  //添加订阅
    // console.log(sub);
    this.subs.push(sub);
  }

  depend() {  //添加依赖
    //自己指定的开始收集监听器依赖的全局标识(new Watcher后),这里指定为指定了属性链的Watcher实例
    if (Dep.target) {
      this.addSub(Dep.target);
    }
  }

  notify() { //通知更新
    console.log('notify');
    //浅克隆
    const subs = this.subs.slice();
    console.log(subs);
    
    for (let i = 0, l = subs.length; i < l; i++){
      
      subs[i].update();
    }
  }
}

Watcher监听器类:

import Dep from "./dep";

var uid = 0;
export default class Watcher{
  constructor(target,expression,callback) { //target:要监听的对象,expression:'a.b.c'属性调用链,callback:c属性改变时的回调
    this.id = uid++;	//id标识
    this.target = target;
    this.getter = parsePath(expression);//传入"a.b.c",解析返回{a:{b:{c:4}}}中c的值
    this.callback = callback;
    this.value = this.get();//实例化时,就会获取一次属性调用链指定的属性值,会进入每个属性调用链上的属性的get中,所以如a.b.c,当c之前的任意属性更改,都能触发wather的更新update函数,重新获取值
  }
  update() { //重新获取指定expression下的对象的属性值
    this.run()
  }

  run() {
    this.getAndInvoke(this.callback);
  }

  getAndInvoke(callback) { //重新调用get方法,获取指定expression下的对象的属性值
    const value = this.get();

    if (value !== this.value || typeof value == 'object') { //当重新获取的值和上一次值不相同时,更新值,并触发回调
      const oldValue = this.value;
      this.value = value;
      callback.call(this.target, value, oldValue);//调用对象为要监听的对象,value为新值,oldValue为之前的值
    }
  }

  get() {//获取指定expression下的对象的属性值

    //进入依赖收集,即全局Dep.target设置为watcher实例,能调用其中方法
    Dep.target = this;
    const obj = this.target;
    let value;
    try {
      value =this.getter(obj);  //会进入每个属性调用链上的属性的get中,所以如a.b.c,当c之前的任意属性更改,都能触发wather的更新update函数,重新获取值
    } catch (e) {
      
    } finally {
      
      Dep.target = null; //退出依赖收集,让给别的watcher
    }
    
    return value;
  }
}

function parsePath(str) { //传入"a.b.c",返回一个函数用来解析返回{a:{b:{c:4}}}中c的值,最后返回的函数的返回值为c的值
  let segments = str.split('.');

  return (obj) => {
    for (let i = 0; i < segments.length; i++){
      if (!obj) {
        return;
      }
      obj = obj[segments[i]];
    }
    return obj;
  }
}

修改Observer类,添加dep实例属性,因为每个数组对象都有__ob__属性,即Observer实例,所以将监听器放在添加的dep属性上,dep是个Dep实例,当数组调用.push方法等,会进入修改后的方法,修改后的方法能获取到Observer实例,从而获取到dep上存储的监听器,从而通知监听器再次获取指定属性的值:

import { def } from './util'
import defineReactive from './defineReactive'
import {arrayMethods} from './array'
import observe from './observe';
import Dep from './dep';


export default class Observer{ //将传入的对象转换为每个层级都是响应式的(可以被数据劫持侦测的)的对象
  constructor(value) {
    
    //会为每个对象绑定一个对应的__ob__属性,即Observer实例,故在此收集的监听器可以提供给修改后的数组方法中能够访问
    this.dep = new Dep();


    //给传入的对象添加不可枚举属性__ob__,避免遍历时获取到,并将将__ob__属性值设置为当前实例this
    def(value, '__ob__', this, false);

    //检查属性是否是数组
    if (Array.isArray(value)) {
      //是数组,修改数组的原型为修改后的对象
      Object.setPrototypeOf(value, arrayMethods); //使得调用数组的七种指定方法能够被侦测
      //遍历数组元素调用observe方法,只将数组中的对象变成响应式的,数组中的数组调用七种指定方法能够侦测,普通值不响应
      //解释了为什么vue中,数组[下标]=值视图不能刷新,但若数组[下标]为对象,数组[下标]=值/或数组[下标].属性=值能够被响应,数组.push等七种指定方法能够被追踪刷新视图
      this.observeArray(value);
      
    } else {  //如果是对象
      this.walk(value);  
    }
    
    
  }

  walk(value) { //遍历
    
    for (let k in value) {
      defineReactive(value, k); //将对象属性转换成可被数据劫持的对象
    }
  }

  observeArray(arr) {
    for (let i = 0,l=arr.length; i < l; i++){ //循环检测每一项是否是数组或对象,普通值会退出
      observe(arr[i]);
    }
  }
}

修改defineReactive,使得非数组的普通值元素每次get/set都能触发收集监听器/触发监听器再次更新,以及将监听器收集在Observer实例的dep上,便于.push等自定义函数中获取到

import observe from './observe'; //辅助判别函数,将传入的对象添加__ob__属性,并将对象每个层级属性转换成响应式的
import Dep from './dep';

export default function defineReactive(obj, key, value) {

  const dep = new Dep();

  if (arguments.length == 2) {
    value = obj[key];
  }
  
  //会产生递归的效果,会将当前属性值中的内层属性遍历,内层属性为了变成响应式又会调用defineReactive方法,直到当前属性内层的属性不是个对象为止,才会走后续的操作,然后递归进行返回走之后的操作
  //循环调用顺序observe->Observer->defineReactive->observe...
  //observe:将属性对象交给Observer,属性不为对象返回
  //Observer:将属性对象添加__ob__属性并在__ob__上挂载一个Observer实例,并调用实例的walk方法遍历对象属性调用defineReactive
  //defineReactive:若属性不为对象,将属性变为响应式,否则继续交给observe递归调用
  let childOb=observe(value); //返回属性对象对应的Observer实例

  Object.defineProperty(obj, key, {
    get() {
      console.log('访问obj的' + key + '属性')
      
      if (Dep.target) { //如果处于依赖收集阶段,即new Watcher了,因为会先调用new Watcher,其中的get方法,会根据属性链(如:"a.b.c"),挨个进入getter,并在各自的闭包中保存watcher实例
        //使得"a.b.c",若监听的是.c,则c之前任意一个属性变化,都会触发
        dep.depend();  //将对应的watcher实例保存在当前属性的set闭包中
        if (childOb) { //如果当前属性是个数组,则不会触发set,为了在.push等方法中触发监听,所以将watcher实例保存在,数组对象的Observer实例的dep属性上,在修改后的数组的方法(array.js),如push,又能通过this.__ob__获取到Observer实例,继而能够通过Observer实例获取到dep实例,然后调用dep实例上的方法通知watcher来更新监听
          childOb.dep.depend();//将watcher实例保存在当前对象属性的Observer实例的dep实例属性中
        }
      }

      return value;
    },
    set(newValue) {
      console.log('修改obj的' +key+'属性为'+newValue)
      if (newValue == value) {
        return;
      }  
      
      value = newValue;
      //如果赋的值是一个对象,则赋值后对象内部属性也应该是响应式的
      childOb = observe(newValue);
      
      //发布订阅模式,当对象属性改变时,通知dep改变,这里无法通知到数组.push等调用的改变,需要在array.js中,重写的push等方法中进行通知
      dep.notify()
    },
    enumerable: true,
    configurable:true
  })
}

修改array.js,因为数组的push等方法会进入这里,所以在这里获取Observer实例上保存的dep属性上保存的监听器,并通知监听器再次获取值

import {def} from './util'

//被改写的七个数组方法
const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']

//获取原生数组原型链方法
const arrayPrototype = Array.prototype;

//创造一个原型链为原生数组原型链的对象,不能直接使用原型对象赋值,否则将直接修改原生原型链上的方法
//这样的目的是,如果使用改写的方法,则直接调用对象
//并将其暴露,在Obsever类中,修改数组的原型指向
export const arrayMethods = Object.create(arrayPrototype);


methods.forEach(method => {
  //备份原来的方法
  const original = arrayPrototype[method]; //直接执行原函数,会因为无上下文而将内部的this指向window而报错

  //取出数组身上的在observe方法调用后添加的__ob__属性,即Observer实例
  // console.log(this);


  //定义新的方法
  def(arrayMethods, method, function () {

    //因为this指向arrayMethods对象,而arrayMethods是数组对象的原型,在调用时是数组在调用该方法,所以this又指向了数组本身
    //取出数组身上的在observe方法调用后添加的__ob__属性,即Observer实例
    const ob = this.__ob__;
    
    //将类数组变成数组,使其具有数组的方法
    let transArguments = Array.from(arguments);
    

    //能够添加新元素的三种方法push、spilce、unshift,添加的元素都应该是响应式的
    let inserted = [];
    switch (method) {
      case "push":  //push、unshift参数都相同
      case "unshift":
        inserted = transArguments;
        break;
      case "splice":
        inserted = transArguments.slice(2); //splice从第二参数之后才是添加的元素
        break;
    }

    //将新元素变成响应式
    if (inserted.length) {
      ob.observeArray(inserted);
    }
    
    //当数组调用了修改后的方法时,也通知改变
    ob.dep.notify();

    console.log('new me')
    //arguments为调用该方法传入的参数
    let res = original.apply(this, arguments); //调用原数组方法,使得功能未改变
    
    return res;
  }, false);

})

index.js调用监听器:

import defineReactive from './defineReactive';
import observe from './observe';
import Watcher from './watcher';

var obj = {
  a: {
    m: {
      n: 5,
      g:7
    }
  },
  b: 4,
  c:[1,2,3]
};


observe(obj);


new Watcher(obj, 'a.m.n', (val) => {
  console.log("***watcher监听***",val);
})


obj.a.m.n= 4;
// obj.a.m.g = 9;
// obj.b = 5;

会打印输出监听修改的结果:
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值