Vue1.x 2.x 3.x 的响应式实现

272 阅读5分钟

前言

Vue 的响应式原理一直都在被人解析,每个人都有不同的理解,在这里我只是记录自己所学的知识,以及尽可能写的清晰,并且希望能帮到还没有理解的同学。

响应式原理

vue底层原理关系图.png

Observer

劫持 data 内的所有数据进行响应处理

Compile

编译模版,只要模版内引用了 data 内的属性,就创建一个 Watcher,通过 Watcher 与更新函数、渲染函数之间建立一个关系。

Dep

它是一个 管家 的角色,管理着所有的 Wather,当数据变化的时候就通知 Wather,然后由 Wather 去通知页面进行更新。

Watcher

它是一个 中间人 的角色,用于监视页面中到底谁和属性之间有关系。

1.0

step1 数据响应化

首先对用户传入的配置做了一些保存,然后通过循环及递归对每一个属性进行响应化处理,而在 defineReactiveset 中巧妙的运用了 闭包 的特性去修改属性值。

class Vue {
	constructor(options) {
		this.$options = options;
		this.$data = options.data;

		this.observe(this.$data);

		new Compile(options.el, this);
	}

	observe(data) {
		if (!data || typeof data !== 'object') {
			return;
		}

		for (const [key, value] of Object.entries(data)) {
			this.defineReactive(data, key, value);
		}
	}

	defineReactive(data, key, value) {
		this.observe(value);

		const dep = new Dep();

		Object.defineProperty(data, key, {
			get() {
				// 依赖收集:把 Watcher 放到对应的 Dep 里
				Dep.target && dep.addDep(Dep.target);
				return value;
			},
			set(newVal) {
				if (newVal === value) {
					return;
				}
				value = newVal;
				console.log(`${key}属性更新了`);
			},
		});
	}
}

step2 实现 Dep

Dep 在这里的实现很简单,只需要实现添加函数 addDep 和通知更新函数 notify,用于通知所有 Watcher 进行更新。

this.deps 中存放的实际上就是一个个 Watcher

class Dep {
	constructor() {
		this.deps = [];
	}

	addDep(dep) {
		this.deps.push(dep);
	}

	notify() {
		this.deps.forEach((dep) => dep.update());
	}
}

step3 实现 Watcher

Watcher 负责创建 data 中的 key 和更新函数的映射关系。

假设页面中发现了一个地方用到了 data 里的属性,就需要 new Wacther()Watcher 的实例指定到 Dep.target 上,然后立刻读取一下这个属性,get 函数就会触发,于是 Watcher 就会作为依赖和 Dep 之间建立一个关系

class Watcher {
	constructor(vm, key, callback) {
		this.$vm = vm;
		this.key = key;
		this.callback = callback;

		Dep.target = this;
		this.vm[key]; // 触发依赖收集
		Dep.target = null; // 防止多次收集
	}

	update() {
		console.log(`${this.key} 更新了`);
		this.callback.call(this.vm, this.vm[this.key]);
	}
}

step4 实现 Compile

如果有一个 html 的页面,用户在写的时候会用到 data 里的值,我们需要尝试遍历 html 把它收集起来,每次发现一个就创建一个 WatcherCompile 对应起来,另外创建一个 DepObserver 对应起来,在过程中,只要有一个 data 里的属性出现就会有一个 Dep 的出现,DepData 里的属性是一对一的对应关系,而 DepWather 则是一对多的对应关系,因为一个属性在一个页面中可能会出现多次。

下面的代码理解起来比较简单,就直接贴出来吧。

class Compile {
	constructor(el, vm) {
		this.$vm = vm;

		this.$el = document.querySelector(el);

		this.compile(this.$el);
	}

	compile(el) {
		let childNodes = el.childNodes;
		Array.from(childNodes).forEach((node) => {
			// 判断节点类型
			if (this.isElement(node)) {
				// 编译节点
				this.compileElement(node);
			} else if (this.isInter(node)) {
				// 编译插值文本 {{}}
				this.compileText(node);
			}

			this.compile(node);
		});
	}

	isElement(node) {
		return node.nodeType === 1;
	}

	isInter(node) {
		return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
	}

	// 负责更新dom 同时创建 Watcher 实例在两者之间挂钩
	update(node, exp, dir) {
		// 初始化
		const updaterFn = this[dir + 'Updater'];
		const key = exp.trim();
		updaterFn && updaterFn(node, this.$vm[key]);

		new Watcher(this.$vm, key, function (value) {
			updaterFn && updaterFn(node, value);
		});
	}

	compileText(node) {
		this.update(node, RegExp.$1, 'text');
	}

	textUpdater(node, value) {
		node.textContent = value;
	}

	compileElement(node) {
		const nodeAttrs = node.attributes;

		Array.from(nodeAttrs).forEach((attr) => {
			const name = attr.name;
			const exp = attr.value;

			if (this.isDireactive(name)) {
				const dir = name.substring(2);
				this[dir] && this[dir](node, exp, dir);
			}
			if (this.isEvent(name)) {
				const dir = name.substring(1);
				this.eventHandler(node, exp, dir);
			}
		});
	}

