通过Object.defineProperty可以在一个对象上定义一个属性,或者修改这个属性,第一个参数。至于这个defineProperty的详细用法,可以参阅MDN手册,在此不赘叙。
var obj = {}
Object.defineProperty(obj, 'name', {
get () {
console.log('getter',value)
return value
},
set (newValue) {
console.log('setter',newValue)
value = newValue
},
enumerable: true,
configurable: true
})
// 如此以来,访问obj.name属性或修改obj.name的值,都会触发get()或set()。
在此基础上,遍历这个对象的所有属性,就可以实现一个丐版的数据劫持。
<input type="text" id ='input' onchange="input(event)">
<span id="span"></span>
var obj = {}
Object.defineProperty(obj,'name',{
get() {
console.log('getter')
},
set(newValue) {
console.log('setter',newValue)
document.getElementById('input').value=newValue
document.getElementById('span').innerHTML=newValue
}
})
function input (event){
obj.name =event.target.value
}
但是上面的代码,数据、函数和DOM紧紧地耦合在一起,所以要进行解耦。 回顾学习VUE之初,推荐使用VUE的方法是引入vue.js文件。所以,下面,先写上。要做的第一件事,是将模版解析出来,能更好地理解数据的双向绑定。
<div id="app">
<input type="text" v-model="user.name" />
<div>
<div>{{user.name}}-{{user.age}}</div>
</div>
</div>
<script type="text/javascript">
const app = new MineVue({
el: '#app',
data: {
user: {
name: 'Tom',
age: 18
}
}
})
//基类,负责调度
class MineVue {
constructor(options) {
this.el = options.el
this.data = options.data
this.init()
}
init() {
if (this.el) {
this.vm = document.querySelector(this.el)
new Compiler(this.el, this)
}
}
}
//编译模版
class Compiler {
constructor(el, vm) {
this.el = document.querySelector(el)
this.vm = vm
this.init(this.el)
}
init(el) {
const fragment = this.nodeToFragment(el)
this.compile(fragment)
el.appendChild(fragment)
}
// 为了避免对DOM的多次操作引起重排回流,所以将DOM存到内存中
nodeToFragment(node) {
const fragment = document.createDocumentFragment()
let firstChild = node.firstChild
while (firstChild) {
fragment.appendChild(firstChild)
firstChild = node.firstChild
}
return fragment
}
// 将模版编译
compile(node) {
// 将子节点取出
const childNodes = [...node.childNodes]
// 编译
childNodes.forEach(childNode => {
if (childNode.nodeType === 1) {
this.compileElement(childNode)
this.compile(childNode)
} else {
this.compileText(childNode)
}
})
}
// 编译标签中的v-model
compileElement(node) {
// 取出节点的所有属性
const attributes = [...node.attributes]
attributes.forEach(attr => {
const {name,value: expression} = attr
const reg = /^v-model/
if (reg.test(name)) {
// 将值放到节点中
node.value = getValue(this.vm, expression)
}
})
}
// 编译{{}}
compileText(node) {
const reg = /\{\{(.+?)\}\}/g
const expression = node.textContent
if (reg.test(expression)) {
const value = expression.replace(reg, (...args) => {
return getValue(this.vm, args[1])
})
node.textContent = value
}
}
}
</script>
将模版编译好之后,引入一个发布订阅模式,首先实现一个观察者Observer,监听每个数据的变化。这样,就对MinewVue.html中app的数据进行了监控。
//修改一下MineVue,调用Observer,将数据都监控起来
class MineVue {
constructor(options) {
this.el = options.el
this.data = options.data
this.init()
}
init() {
if (this.el) {
this.vm = document.querySelector(this.el)
new Observer(this.data)
new Compiler(this.el, this)
}
}
}
//观察者,对data里面的所有属性进行拦截。
class Observer {
constructor(data) {
this.observer(data)
}
//遍历data对象的所有属性
observer(data) {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
//对所有属性设置setter和getter
defineReactive(data, key, value) {
//递归遍历
this.observer(value)
//拦截
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
return value
},
set(newValue) {
if (value !== newValue) {
console.log(`检测到${value}=>${newValue}`)
value = newValue
}
}
})
}
}
既然这里把数据监控了起来,那么接下来要做的,就是做一个订阅者,来订阅这些数据,同时维护一份订阅者的列表,存放所有的订阅者,如果数据更新了,就从订阅者列表依次更新。
class Watcher {
constructor(expression, vm) {
this.expression = expression
this.vm = vm
//获取旧的值
this.value = this.get()
}
get() {
Deposit.target = this
const value = getValue(this.expression, this.vm)
Deposit.target = null
return value
}
//数据更新时触发的函数
updata() {
const newValue = getValue(this.expression, this.vm)
if (this.value !== newValue) {
this.value = newValue
}
}
}
class Deposit {
constructor() {
this.watchers = []
}
//将订阅者(Watcher)添加到订阅者列表
addWatcher(watcher) {
this.watchers.push(watcher)
}
//通知所有的订阅者(Watcher)数据更新
notify() {
this.watchers.forEach(watcher => {
watcher.update()
})
}
}
这样,就完成了一个订阅者和订阅发布中心。下面要做的事情就是关联起来。 首先,在Observer中修改一下,无论是访问数据还是更新数据,都通过订阅发布中心Deposit来进行。
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(data, key, value) {
this.observer(value)
//给当前属性添加监听
const deposit = new Deposit()
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
//将属性添加到订阅者中心Deposit当中
if (Deposit.target) deposit.addWatcher(Deposit.target)
return value
},
set(newValue) {
if (value === newValue) return
value = newValue
//数据更新后,通知订阅者中心
deposit.notify()
}
})
}
}
这样,数据就和订阅者中心(Deposit)关联了起来,还差最后一个,就是在编译模版的过程当中,将数据通过Watcher来绑定,所以修改一下Compilder。
class Compiler {
constructor(el, vm) {
this.el = document.querySelector(el)
this.vm = vm
this.init(this.el)
}
init(el) {
const fragment = this.nodeToFragment(el)
this.compile(fragment)
el.appendChild(fragment)
}
nodeToFragment(node) {
const fragment = document.createDocumentFragment()
let firstChild = node.firstChild
while (firstChild) {
fragment.appendChild(firstChild)
firstChild = node.firstChild
}
return fragment
}
compile(node) {
const childNodes = [...node.childNodes]
childNodes.forEach(node => {
if (node.nodeType === 1) {
this.compileElement(node)
this.compile(node)
} else {
this.compileText(node)
}
})
}
compileElement(node) {
const attributes = [...node.attributes]
attributes.forEach(attr => {
const { name, value: expression } = attr
if (name !== 'v-model') return
const value = getValue(expression, this.vm)
//加上观察者
new Watcher(expression, this.vm, (newValue) => {
node.value = newValue
})
node.addEventListener('input', e => {
const value = e.target.value
setValue(expression, this.vm, value)
})
node.value = value
})
}
compileText(node) {
const reg = /\{\{(.+?)\}\}/g
const content = node.textContent
if (reg.test(content)) {
const value = content.replace(reg, (...args) => {
//给每个{{}}表达式都加上观察者
new Watcher(args[1], this.vm, () => {
//因为DOM中都模版可能是{{a}}{{b}}这样的形式,所以要返回整个字符串
node.textContent = this.getCompileTextValue(content, this.vm)
})
return getValue(args[1], this.vm)
})
node.textContent = value
}
}
getCompileTextValue(expression, vm) {
const reg = /\{\{(.+?)\}\}/g
const value = expression.replace(reg, (...args) => {
const value = getValue(args[1], vm)
return value
})
return value
}
}