vue2 数据响应式原理

154 阅读7分钟

分享内容与目的

彻底搞懂vue2的数据更新原理,手写相关代码。

\

什么是MVVM模式

数据变化,视图会自动变化。

// 模版
<p>我{{ age }}岁了</p>
// 数据变化
this.age++;

model:表示数据模型;

view:表示视图;

view-model:表示桥梁。

\

侵入式和非侵入式

// Vue 数据变化 (非侵入式)
this.age++; 

// React 数据变化  (侵入式)
this.setState({
	age: this.state.age + 1
})

侵入式:响应式将视图的更新封装到了setState中,调取函数改版数据与视图;

非侵入式:修改数据自动触发更新视图。

\

Vue使用非侵入式的钥匙

Object.defineProperty();

数据劫持/数据代理

\

利用javascript引擎赋予的功能检测对象属性变化。(IE8+)

\

Object.defineProperty()方法

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

const obj = {};

Object.defineProperty(obj, "a", {
	value: 3
})
Object.defineProperty(obj, "b", {
	value: 5
})
console.log(obj);

\

Object.defineProperty方法可以设置一些额外是隐藏属性。

Object.defineProperty(obj, "a", {
	value: 3,
  configurable: false, // 是否可被改变与删除
	writable: false, // 是否可写
  emumerable: false // 是否可被枚举
})

Object.defineProperty(obj, "a", {
	get() {
		console.log("试图访问obj的a的属性")
	},
	set() {
		console.log("试图改变obj的a属性")
	}
})

configurable

当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。
默认为 ****false

enumerable

当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。
默认为 ****false

\

数据描述符还具有以下可选键值:

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
默认为 ****undefined

writable

当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符(en-US)改变。
默认为 false

\

存取描述符还具有以下可选键值:

get

属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 ****undefined

set

属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined

\

defineReactive函数

为什么需要定义defineReactive

let temp = 1;
Object.defineProperty(obj, "a", {
	get() {
		console.log("试图访问obj的a的属性");
		return temp;
	},
	set(newVal) {
		console.log("试图改变obj的a属性");
		temp = newVal;
	}
})

getter/setter 需要变量中转。

function defineReactive(data, key, value) {
  const property = Object.getOwnPropertyDescriptor(data, key);
	if (property && property.configurable) {
		return
	}
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: true,
		get() {
			console.log(`试图访问obj的${key}属性`)
			return value;
		},
		set(newVal) {
			console.log(`试图改变obj的${key}属性`)
			if (value === newVal) return;
			value = newVal;
		}
	})
}

\

递归侦测对象全部属性

\

Observer类

创建一个正常的object转换为每一层的属性都是响应式(可以被侦测的)的object

class Observer {
	constructor(data) {
		this.walk(data);
	}
  // 遍历
	walk(data) {
		for (let key in data) {
			defineReactive(data, key, data[key]);
		}
	}
}

\

observe函数

创建一个observe函数返回响应式对象

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

function hasOwn(obj, key) {
	return Object.prototype.hasOwnProperty.call(obj, key);
}
export function typeOf(val) {
	return Object.prototype.toString.call(val).slice(8, -1).toLowerCase();
}
class Observer {
	constructor(data) {
    this.value = data;
		def(data, "__ob__", this); // this表示实例
		this.walk(data);
	}
	walk(data) {
		for (let key in data) {
			defineReactive(data, key, data[key]);
		}
	}
}

function observe(data) {
	const type = typeOf(data);
	if (!["array", "object"].includes(type)) {
		return
	}
	let ob;
	if (hasOwn(data, "__ob__")) {
		ob = data.__ob__;
	} else {
		ob = new Observer(data);
	}
	return ob;
}

\

改造defineReactive

循环创建observe函数;

function defineReactive(data, key, value) {
  const property = Object.getOwnPropertyDescriptor(data, key);
	if (property && !property.configurable) {
		return
	}
  // 递归调用
  observe(value);
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: true,
		get() {
			console.log(`试图访问obj的${key}属性`)
			return value;
		},
		set(newVal) {
			console.log(`试图改变obj的${key}属性`)
			if (value === newVal) return;
			value = newVal;
      observe(newVal);
		}
	})
}

