前言
Vue (读音 /vju/,类似于 view)
,是中国的大神尤雨溪开发的,为数不多的国人开发的世界顶级开源软件,
是一套用于构建用户界面的渐进式框架。Vue 被设计为可以自底向上逐层应用。MVVM响应式编程模型,避免直接操作DOM , 降低DOM操作的复杂性。
讲到Vue的响应式原理,我们可以从它的兼容性说起,Vue不支持IE8以下版本的浏览器,因为Vue2.x是基于Object.defineProperty来实现数据响应的,而 Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器的原因;Vue通过Object.defineProperty的 getter/setter 对收集的依赖项进行监听,在属性被访问和修改时通知变化,进而更新视图数据
上述是官网的图,看着不是很清楚,下面具体分析一下,实际上Vue数据响应式变化主要涉及 Observer, Watcher , Dep 这三个类;因此要弄清Vue响应式变化需要明白这个三个类之间是如何运作联系的;以及它们的原理,负责的逻辑操作。那么我们从一个简单的Vue实例的代码来分析Vue的响应式原理,下图是一个简易版分析
观察者模式VS发布订阅模式
观察者模式
class Subject {
constructor() {
this.observers = [];
}
add(observer) {
this.observers.push(observer);
}
notify(...args) {
this.observers.forEach(observer => observer.update(...args));
}
}
class Observer {
update(...args) {
console.log('do something');
}
}
const sub = new Subject(); /* 系统 */
sub.add(new Observer()); /* 张三点了预约 */
sub.add(new Observer()); /* 李四点了预约 */
sub.notify(); /* 双十一了,通知所有点了预约的人来抢货了 */
发布订阅模式
class SubPub {
constructor() {
this.observers = {};
}
subscribe(topic, callback) {
if (!this.observers[topic]) {
this.observers[topic] = [];
}
this.observers[topic].push(callback);
}
publish(topic, ...args) {
let callbacks = this.observers[topic] || [];
callbacks.forEach(cb => cb.call(this, ...args));
}
}
const subject = new SubPub(); /* 中介 */
subject.subscribe('两室一厅', () => { console.log('张三'); }); /* 张三想买一个两室一厅的房子 */
subject.subscribe('三室一厅', () => { console.log('李四') }); /* 李四想买一个三室一厅的房子 */
subject.subscribe('两室一厅', () => { console.log('王五'); }); /* 王五想买一个两室一厅的房子 */
subject.publish('两室一厅'); /* 当出现一个两室一厅的房子时,中介会通知张三和王五来看房 */
区别
- 观察者通信双方是明确知道对方的存在,发布订阅是通过一个消息中心进行代理。
- 观察者的代码是高耦合的。
- 观察者的代码是同步的,而发布订阅由于存在一个消息中心,所以多是异步的。
从我们实现代码也可以看出来,其实观察者和发布订阅最大的区别就是消息的两个主体是否知道对方,是否通过代理的方式进行消息通信(即有无中间商赚差价)
实现示例
<div id="app">
<h3>{{ msg }}</h3>
<p>{{ count }}</p>
<h1>v-text</h1>
<p v-text="msg"></p>
<input type="text" v-model="count">
<button type="button" v-on:click="increase">add+</button>
<button type="button" v-on:click="changeMessage">change message!</button>
<button type="button" v-on:click="recoverMessage">recoverMessage!</button>
</div>
源码实现
<script>
function __isNaN(a, b) {
return Number.isNaN(a) && Number.isNaN(b);
}
class miniVue {
constructor(options = {}) {
//在miniVue构造函数的内部
//保存根元素,能简便就尽量简便,不考虑数组情况
this.$options = options;
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
this.$data = options.data;
this.$methods = options.methods;
this.proxy(this.$data);
// observer,拦截this.$data
new Observer(this.$data);
new Compiler(this);
}
//proxy代理实例上的data对象
proxy(data) {
// 因为我们是代理每一个属性,所以我们需要将所有属性拿到
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(newValue) {
//这里我们需要判断一下如果值没有做改变就不用赋值,需要排除NaN的情况(// NaN !== NaN)
if (newValue === data[key] || __isNaN(newValue, data[key])) return;
data[key] = newValue;
}
})
})
}
}
class Dep {
constructor() {
//后续代码
this.deps = new Set();
}
add(dep) {
//判断dep是否存在并且是否存在update方法,然后添加到存储的依赖数据结构中
if (dep && dep.update) this.deps.add(dep);
}
notify() {
// 发布通知无非是遍历一道dep,然后调用每一个dep的update方法,使得每一个依赖都会进行更新
this.deps.forEach(dep => dep.update())
}
}
class Watcher {
//3个参数,当前组件实例vm,state也就是数据以及一个回调函数,或者叫处理器
constructor(vm, key, cb) {
//后续代码
//构造函数内部
this.vm = vm;
this.key = key;
this.cb = cb;
//依赖类
Dep.target = this;
// 我们用一个变量来存储旧值,也就是未变更之前的值
this.__old = vm[key];
Dep.target = null;
}
update() {
//获取新的值
let newValue = this.vm[this.key];
//与旧值做比较,如果没有改变就无需执行下一步
if (newValue === this.__old || __isNaN(newValue, this.__old)) return;
//把新的值回调出去
this.cb(newValue);
//执行完之后,需要更新一下旧值的存储
this.__old = newValue;
}
}
class Observer {
constructor(data) {
//后续实现
this.walk(data);
}
//再次申明,不考虑数组,只考虑对象
walk(data) {
if (typeof data !== 'object' || !data) return;
// 数据的每一个属性都调用定义响应式对象的方法
Object.keys(data).forEach(key => this.defineReactive(data, key, data[key]));
}
defineReactive(data, key, value) {
// 获取当前this,以避免后续用vm的时候,this指向不对
let vm = this;
// 递归调用walk方法,因为对象里面还有可能是对象
this.walk(value);
//实例化收集依赖的类
let dep = new Dep();
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 收集依赖,依赖存在Dep类上
Dep.target && dep.add(Dep.target);
return value;
},
set(newValue) {
// 这里也判断一下
if (newValue === value || __isNaN(value, newValue)) return;
// 否则改变值
value = newValue;
// newValue也有可能是对象,所以递归
vm.walk(newValue);
// 通知Dep类
dep.notify();
}
})
}
}
class Compiler {
constructor(vm) {
//后续代码
//根元素
this.el = vm.$el;
//当前组件实例
this.vm = vm;
//事件方法
this.methods = vm.$methods;
//调用编译函数开始编译
this.compile(vm.$el);
}
compile(el) {
//拿到所有子节点(包含文本节点)
let childNodes = el.childNodes;
//转成数组
Array.from(childNodes).forEach(node => {
//判断是文本节点还是元素节点分别执行不同的编译方法
if (this.isTextNode(node)) {
this.compileText(node);
} else if (this.isElementNode(node)) {
this.compileElement(node);
}
//递归判断node下是否还含有子节点,如果有的话继续编译
if (node.childNodes && node.childNodes.length) this.compile(node);
})
}
compileText(node) {
let reg = /\{\{(.+?)\}\}/
let value = node.textContent;
if (reg.test(value)) {
let key = RegExp.$1.trim();
node.textContent = value.replace(reg, this.vm[key]);
new Watcher(this.vm, key, val => {
node.textContent = val;
})
}
}
compileElement(node) {
//指令不就是一堆属性吗,所以我们只需要获取属性即可
let attrs = node.attributes;
if (attrs.length) {
Array.from(attrs).forEach(attr => {
//这里由于我们拿到的attributes可能包含不是指令的属性,所以我们需要先做一次判断
let attrName = attr.name;
if (this.isDirective(attrName)) {
attrName = attrName.indexOf(":") > -1 ? attrName.substr(5) : attrName.substr(2);
let key = attr.value;
this.update(node, key, attrName, this.vm[key]);
}
})
}
}
update(node, key, attrName, value) {
//后续代码
if (attrName === 'text') {
//执行v-text的操作
node.textContent = value;
new Watcher(this.vm, key, newValue => node.textContent = newValue)
} else if (attrName === 'model') {
//执行v-model的操作
node.value = value;
new Watcher(this.vm, key, newValue => node.value = newValue);
node.addEventListener('input', (e) => {
this.vm[key] = node.value;
})
} else if (attrName === 'click') {
//执行v-on:click的操作
node.addEventListener(attrName, this.methods[key].bind(this.vm));
}
}
isDirective(dir) {
return dir.startsWith('v-');
}
isTextNode(node) {
return node.nodeType === 3;
}
isElementNode(node) {
return node.nodeType === 1;
}
}
</script>
<script>
const app = new miniVue({
el: "#app",
data: {
msg: "hello,mini vue.js",
count: 666
},
methods: {
increase() {
this.count++;
},
changeMessage() {
this.msg = "hello,eveningwater!";
},
recoverMessage() {
console.log(this)
this.msg = "hello,mini vue.js";
}
}
});
</script>
总结
回顾一下,Vue响应式原理的核心就是Observer、Dep、Watcher。
Observer中进行响应式的绑定,在数据被读的时候,触发get方法,执行Dep来收集依赖,也就是收集Watcher。
在数据被改的时候,触发set方法,通过对应的所有依赖(Watcher),去执行更新。比如watch和computed就执行开发者自定义的回调方法。