	// 指令 v-xx
	isDireactive(attr) {
		return attr.includes('v-');
	}

	text(node, exp, dir) {
		this.update(node, exp, dir);
	}

	html(node, exp, dir) {
		this.update(node, exp, dir);
	}

	htmlUpdater(node, value) {
		node.innerHTML = value;
	}

	model(node, exp, dir) {
		this.update(node, exp, dir);

		node.addEventListener('input', (e) => {
			this.$vm[exp] = e.target.value;
		});
	}

	modelUpdater(node, value) {
		node.value = value;
	}

	// 事件 @xx
	isEvent(attr) {
		return attr.includes('@');
	}

	eventHandler(node, exp, dir) {
		let fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
		if (fn) {
			node.addEventListener(dir, fn.bind(this.$vm));
		}
	}
}

2.0

Vue1.0 中,每出现一次动态的值,都要创建一个 Watcher,那如果有几百个甚至几千个呢?

所以在 Vue2.0 中为了提升性能,引入了 虚拟 DOM。 这样使得 Watcher 的粒度变小了,小到了组件级别,也就是每个组件只有一个 Watcher

Vue1.0 中是一个 Dep 对应多个 Watcher ,而到了 Vue2.0 情况反过来了,多个 Dep 与一个 Watcher 产生了对应关系。

也正是因为每个组件只有一个 Watcher,当组件里的数据发生了变化,我不知道哪个组件发生了变化,所以 VDOM 就显得由为重要!

VDOM 把所有的变化记录下来,把上一次的变化和这一次的变化之间做个比对,发现这两个对象之间变化的那些地方,就是要更新的地方。

也就是说在 2.0 中,最大的变化其实就是引入了 虚拟 DOM

3.0

Vue3.0 使用了 Proxy 重构了响应式的代码,这使得性能获得了提升,并且

前置知识 ProxyWeakMapReflectJS 的 Reflect 学习和应用

为了便于理解,画了一个简单的流程图,跟 Vue1.0 类似。

Vue3响应式流程.png

reactive

用户在获取属性时会触发 get 这时候调用 track 进行依赖收集,并且只有在用户访问更深层次的属性时,才会进行递归

function reactive(target) {
	const handler = {
		get(target, key) {
			const res = Reflect.get(target, key);
			// 依赖收集
			track(target, key);
			return typeof res === 'object' ? reactive(res) : res;
		},
		set(target, key, val) {
			// 如果数据没变,就什么都不做
			if (target[key] === val) {
				return;
			}

			Reflect.set(target, key, val);
			// 通知变化,触发执行 effect
			trigger(target, key);
		},
	};
	const observed = new Proxy(target, handler);
	return observed;
}

effect

function effect(fn, options = {}) {
	// 依赖函数
	let e = createReactiveEffect(fn, options);
	// lazy仕computed配置的
	if (!options.lazy) {
		// 不是懒执行
		e();
	}
	return e;
}
function createReactiveEffect(fn, options) {
	// 构造固定格式的effect
	const effect = function effect(...args) {
		return run(effect, fn, args);
	};
	// effect的配置
	effect.deps = [];
	effect.computed = options.computed;
	effect.lazy = options.lazy;
	return effect;
}
function run(effect, fn, args) {
	// 执行effect
	// 取出effect 执行
	if (effectStack.indexOf(effect) === -1) {
		try {
			effectStack.push(effect);
			return fn(...args); // 执行effect
		} finally {
			effectStack.pop(); // effect执行完毕
		}
	}
}

track

假如数据是这样的。

  {
    name: '18zili',
    obj: {
      age: 18
    }
  }

那么经过 track 的处理,我们就得到了这样的数据格式。

WeakMap.png

// 存储 effect
let effectStack = [];
let targetMap = new WeakMap();
function track(target, key) {
	const effect = effectStack[effectStack.length - 1];
	if (effect) {
		let depMap = targetMap.get(target);
		if (!depMap) {
			depMap = new Map();
			targetMap.set(target, depMap);
		}

		let dep = depMap.get(key);
		if (!dep) {
			dep = new Set();
			depMap.set(key, dep);
		}

		if (!dep.has(effect)) {
			// 新增依赖 双向存储 方便查找优化
			dep.add(effect);
			effect.deps.push(dep);
		}
	}
}

trigger

function trigger(target, key) {
	// 数据变化后,通知更新 执行effect
	const depMap = targetMap.get(target);
	if (depMap === undefined) {
		return;
	}
	const effects = new Set();
	if (key) {
		let deps = depMap.get(key);
		deps.forEach((effect) => {
			effects.add(effect);
		});
		effects.forEach((effect) => effect());
	}
}

往期文章

50 行代码带你初步理解 Vue-Router 的原理