\

思考

  1. 这样的数据劫持方式对数组有什么影响?

这样递归的方式其实无论是对象还是数组都进行了观测,但是比如此时 data包含数组[1,2,3,4,5]那么我们根据下标可以直接修改数据也能触发 set 但是如果一个数组里面有上千上万个元素 每一个元素下标都添加 get 和 set 方法 这样对于性能来说是承担不起的 所以此方法只用来劫持对象。

\

  1. 根据上面问题在vue中如果对象内容过多依然会出现此性能问题,该怎么办?

例:使用Object.freeze对象做数据冻结。

\

  1. Object.defineProperty 缺点?

对象新增或者删除的属性无法被监听到,只有对象本身存在的属性修改才会被劫持。

\

数组的响应式处理

使用侵入式数据侦测。

vue中使用改写push pop shift unshift splice reverse sort这7种方式主动触发。

\

  1. 备份数组自身方法
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
  1. 重写 array 7个方法
const methodsToPatch = ["push", "pop", "shift", "unshift", "splice", "reverse", "sort"];
methodsToPatch.forEach(function(method) {
  def(arrayMethods, method, function(...arg) {
  	const result = arrayProto[method].apply(this, args);
    return result;
  })
})
  1. Observer类中改写真实数组方法
const isProto = "__proto__" in {};
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
class Observer {
	constructor(data) {
		def(data, "__ob__", this);
		if (Array.isArray(data)) {
			rewriteArrayMethods(data, arrayMethods);
			this.observeArray(data);
		} else {
			this.walk(data);
		}
	}
	observeArray(items) {
		for (let i = 0; i < items.length; i++) {
			observe(items[i]);
		}
	}
	walk(data) {
		for (let key in data) {
			defineReactive(data, key, data[key]);
		}
	}
}
function rewriteArrayMethods(target, src) {
	if (isProto) {
		target.__proto__ = src; // ie 11+
	} else {
		arrayKeys.forEach(key => {
			def(target, key, src[key]);
		})
	}
}
  1. 获取新增数据手动触发响应式绑定
  def(arrayMethods, method, function(...arg) {
  	const result = arrayProto[method].apply(this, args);
    const ob = this.__ob__;
    let inserted;
     switch (method) {
      // [].push(1); => arg = [1];
      case "push":
      // [].unshift(2); => arg = [2]
      case "unshift":
        inserted = args;
        break;
      // [].splice(0, 1, 1); => arg = [0, 1, 1];
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    // 判断有没有插入的新项,让新项也变为响应式
    if(inserted) {
    	ob.observeArray(inserted);
    }
    return result;
  })

\

优化observe处理冻结对象问题

function observe(data) {
	const type = typeOf(data);
	if (!["array", "object"].includes(type)) {
		return
	}
	let ob;
	if (hasOwn(data, "__ob__")) {
		ob = data.__ob__;
	} else if (type === "array" || type === "object" &&Object.isExtensible(data)){
		ob = new Observer(data);
	}
	return ob;
}

\

处理对象添加与删除事件

function set (target, key, val) {
  if (Array.isArray(target)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val);
    return val;
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  const ob = target.__ob__
  if (!ob) {
    target[key] = val;
    return val;
  }
  defineReactive(ob.value, key, val);
  // 通知
  return val;
}

export function del (target, key) {
  if (Array.isArray(target)) {
    target.splice(key, 1);
    return;
  }
  if (!hasOwn(target, key)) {
    return;
  }
  delete target[key];
  // 通知
}

\

依赖收集

\

什么是依赖?

  • 需要用到数据的地方
  • 在getter中收集依赖,在setter中触发依赖

\

Dep类与Watcher类

  • 把依赖收集得代码封装成一个Dep类,它专门用来管理依赖
  • Watcher是一个中介,数据发生变化时通过Watcher中转通知组件

  • 依赖就是Watcher,只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
  • Dep采用发布订阅模式,当数据方式变化时,循环遍历依赖列表,通知所有的Watcher。
  • 将Wacher设置到全局的一个指定位置,然后读取数据,因为读取数据,所有触发getter。在getter中就能得到当前正在读取数据的Watcher,并把整个Watcher收集到Dep中。

  1. 发生setter与getter通知dep实例
class Dep{
	constructor() {
    console.log("创建一个Dep实例");
	}
	depend() {
    console.log("依赖添加");
	}
  notify() {
  	console.log("数据发生改变通知");
  }
}

function defineReactive(data, key, value) {
	const dep = new Dep();
	const property = Object.getOwnPropertyDescriptor(data, key);
	if (property && !property.configurable) {
		return
	}
	let childOb = observe(value);
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: true,
		get() {
			dep.depend();
			if (childOb) {
				childOb.dep.depend();
				if (Array.isArray(childOb)) {
					dependArray(childOb);
				}
			}
			console.log(`试图访问obj的${key}属性`)
			return value;
		},
		set(newVal) {
			console.log(`试图改变obj的${key}属性`)
			if (value === newVal) return;
			value = newVal;
			childOb = observe(newVal);
      // 通知dep
			dep.notify();
		}
	})
}

