响应式系统的构建思路
tips: 该系统搭建思路参考自vuejs2.7.14
源码
Vue
的响应式系统的构建是比较复杂的,在深入探究vue整体实现思路之前,我认为在尽可能保留源码设计逻辑下,自己动手实现一个基础的响应式系统是有必要的。这样对Dep
、Watcher
和Observer
等概念也会有初步的认识。
框架搭建
接下来我们将模拟Vue源码的实现思路,实现一个简单的类响应式框架MiniVue
。
参照vue的使用方法,我们在实例化MiniVue
时可传递一个配置项。这个配置项需要尽可能简单化。定义入参只由挂载元素el
,数据对象data
和方法对象methods
构成。模拟源码实现思路,MiniVue在实例化时会进行数据的初始化,数据初始化后开始真实dom的挂载。
<!-- index.html -->
<body>
<div id="app">
<h1>{{ count }}</h1>
</div>
<button id="addBtn">add</button>
<button id="subBtn">sub</button>
<script src="minivue.js"></script>
<script>
const app = new MiniVue({
el: '#app',
data() {
return {
count: 0,
};
},
methods: {
add() {
this.count++;
},
sub() {
this.count--;
},
},
});
const addBtn = document.getElementById('addBtn');
const subBtn = document.getElementById('subBtn');
addBtn.onclick = () => app.add();
subBtn.onclick = () => app.sub();
</script>
</body>
// minivue.js
class MiniVue {
constructor(options) {
// 初始化
this.$options = options;
this.$el = document.querySelector(el);
this.init();
}
init() {
// 初始化data
this.initData();
// 初始化methods
this.initMethods(this);
// 挂载真实dom
this.$mount();
}
initData() {}
initMethods() {}
$mount() {}
}
初始化数据对象data
数据代理
将数据对象data响应式化之前,需要将数据存放至实例的_data
属性中,使用proxy
方法将_data
内的数据代理到MiniVue
实例的第一层。
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: () => {},
set: () => {},
};
initData() {
let { data } = this.$options;
data = this._data =
typeof data === 'function' ? data.call(this, this) : data || {};
const keys = Object.keys(data);
let i = keys.length;
while (i--) {
// 将data属性代理至第一层
this.proxy(this, '_data', keys[i]);
}
// 响应式化
observer(data);
}
proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
设置响应式对象 - Observer
data
会通过observer
方法将自身修改为响应式对象,方法内部会实例化一个Observer
类,最终data会利用这个类将数自身修改为响应式对象,这是所有流程的基础。
function observer(data) {
if (Array.isArray(data) || _toString.call(data) === '[object Object]') {
return new Observer(data);
}
}
// 依赖
class Observer {
constructor(value) {
// 实例化依赖存储器
this.dep = new Dep();
// 为value添加__ob__属性
Object.defineProperty(value, '__ob__', {
value: this,
enumerable: false,
writable: true,
configurable: true,
});
if (Array.isArray(value)) {
// 略过,后续择机补充
} else {
// value是对象类型时,遍历对象的key
const keys = Object.keys(value);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// 为所有属性重写getter setter
defineReactive(value, key);
}
}
}
}
订阅者 - Watcher
我们可以理解为一个Watcher
实例就是一个依赖(订阅者),Watcher内部记录了这个订阅者监听的状态和更新操作的方法。 当某个Watcher实例的更新操作用于模板渲染时其被称为渲染Watcher。
class Watcher {
constructor(vm, expOrFn) {
vm._watcher = this;
this.vm = vm;
this.uid = uid_watcher++;
this.deps = [];
this.depIds = new Set();
this.getter = expOrFn;
// Watcher.prototype.get的调用会进行状态的更新。
this.get();
}
get() {
// 执行getter方法 状态的更新
}
update() {
this.run();
}
run() {
const value = this.get();
}
}
那么哪个时间点会实例化Watcher
并更新数据状态呢?1. 在$mount
方法调用时会实例化一个渲染Watcher
,而实例化后最终会调用this.getter()
,也就是vm._update(vm._render())
去更新页面。2. 用户定义的computed
等数据读取时也会实例化Watcher。这里我们只简单介绍第一情况——挂载真实dom
时创建Watcher
的流程,可以对vm._update(vm._render())
代码做一下精简。省去创建虚拟dom
和patch
打补丁的复杂操作。详见updateDom
方法。
class MiniVue {
$mount() {
function _createUpdateDomFn(root) {
// 缓存模板
const cacheTemplate = root.$el.innerHTML;
function update(innerHtml) {
root.$el.innerHTML = innerHtml;
}
return function (vm) {
let innerHtml = cacheTemplate;
const matched = cacheTemplate.matchAll(/\{\{([^}]+)\}\}/g);
let current;
while ((current = matched.next().value)) {
// 这里访问到了响应式数据,将进行依赖收集
const cur = current[1].trim();
const val = vm[cur] || '';
innerHtml = innerHtml.replace(current[0], val);
}
update(innerHtml);
};
}
const updateDom = _createUpdateDomFn(this);
// 传入更新方法
new Watcher(this, updateDom);
}
}
订阅者(依赖)管理 - Dep
Watcher
如果被理解为每个数据的订阅者(依赖),那么Dep
就可以理解为订阅者(依赖)的管理器。所有响应式数据都可以在渲染时使用,也可以在计算属性中使用,相对的每个数据的Watcher也会很多,那么如何在数据更新时能够通知到每个Watcher进行更新呢?这时候就要用到Dep来进行通知管理了。而Dep这个类只需要做两件事情,依赖的收集,派发依赖的更新。
let uid = 0;
// 依赖收集器
class Dep {
constructor() {
// 唯一标识,为防止相同dep重复收集
this.id = uid++;
// sub 存储该Dep收集器所属数据的依赖watcher
this.subs = [];
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 使用当前watcher进行收集
Dep.target.addDep(this);
}
}
notify() {
// 派发更新
const subs = this.subs.slice(0);
for (let i = 0; i < subs.length; i++) {
// 遍历dep中的依赖,对每个依赖执行更新操作
const sub = subs[i];
sub.update();
}
}
}
响应式化过程 defineReactive
我们来看看数据拦截的过程,前面Observer实例化最终会调用defineReactive
方法重写传入属性的getter、setter,方法内首先实例化了一个依赖管理器Dep
,重写的getter方法中对依赖进行了收集,也就是调用dep.depend()
方法。setter阶段,在新旧值比较出不同后会调用dep.notify()
方法让依赖管理器派发更新。
function defineReactive(obj, key) {
// 实例化该属性的依赖管理器
const dep = new Dep();
let val = obj[key];
let childObj = observer(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
// 如果依赖存储器原型的target(渲染watcher)有值,则进行依赖收集
dep.depend();
if (childObj) {
childOb.dep.depend();
}
}
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
val = newVal;
childObj = observer(newVal);
// 派发更新
dep.notify();
},
});
}
回过头看watcher
,实例化watcher
时会执行get
将自身实例push到全局数组targetStack
中,并执行getter更新状态。此时如果是渲染watcher,源码中则会执行updateComponents
函数,即调用vm._update(vm._render())
进行真实dom的更新,在执行vm._render()
创建VNode tree
过程中便有可能会访问到响应式数据,响应式数据重写的getter
方法便会执行,并开始进行依赖收集。此时targetStack
中存放了当前的watcher
依赖(订阅者),Dep便能将之收集并相互建立关系。在响应式数据改变后派发watcher
执行更新操作。
let targetStack = [];
Dep.target = null;
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
let uid_watcher = 0;
class Watcher {
constructor(vm, expOrFn) {
vm._watcher = this;
this.vm = vm;
this.uid = uid_watcher++;
this.deps = [];
this.depIds = new Set();
this.getter = expOrFn;
this.get();
}
get() {
// 将自身实例push到全局数组targetStack
pushTarget(this);
let value;
const vm = this.vm;
try {
// 依赖收集发生在这里
value = this.getter.call(vm, vm);
} catch (err) {
throw err;
} finally {
// this.cleanupDeps();
popTarget();
}
}
addDep(dep) {
// 根据dep的id 防止当前watcher被同一Dep多次收集
if (!this.depIds.has(dep.id)) {
// watcher收集dep
this.deps.push(dep);
this.depIds.add(dep.id);
// dep收集watcher
dep.addSub(this);
}
}
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
dep.removeSub(this);
}
}
update() {
this.run();
}
run() {
const value = this.get();
}
}
初始化methods方法对象
methods对象初始化就非常简单了,和data初始化时一样的思路,唯一不同的是我们需要将vm实例绑定作为methods的this上下文,这样在methods
内部访问this
时能够拿到MiniVue
实例
class MiniVue {
initMethods(vm) {
let { methods } = this.$options;
this._methods = methods;
const keys = Object.keys(methods);
let i = keys.length;
while (i--) {
this._methods[keys[i]] = this._methods[keys[i]].bind(vm);
this.proxy(this, '_methods', keys[i]);
}
}
proxy(target, sourceKey, key) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
}
源码详见
vuejs2.7.14
源码地址
本文完整源码