前言
Vue 的响应式原理一直都在被人解析,每个人都有不同的理解,在这里我只是记录自己所学的知识,以及尽可能写的清晰,并且希望能帮到还没有理解的同学。
响应式原理
Observer
劫持 data 内的所有数据进行响应处理
Compile
编译模版,只要模版内引用了 data 内的属性,就创建一个 Watcher,通过 Watcher 与更新函数、渲染函数之间建立一个关系。
Dep
它是一个 管家 的角色,管理着所有的 Wather,当数据变化的时候就通知 Wather,然后由 Wather 去通知页面进行更新。
Watcher
它是一个 中间人 的角色,用于监视页面中到底谁和属性之间有关系。
1.0
step1 数据响应化
首先对用户传入的配置做了一些保存,然后通过循环及递归对每一个属性进行响应化处理,而在 defineReactive 的 set 中巧妙的运用了 闭包 的特性去修改属性值。
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 把它收集起来,每次发现一个就创建一个 Watcher 跟 Compile 对应起来,另外创建一个 Dep 跟 Observer 对应起来,在过程中,只要有一个 data 里的属性出现就会有一个 Dep 的出现,Dep 和 Data 里的属性是一对一的对应关系,而 Dep 与 Wather 则是一对多的对应关系,因为一个属性在一个页面中可能会出现多次。
下面的代码理解起来比较简单,就直接贴出来吧。
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 重构了响应式的代码,这使得性能获得了提升,并且
前置知识 Proxy、WeakMap、Reflect、JS 的 Reflect 学习和应用
为了便于理解,画了一个简单的流程图,跟 Vue1.0 类似。
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 的处理,我们就得到了这样的数据格式。
// 存储 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());
}
}