Vue响应式原理
利用Object.defineProperty()劫持data数据,里面每个属性都定义了get和set方法 监听属性的变化,
依赖收集:每个属性也都有个数组 保存着谁(视图watcher)依赖了它,当获取属性触发get时,收集了依赖,
依赖更新:当属性变化触发set函数时,通知依赖,通知视图(watcher),视图(watcher)开始更新。
一、 Object.defineProperty()
Object.defineProperty():在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
get 值是一个函数,当属性被访问时,会触发 get 函数
set 值同样是一个函数,当属性被赋值时,会触发 set 函数
Vue 是怎么知道数据被调用或改变的?
Vue对data对象中的每个属性进行了get、set监听,当属性被调用时触发get方法,当属性被改变时触发set方法,所以Vue 能知道数据被调用或改变。
代码如下:
class KVue {
constructor(options) {
this.$options = options;
if(this.$options == undefined) {
return;
}
this.$data = this.$options.data; //把data赋值给this,在创建的实例中才能获取到data
this.observe(this.$data);
}
// 操作data:如果data是对象,则对属性进行遍历
observe(value) {
if(!value || typeof value !== 'object') {
return;
}
Object.keys(value).forEach((key) => {
this.defineReactive(value, key, value[key]);
})
}
// 操作属性:用Object.defineProperty()监听属性
defineReactive(obj, key, val) {
this.observe(val);
Object.defineProperty(obj, key, {
get () {
return val;
},
set (newValue) {
if(val === newValue) {
return;
}
val = newValue;
console.log(val);
}
})
}
}
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="defineProperty.js"></script>
<script type="text/babel">
let kvue = new KVue({
data: {
name: 'fndjf',
list: {
bar: 'bar'
}
}
})
console.log(kvue)
console.log(kvue.$data.name)
kvue.$data.name = '1dgs';
console.log(kvue.$data.name)
console.log('--------------------');
console.log(kvue.$data.list.bar);
kvue.$data.list.bar = 'dfmd';
console.log(kvue.$data.list);
let kvue1 = new KVue();
</script>
</body>
</html>
二、依赖收集、更新
依赖收集:data中每个属性都会有一个数组作为依赖收集器,当页面使用某个属性(get)时,页面的watcher就会被放到对应属性的依赖收集器中。
依赖更新:通知属性依赖收集器中的watcher进行更新
代码如下:
class KVue {
constructor(options) {
this.$options = options;
if(this.$options == undefined) {
return;
}
this.$data = this.$options.data; //把data赋值给this,在创建的实例中才能获取到data
this.observe(this.$data);
// 模拟watcher创建
new Watcher(); //已将实例赋值给Dep.target
this.$data.name;// 将Dep.target插入name属性中的dep
new Watcher();//已将实例赋值给Dep.target,覆盖了之前的Dep.target
this.$data.list.bar; //将Dep.target插入list.bar属性中的dep
}
// 操作data
observe(value) {
if(!value || typeof value !== 'object') {
return;
}
Object.keys(value).forEach((key) => {
this.defineReactive(value, key, value[key]);
})
}
// 操作key
defineReactive(obj, key, val) {
// 递归监听data属性中的属性
this.observe(val);
// 初始化一个属性中的依赖
const dep = new Dep();
Object.defineProperty(obj, key, {
get () {
// Dep.target是调用该属性的页面的Watcher,添加到该属性的依赖中
Dep.target && dep.addDep(Dep.target);
return val;
},
set (newValue) {
if(val === newValue) {
return;
}
val = newValue;
console.log(val);
// 通知依赖更新
dep.notify();
}
})
}
}
// Dep:依赖
class Dep {
constructor() {
// 存放若干依赖(watcher)
this.$deps = [];
}
// 添加依赖
addDep(dep) {
this.$deps.push(dep);
}
// 通知依赖数组中的每个watcher更新
notify() {
this.$deps.forEach(dep => dep.update(dep));
}
}
// Watcher:观察者,做具体更新
class Watcher {
constructor() {
// 将当前watcher实例赋值给Dep的静态属性,即后一个watcher实例会覆盖前一个watcher实例
Dep.target = this;
}
update(dep) {
console.log(dep);
console.log('属性更新了');
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="defineProperty.js"></script>
<script type="text/babel">
let kvue = new KVue({
data: {
name: 'fndjf',
list: {
bar: 'bar'
}
}
})
console.log(kvue)
console.log(kvue.$data.name)
kvue.$data.name = '1dgs';
console.log(kvue.$data.name)
console.log('--------------------');
console.log(kvue.$data.list.bar);
kvue.$data.list.bar = 'dfmd';
console.log(kvue.$data.list);
</script>
</body>
</html>
三、总结
- data中每个属性都有个数组作为依赖收集器,收集使用该属性的页面的watcher
- Object.defineProperty() - get ,用于 依赖收集
- Object.defineProperty() - set,用于 依赖更新
- watcher 可用于视图更新
四、另一写法
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
// 1.实现整体架构(Vue类、Watcher类),用到了订阅发布者设计模式
// 2.实现MVVM中的M到V,把数据绑定到视图
// 3.实现V-M,当文本框输入文本时,触发更新模型中的数据,同时更新相应的视图
class Vue {
constructor(options) {
this.$data = options.data; //获取数据
this.$el = document.querySelector(options.el); //获取元素#app
this._directive = {}; //存放订阅者信息
this.Observer(this.$data);
this.Compile(this.$el);
}
// 劫持数据:
Observer(data) {
for(let key in data) {
this._directive[key] = [];
let val = data[key];
let watcher = this._directive[key];
console.log(watcher);
Object.defineProperty(data, key, {
get() {
return val;
},
set(newVal) {
if(val !== newVal) {
val = newVal;
// 更新视图
watcher.forEach(element => {
element.update();
});
}
}
})
}
}
// 解析指令:收集依赖、更新视图、订阅
Compile(el) {
let node = el.children;
console.log(node);
for(let i = 0; i < node.length; i++) {
// let node = node[i];
if(node[i].children.length) {
this.Compile(node[i]);
}
if(node[i].hasAttribute('v-text')) {
let attrVal = node[i].getAttribute('v-text'); // myText myBox
this._directive[attrVal].push(new Watcher(node[i], this, attrVal, "innerHTML"));
console.log(this._directive);
}
if(node[i].hasAttribute('v-model')) {
let attrVal = node[i].getAttribute('v-model'); // myText myBox
this._directive[attrVal].push(new Watcher(node[i], this, attrVal, "innerHTML"));
// 文本框出现变化时更新模型
node[i].addEventListener('input', ()=>{
this.$data[attrVal] = node[i].value;
})
}
}
}
}
// 订阅者
class Watcher { //主要功能是更新视图
constructor(el, vm, exp, attr) {
this.el = el;
this.vm = vm;
this.exp = exp;
this.attr = attr;
this.update();
}
update() {
this.el[this.attr] = this.vm.$data[this.exp];
// div.innerHTML = vm.$data.myText
};
}
</script>
</head>
<body>
<div id="app">
<div>
<div v-text="myText"></div>
<div v-text="myBox"></div>
<input type="text" v-model="myText">
<input type="text" v-model="myBox">
</div>
</div>
<script>
const app = new Vue({
el:'#app',
data: {
myText: 'textaaaaaa',
myBox: 'myBoxaaaaa'
}
})
</script>
</body>
</html>