Vue的响应式原理
Vue采用的是数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板命令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化=>视图更新;视图交互变化(input)=>数据model变更的双向绑定效果。
实现
要实现双向数据绑定,就必须实现以下几点:
- 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新的值并通知订阅者。
- 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
- 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定相应的回调函数,从而更新视图。
初始化MVue.js
class MVue {
constructor(options) {
//将属性绑定
this.$el = options.el;
this.$data = options.data;
//将 options 参数保存,后面处理数据时需要用到
this.$options = options;
//对 el 进行判断,有值才进行操作
if (this.$el) {
//1.实现一个数据观察者
//2.实现一个指令解析器
new Compile(this.$el, this);
}
}
}
实现指令解析器Compile
实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
class Compile {
/**
*
* @param {传入的元素参数} el
* @param {MVue实例参数} vm
*/
constructor(el, vm) {
//判断 el 是否是一个元素节点,如果是,直接赋值,如果不是,获取后再赋值
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
//1.获取文档碎片对象,放入缓存中,减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
//2.编译模板
this.compile(fragment);
//3.将子节点添加到根节点
this.el.appendChild(fragment);
}
/**
* 编译器
* @param {} fragment
*/
compile(fragment) {
//1.获取子节点
const childNodes = fragment.childNodes;
//遍历所有子节点,对元素节点和文本节点分别进行处理
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
//元素节点,编译元素节点
this.compileElement(child);
} else {
//文本节点,编译文本节点
this.compileText(child);
}
//对于元素节点可能还存在孩子节点,需要进行递归遍历
if (child.childNodes && child.childNodes.length) {
this.compile(child);
}
})
}
/**
* 判断是否是通过 : 绑定
* @param {:src等} attrName
*/
isBindName(attrName) {
return attrName.startsWith(':');
}
/**
* 判断是否是通过 @ 绑定
* @param {@click等} attrName
*/
isEventName(attrName) {
return attrName.startsWith('@');
}
/**
* 判断是否是指令
* @param {} attrName
*/
isDirective(attrName) {
return attrName.startsWith('v-');
}
/**
* 判断是否是元素节点
* @param {} node
*/
isElementNode(node) {
return node.nodeType === 1;
}
}
编译元素节点
/**
* 编译元素节点
* @param {} node
*/
compileElement(node) {
//获取节点的所有属性
[...node.attributes].forEach(attr => {
//对 v-text="msg" 等进行解构取值
const {
name,
value
} = attr;
//对 name 进行判断,是否是指令
//v-text v-html v-model v-on:click v-bind:src
if (this.isDirective(name)) {
//分割,获取指令 text、html、model、on:click、bind:src
const [, directive] = name.split('-');
//对 on:click 进行处理
const [dirName, eventName] = directive.split(':');
//对不同的指令分别进行处理
//参数: 当前节点 指令绑定的值(v-text='msg')msg等 MVue实例 v-on指令对应的事件名
//更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
//移除标签上的指令
node.removeAttribute('v-' + directive);
} else if (this.isEventName(name)) {
//处理 @ 绑定方式
let [, eventName] = name.split('@');
compileUtil['on'](node, value, this.vm, eventName);
} else if (this.isBindName(name)) {
//处理 : 绑定方式
let [, eventName] = name.split(':');
compileUtil['bind'](node, value, this.vm, eventName);
}
})
}
编译文本节点
/**
* 编译文本节点
* @param {} node
*/
compileText(node) {
//正则匹配 {{}}
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](node, content, this.vm);
}
}
CompileUtil
const compileUtil = {
/**
* 对不同的书写形式获取值进行处理
* @param {} expr
* @param {} vm
*/
getValue(expr, vm) {
//注意 reduce 方法的原理,它会将取到的值返回给data,然后再次遍历取data中对应的属性的值
//第一次 data[person] 取到person对象 返回给data
//第二次 person[name] 取到name值 返回
return expr.split('.').reduce((data, currentValue) => {
return data[currentValue]
}, vm.$data); //将MVue实例中的data作为参数传入
},
/**
* 对v-text指令进行处理
* @param {} node
* @param {指令中绑定的msg等} expr
* @param {} vm
*/
text(node, expr, vm) {
//直接取值对于 <div v-text='person.name'></div> 这种形式的来说不可行
//const value = vm.$data[expr];
//通过函数对不同书写形式进行处理,获取值
//const value = this.getValue(expr, vm);
//对于文本中的 {{}} 需要重新进行处理
let value;
if (expr.indexOf('{{') !== -1) {
//replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
//用函数中返回的值替换掉 expr 中原来的值 替换掉 {{person.name}}
//函数中的 args 参数就是 expr
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
})
} else {
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
/**
* 对v-html指令进行处理
* @param {} node
* @param {} expr
* @param {} vm
*/
html(node, expr, vm) {
const value = this.getValue(expr, vm);
this.updater.htmlUpdater(node, value);
},
/**
* 对v-model指令进行处理
* @param {} node
* @param {} expr
* @param {} vm
*/
model(node, expr, vm) {
const value = this.getValue(expr, vm);
this.updater.modelUpdater(node, value);
},
/**
* 对v-on指令进行处理
* @param {} node
* @param {绑定的方法名} expr
* @param {} vm
* @param {事件名} eventName
*/
on(node, expr, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[expr];
//绑定this指向 绑定vm,阻止冒泡
node.addEventListener(eventName, fn.bind(vm), false);
},
/**
*
* @param {} node
* @param {属性值} expr
* @param {} vm
* @param {src等属性名} eventName
*/
bind(node, expr, vm, eventName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, value, eventName);
},
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, value, eventName) {
node.setAttribute(eventName, value);
}
}
}
优化编译
编译需要将根节点的所有子节点全部拿出来,然后进行替换,每次替换都会导致页面的回流与重绘,非常影响页面的性能。
在原生js中有个文档碎片,它能够将所有的子节点放入缓存中,当需要的时候直接从缓存中获取,减少页面的回流和重绘。
创建文档碎片
createDocumentFragment()方法,是用来创建一个虚拟的节点对象,节点对象包含所有属性和方法。或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。
当你想提取文档的一部分,改变,增加,或删除某些内容及插入到文档末尾可以使用createDocumentFragment() 方法。你也可以使用文档的文档对象来执行这些变化,但要防止文件结构被破坏,createDocumentFragment() 方法可以更安全改变文档的结构及节点。
语法:const f = document.createDocumentFragment();
/**
* 创建虚拟节点并返回,减少页面的回流与重绘
* @param {} el
*/
node2Fragment(el) {
const f = document.createDocumentFragment();
let firstChild;
//赋值并判断是否有值
while (firstChild = el.firstChild) {
/*
Node.appendChild() 方法将一个节点附加到指定父节点的子节点列表的末尾处。
如果将被插入的节点已经存在于当前文档的文档树中,那么 appendChild() 只会将
它从原先的位置移动到新的位置(不需要事先移除要移动的节点)。
如果某个节点已经拥有父节点,在被传递给此方法后,它首先会被移除,再被插入到新的位置。
*/
f.appendChild(firstChild);
}
return f;
}
实现Observer
通过Object.defineProperty()来劫持监听所有属性,当数据发生变化时通知Dep容器
class Observer {
constructor(data) {
this.observe(data);
}
/**
* 监听数据
* @param {} data
*/
observe(data) {
if (data && typeof data === 'object') {
//获取data中属性的键
//获取的数据中可能还有对象,遍历判断
/*Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,
数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。*/
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
}
/**
* 递归遍历,对所有数据进行监听
* @param {} obj
* @param {} key
* @param {} value
*/
defineReactive(obj, key, value) {
//递归遍历
this.observe(value);
//劫持数据时创建 Dep 和 Observer 进行关联
const dep = new Dep();
//数据劫持
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
//初始化的时候调用get()
get() {
//数据发生变化时,向 Dep 中添加订阅者
Dep.target && dep.addSub(Dep.target);
return value;
},
//通过箭头函数处理this指向问题
set: (newValue) => {
//对新值进行监听,否则更改后的新值将不能被监听到
this.observe(newValue)
//对新值进行判断
if (newValue != value) {
value = newValue;
}
//告诉Dep通知数据发生变化
dep.notify();
}
});
}
}
实现Dep
Dep容器主要有两个作用:
- 将所有订阅者进行添加
- 数据发生变化时,通知所有订阅者
class Dep {
//1.添加所有订阅者
//2.数据发生变化时,通知所有订阅者
constructor() {
//用于保存所有订阅者
this.subs = [];
}
/**
* 添加订阅者
* @param {} watcher
*/
addSub(watcher) {
this.subs.push(watcher);
}
/**
* 通知所有订阅者
*/
notify() {
this.subs.forEach(w => w.update())
}
}
实现Watcher
Watcher是连接Observer和Compile的桥梁,能够订阅并收到每个属性变化的通知,执行指令绑定的相应的回调函数,更新视图。它所需要做的两个事情:
- 实例化时向Dep中添加自己
- 当属性的值发生变化时,dep.notify()通知Watcher调用update()方法,触发绑定的回调函数,更新视图
class Watcher {
/**
*
* @param {MVue实例} vm
* @param {指令中绑定的msg、person.name等} expr
* @param {回调函数} cb
*/
constructor(vm, expr, cb) {
//保存
this.vm = vm;
this.expr = expr;
this.cb = cb;
//保存旧值
this.oldValue = this.getOldValue();
}
getOldValue() {
//new MVue()时,创建Watcher对象,将当前观察者挂载到 Dep 上
Dep.target = this;
//通过 MVue.js 中的 compileUtil 获取值
const oldValue = compileUtil.getValue(this.expr, this.vm);
//获取完旧值之后进行销毁,否则再次更新数据时会导致创建多个订阅者
Dep.target = null;
return oldValue;
}
update() {
const newValue = compileUtil.getValue(this.expr, this.vm);
if (newValue != this.oldValue) {
//将新值回调返回
this.cb(newValue);
}
}
}
代理
在vue中可以直接通过this.person.name = 'Alex';来进行赋值,而我们现在是通过this.$data.person.name = 'Alex';来进行赋值,需要通过 Object.defineProperty()对this.$data进行代理来实现直接使用this调用。
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newValue) {
data[key] = newValue;
}
})
}
}
思考
在哪里绑定Watcher?在Watcher中怎样获取值?
在Compile中对各个指令进行编译,初始化视图时,创建Watcher进行绑定,并将vm(当前MVue实例)、expr(指令绑定的属性msg、person.name等)、回调函数作为参数传入。
在Watcher中将传入的参数获取、保存,调用compileUtil.getValue()获取值,并进行新值和旧值的判断,更新视图。
Dep怎样和Observer进行关联?
初始化时,Observer会对所有属性进行劫持监听,此时创建const dep = new Dep(),进行关联,同时在Dep的 Object.defineProperty()的get方法中将订阅者Watcher进行添加。
Dep怎样获取Watcher?
初始化时在Watcher的getOldValue()将当前的订阅者Watcher实例绑定到Dep上,Dep.target = this;,然后在Object.defineProperty中将Watcher添加dep.addSub(Dep.target);。
整合
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue的MVVM响应式原理</title>
</head>
<body>
<div id="app">
<h2>{{person.name}}---{{person.age}}</h2>
<h3>{{person.hobby}}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{msg}}</h3>
<div v-text='msg'></div>
<div v-text='person.name'></div>
<div v-html='htmlStr'></div>
<input type="text" v-model='msg'>
<button v-on:click='handleClick'>按钮on</button>
<button @click='handleClick'>按钮@</button>
<img style="width: 200px;" v-bind:src="imgSrc" alt="">
<img style="width: 100px;" :src="imgSrc" alt="">
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: "#app",
data: {
person: {
name: 'BooBoo',
age: 21,
hobby: 'music'
},
msg: 'MVVM实现原理',
htmlStr: '<h3>Hello Vue</h3>',
imgSrc: './img/img.jpg'
},
methods: {
handleClick() {
this.person.name = 'Alex';
// console.log(this.$data);
// this.$data.person.name = 'Alex';
}
},
})
</script>
</body>
</html>
MVue.js
const compileUtil = {
/**
* 设置值,视图=>数据=>视图
* @param {} expr
* @param {} vm
* @param {} inputValue
*/
setValue(expr, vm, inputValue) {
return expr.split('.').reduce((data, currentValue) => {
//直接赋值就行
data[currentValue] = inputValue;
}, vm.$data);
},
/**
* 对不同的书写形式获取值进行处理
* @param {} expr
* @param {} vm
*/
getValue(expr, vm) {
//注意 reduce 方法的原理,它会将取到的值返回给data,然后再次遍历取data中对应的属性的值
//第一次 data[person] 取到person对象 返回给data
//第二次 person[name] 取到name值 返回
return expr.split('.').reduce((data, currentValue) => {
return data[currentValue];
}, vm.$data); //将MVue实例中的data作为参数传入
},
/**
* 对 {{person.name}}-{{person.age}} 进行处理
* @param {} expr
* @param {} vm
*/
getContentVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm);
})
},
/**
* 对v-text指令进行处理
* @param {} node
* @param {指令中绑定的msg、person.name等} expr
* @param {} vm
*/
text(node, expr, vm) {
//直接取值对于 <div v-text='person.name'></div> 这种形式的来说不可行
//const value = vm.$data[expr];
//通过函数对不同书写形式进行处理,获取值
//const value = this.getValue(expr, vm);
//对于文本中的 {{}} 需要重新进行处理
let value;
if (expr.indexOf('{{') !== -1) {
//replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
//用函数中返回的值替换掉 expr 中原来的值 替换掉 {{person.name}}
//函数中的 args 参数就是 expr
// /\{\{(.+?)\}\}/g 匹配 {{}}
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//绑定订阅者,将来数据发生变化触发回调,进行更新
new Watcher(vm, args[1], () => { //expr传入的可能是 {{person.name}}-{{person.age}}
//更新视图
this.updater.textUpdater(node, this.getContentVal(expr, vm));
});
return this.getValue(args[1], vm);
})
} else {
value = this.getValue(expr, vm);
}
this.updater.textUpdater(node, value);
},
/**
* 对v-html指令进行处理
* @param {} node
* @param {} expr
* @param {} vm
*/
html(node, expr, vm) {
const value = this.getValue(expr, vm);
//绑定Watcher对数据进行监听
new Watcher(vm, expr, (newValue) => {
//更新数据
this.updater.htmlUpdater(node, newValue);
})
//更新数据
this.updater.htmlUpdater(node, value);
},
/**
* 对v-model指令进行处理
* @param {} node
* @param {} expr
* @param {} vm
*/
model(node, expr, vm) {
const value = this.getValue(expr, vm);
//绑定更新函数 数据=>视图
new Watcher(vm, expr, (newValue) => {
//更新数据
this.updater.modelUpdater(node, newValue);
})
// 视图=>数据=>视图
//绑定 input 事件
node.addEventListener('input', (e) => {
//设置值
this.setValue(expr, vm, e.target.value);
})
this.updater.modelUpdater(node, value);
},
/**
* 对v-on指令进行处理
* @param {} node
* @param {绑定的方法名} expr
* @param {} vm
* @param {事件名} eventName
*/
on(node, expr, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[expr];
//绑定this指向 绑定vm,阻止冒泡
node.addEventListener(eventName, fn.bind(vm), false);
},
/**
*
* @param {} node
* @param {属性值} expr
* @param {} vm
* @param {src等属性名} eventName
*/
bind(node, expr, vm, eventName) {
const value = this.getValue(expr, vm);
this.updater.bindUpdater(node, value, eventName);
},
updater: {
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
node.innerHTML = value;
},
modelUpdater(node, value) {
node.value = value;
},
bindUpdater(node, value, eventName) {
node.setAttribute(eventName, value);
}
}
}
class Compile {
/**
*
* @param {传入的元素} el
* @param {MVue实例} vm
*/
constructor(el, vm) {
//判断 el 是否是一个元素节点,如果是,直接赋值,如果不是,获取后再赋值
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
//1.获取文档碎片对象,放入缓存中,减少页面的回流和重绘
const fragment = this.node2Fragment(this.el);
//2.编译模板
this.compile(fragment);
//3.将子节点添加到根节点
this.el.appendChild(fragment);
}
/**
* 编译器
* @param {} fragment
*/
compile(fragment) {
//1.获取子节点
const childNodes = fragment.childNodes;
//遍历所有子节点,对元素节点和文本节点分别进行处理
[...childNodes].forEach(child => {
if (this.isElementNode(child)) {
//元素节点,编译元素节点
this.compileElement(child);
} else {
//文本节点,编译文本节点
this.compileText(child);
}
//对于元素节点可能还存在孩子节点,需要进行递归遍历
if (child.childNodes && child.childNodes.length) {
this.compile(child);
}
})
}
/**
* 编译元素节点
* @param {} node
*/
compileElement(node) {
//获取节点的所有属性
[...node.attributes].forEach(attr => {
//对 v-text="msg" 等进行解构取值
const {
name,
value
} = attr;
//对 name 进行判断,是否是指令
//v-text v-html v-model v-on:click v-bind:src
if (this.isDirective(name)) {
//分割,获取指令 text、html、model、on:click、bind:src
const [, directive] = name.split('-');
//对 on:click 进行处理
const [dirName, eventName] = directive.split(':');
//对不同的指令分别进行处理
//参数: 当前节点 指令绑定的值(v-text='msg')msg等 MVue实例 v-on指令对应的事件名
//更新数据,数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName);
//移除标签上的指令
node.removeAttribute('v-' + directive);
} else if (this.isEventName(name)) {
//处理 @ 绑定方式
let [, eventName] = name.split('@');
compileUtil['on'](node, value, this.vm, eventName);
} else if (this.isBindName(name)) {
//处理 : 绑定方式
let [, eventName] = name.split(':');
compileUtil['bind'](node, value, this.vm, eventName);
}
})
}
/**
* 编译文本节点
* @param {} node
*/
compileText(node) {
//正则匹配 {{}}
const content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
compileUtil['text'](node, content, this.vm);
}
}
/**
* 创建虚拟节点并返回,减少页面的回流与重绘
* @param {} el
*/
node2Fragment(el) {
const f = document.createDocumentFragment();
let firstChild;
//赋值并判断是否有值
while (firstChild = el.firstChild) {
/*
Node.appendChild() 方法将一个节点附加到指定父节点的子节点列表的末尾处。
如果将被插入的节点已经存在于当前文档的文档树中,那么 appendChild() 只会将
它从原先的位置移动到新的位置(不需要事先移除要移动的节点)。
如果某个节点已经拥有父节点,在被传递给此方法后,它首先会被移除,再被插入到新的位置。
*/
f.appendChild(firstChild);
}
return f;
}
/**
* 判断是否是通过 : 绑定
* @param {:src等} attrName
*/
isBindName(attrName) {
return attrName.startsWith(':');
}
/**
* 判断是否是通过 @ 绑定
* @param {@click等} attrName
*/
isEventName(attrName) {
return attrName.startsWith('@');
}
/**
* 判断是否是指令
* @param {} attrName
*/
isDirective(attrName) {
return attrName.startsWith('v-');
}
/**
* 判断是否是元素节点
* @param {} node
*/
isElementNode(node) {
return node.nodeType === 1;
}
}
class MVue {
constructor(options) {
//将属性绑定
this.$el = options.el;
this.$data = options.data;
//将 options 参数保存,后面处理数据时需要用到
this.$options = options;
//对 el 进行判断,有值才进行操作
if (this.$el) {
//1.实现一个数据观察者(观察数据中所有的属性)
new Observer(this.$data);
//2.实现一个指令解析器
new Compile(this.$el, this);
//代理 this.$data
this.proxyData(this.$data);
}
}
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key];
},
set(newValue) {
data[key] = newValue;
}
})
}
}
}
Observer.js
class Watcher {
/**
*
* @param {MVue实例} vm
* @param {指令中绑定的msg、person.name等} expr
* @param {回调函数} cb
*/
constructor(vm, expr, cb) {
//保存
this.vm = vm;
this.expr = expr;
this.cb = cb;
//保存旧值
this.oldValue = this.getOldValue();
}
getOldValue() {
//new MVue()时,创建Watcher对象,将当前观察者挂载到 Dep 上
Dep.target = this;
//通过 MVue.js 中的 compileUtil 获取值
const oldValue = compileUtil.getValue(this.expr, this.vm);
//获取完旧值之后进行销毁,否则再次更新数据时会导致创建多个订阅者
Dep.target = null;
return oldValue;
}
update() {
const newValue = compileUtil.getValue(this.expr, this.vm);
if (newValue != this.oldValue) {
//将新值回调返回
this.cb(newValue);
}
}
}
class Dep {
//1.添加所有订阅者
//2.数据发生变化时,通知所有订阅者
constructor() {
//用于保存所有订阅者
this.subs = [];
}
/**
* 添加订阅者
* @param {} watcher
*/
addSub(watcher) {
this.subs.push(watcher);
}
/**
* 通知所有订阅者
*/
notify() {
this.subs.forEach(w => w.update())
}
}
class Observer {
constructor(data) {
this.observe(data);
}
/**
* 监听数据
* @param {} data
*/
observe(data) {
if (data && typeof data === 'object') {
//获取data中属性的键
//获取的数据中可能还有对象,遍历判断
/*Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组,
数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致 。*/
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
})
}
}
/**
* 递归遍历,对所有数据进行监听
* @param {} obj
* @param {} key
* @param {} value
*/
defineReactive(obj, key, value) {
//递归遍历
this.observe(value);
//劫持数据时创建 Dep 和 Observer 进行关联
const dep = new Dep();
//数据劫持
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
//初始化的时候调用get()
get() {
//数据发生变化时,向 Dep 中添加订阅者
Dep.target && dep.addSub(Dep.target);
return value;
},
//通过箭头函数处理this指向问题
set: (newValue) => {
//对新值进行监听,否则更改后的新值将不能被监听到
this.observe(newValue)
//对新值进行判断
if (newValue != value) {
value = newValue;
}
//告诉Dep通知数据发生变化
dep.notify();
}
});
}
}