function dependArray(value) {
	for (const e of value) {
		if (e && e.__ob__ && e.__ob__.depend) {
			e.__ob__.depend();
			if (Array.isArray(e)) {
				dependArray(e);
			}
		}
	}
}

特例:数组

数组因为使用侵入式通知,需单独改造数组通知

  arrayMethods[method] = function (...args) {
    const result = arrayProto[method].apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    if (inserted) {
      ob.observeArray(inserted);
    }
    // 但是ob并不存在dep所以需要为Observer实例添加dep
    ob.dep.notify();
    return result;
  }

class Observer {
	constructor(data) {
    // 添加外部手动触发
		this.dep = new Dep();
		def(data, "__ob__", this);
		if (Array.isArray(data)) {
			rewriteArrayMethods(data, arrayMethods);
			this.observeArray(data);
		} else {
			this.walk(data);
		}
	}
}
  1. Watcher类的创建

假如我们希望实现这样的一个例子:当obj.a.m 的m发生变化时执行回调。

const obj = {
	a: {
		m: 1
	},
	b: [1,2,3,4],
	c: 10
};
observe(obj);
new Watcher(obj, "a.m", function (newVal, oldVal) {
	console.log("发生变化回调", newVal, oldVal);
})
  • Watcher实例化时处理 a.m 始其能正常解析;
  • 手动触发 一个value获取,使其触发observe中的getter方法;
  • 添加一个到Dep实例中的方法;
  • 添加update方法触发Watcher实例中的callback方法。

Watcher:

import Dep from "./dep";

function parsePath(str) {
	const arr = str.split(".");
	return function(obj) {
		for (const key of arr) {
			if (!obj) return;
			obj = obj[key];
		}
		return obj;
	}
}
export default class Watcher {
	constructor(target, expOrFn, cb) {
		this.target = target;
		this.getter = typeof expOrFn === "function" ? expOrFn : parsePath(expOrFn);
		this.cb = cb;
		// 手动触发data的getter
		this.value = this.get();
		console.log("我是一个Watcher实例")
	}
	get() {
		Dep.target = this;
		const value =  this.getter(this.target);
		Dep.target = null;
		return value;
	}
	addDep(dep) {
		dep.addSub(this);
	}
	update() {
		this.run();
	}
	run() {
		const value = this.get();
		if (value !== this.value) {
			const oldValue = this.value;
			this.value = value;
			this.cb(value, oldValue);
		}
	}
}

Dep

export default class Dep{
  static target = null;
	constructor() {
	  // 用数组存储自己的订阅者。
		this.subs = [];
		console.log("创建一个Dep实例");
	}
	// 添加订阅
	addSub(sub) {
		this.subs.push(sub);
	}
	// 添加依赖
	depend() {
		if (Dep.target) {
			Dep.target.addDep(this);
		}
	}
	notify() {
		console.log("数据发生改变通知");
		// 克隆subs 避免执行时数据发生变化
		const subs = this.subs.slice();
		for (const watcher of subs) {
			watcher.update();
		}
	}
}