Vue学习笔记 – Vue的响应式原理
今天通过王红元老师的教学视频和一些博主的技术分享,学习了Vue的响应式原理,话不多说直接进入正题
这是Vue官网中提供的响应式原理示意图,总结起来我们最常见到的响应式原理的答案就是:
使用Object.defineProperty将所有属性使用setter和getter进行劫持,在读取数据和写入数据时进行拦截处理
这是我自己总结的响应式流程:
然而这只是响应式原理中的一小部分,下面时是我用自己的话总结的响应式原理:
Vue的响应式原理是通过使用订阅者-发布者模式,配合Object.defineProperty将所有data中的属性进行写入与 获取的劫持,将每一个属性对应一个Dep对象,解析并存储某一属性所对应的vm实例,当属性值发生改变时,Watcher会通知该属性所关联的所有vm实例进行数据更新,者就是响应式的基本原理
一、什么是Object.defineProperty
Object.defineProperty(obj, prop, descriptor)
是js对象操作的常用api之一,他对应的三个参数分别是: 需要被定义属性的对象, 要定义或修改的属性,数据描述符或存取描述符
configurable
当且仅当该属性的 configurable
键值为 true
时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除
enumerable
当且仅当该属性的 enumerable
键值为 true
时,该属性才会出现在对象的枚举属性中。
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。
writable
当且仅当该属性的 writable
键值为 true
时,属性的值,也就是上面的 value
,才能被赋值运算符改变。
get
属性的 getter 函数,如果没有 getter,则为 undefined
。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
set
属性的 setter 函数,如果没有 setter,则为 undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this
对象。
二、什么是订阅者-发布者模式
订阅者-发布者模式简单来说就是:
当我们使用微信关注公众号后,公众号会定期想你推送新消息,在这个场景下我们就是订阅者而公众号就是发布者
我们先定义一个发布者的对象:
class Dep {
constructor() {
//订阅者
this.subscribs = []
}
addSub(sub) {
this.subscribs.push(sub)
}
notify() {
//对订阅者进行遍历,逐一通知修改
this.subscribs.forEach(item => {
item.update()
})
}
}
接下来我们定义数个订阅者
var sub1 = {
update() {
console.log('sub1发生改变')
}
}
var sub2 = {
update() {
console.log('sub2发生改变')
}
}
var sub3 = {
update() {
console.log('sub3发生改变')
}
}
定义之后我们将所有订阅者存入发布者对象中,然后进行发布
var dep = new Dep();
dep.add(sub1);
dep.add(sub2);
dep.add(sub3);
dep.notify();
这样dep就会通知已经进行订阅的用户进行数据修改并更新视图,也就完成了发布者的基本功能
这里我们直接定义一个订阅者:
class Watcher {
constructor(node, name, vm) {
this.node = node;
this.name = name;
this.vm = vm;
Dep.target = this;
this.update();
Dep.target = null;
}
update() {
this.node.nodeValue = this.vm[this.name] //get
}
}
当数据发生改变时,直接使用new Watcher(node, name, this.vm)
对数据进行修改
三、响应式原理
在Vue中我们应该先定义一个Vue的对象
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
this.$el = options.el;
// 先将data挂载到响应式系统中
new Observe(this.$data);
}
}
创建发布者对象
class Dep {
constructor() {
//订阅者
this.subscribs = []
}
addSub(sub) {
this.subscribs.push(sub)
}
notify() {
//对订阅者进行遍历,逐一通知修改
this.subscribs.forEach(item => {
item.update()
})
}
}
定义一个Observe对象,对data中的属性进行劫持
class Observe {
constructor(data) {
this.data = data;
Object.keys(this.data).forEach(key => {
this.defineReactive(this.data, key, data[key])
})
}
defineReactive(data, key, val) {
const dep = new Dep();
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
set(newValue) {
if(newValue === val) {
return
}
val = newValue;
//通知修改属性
dep.notify()
},
get() {
if(Dep.target) {
dep.addSub(Dep.target)
}
return val
}
})
}
}
将data中的所有属性使用proxy进行代理
class Vue {
constructor(options) {
this.$options = options;
this.$el = options.el;
this.$data = options.data;
//将数据挂载到响应式系统
new Observe(this.$data)
//将data代理到this中
Object.keys(this.$data).forEach(key => {
this._proxy(key)
})
}
_proxy(key) {
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
set(newValue) {
this.$data[key] = newValue
},
get() {
return this.$data[key]
}
})
}
}
配置订阅者对象
class Watcher {
constructor(node, name, vm) { //节点, 属性名, Vue实例
this.node = node;
this.name = name;
this.vm = vm;
Dep.target = this;
this.update();
Dep.target = null
}
update() {
//修改视图数据
this.node.nodeValue = this.vm[this.name]
}
}
配置正则处理规则
const reg = /\{\{(.*)\}\}/; //{{}}
配置视图解析对象
class Compiler {
constructor(el, vm) {
this.el = document.querySelector(el);
this.vm = vm;
this.frag = this._createFragment();
//视图解析创建虚拟节点时会将原节点删除,因此需要重新加入节点
this.el.appendChild(this.frag)
}
//创建虚拟节点并对原节点进行解析处理
_createFragment() {
//创建虚拟dom根节点
const frag = document.createDocumentFragment()
let child;
//循环搜索节点,解析视图
while(child = this.el.firstChild) {
this._compile(child);
frag.appendChild(child)
}
return frag
}
//对节点进行解析,并且添加监听和视图修改操作
_compile(node) {
console.log(node);
if(node.nodeType === 1){ //标签节点
const attrs = node.attributes;
if(attrs.hasOwnProperty('v-model')) {
const name = attrs['v-model'].nodeValue;
node.addEventListener('input', e => {
this.vm[name] = e.target.value;
})
}
}
if(node.nodeType === 3) {
console.log("node: " + node.nodeValue);
console.log(reg.test(node.nodeValue));
if (reg.test(node.nodeValue)) {
const name = RegExp.$1.trim()
console.log('name: ' + name);
new Watcher(node, name, this.vm)
}
}
}
}
完善Vue对象的创建过程
class Vue {
constructor(options) {
this.$options = options;
this.$el = options.el;
this.$data = options.data;
//将数据挂载到响应式系统
new Observe(this.$data);
// 将data进行代理处理
Object.keys(this.$data).forEach(key => {
this._proxy(key)
});
// 将el进行解析并添加订阅者
new Compiler(this.$el, this)
}
总结
总结起来响应式的过程大致如下:
- 定义Vue对象,将Vue对象中的data属性值进行响应式挂载, Observe
- 在Observe中对每一个属性进行劫持处理,添加setter和getter方法,,在watcher的 update()方法被调用时,会自动执行getter方法,此时将这个Watcher对象(即订阅者)添加到发布者中
- 每当数据变化时就会触发该属性对应的Dep对象中的notify()方法,通知所有成员进行数据更新
- 订阅者此时触发update()方法,改变了Watcher中对应node的nodeValue,也就是视图显示的数据
- 就这样形成了数据的双向绑定,即视图值修改,数据值即修改,反之亦然
写在后面
所有文章收发与我的个人博客幻尘の屋