项目地址:gitee.com/luobf22/vue…
vue2双向绑定核心:
- Vue2 利用
Object.defineProperty来进行数据劫持。这个方法可以对对象的属性进行重新定义,使其具有自定义的访问器属性(get和set)
- 当读取属性(
get)时,可以收集依赖;当修改属性(set)时,可以通知相关的依赖进行更新。
完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<button @click="btn">修改msg</button>
<p>{{msg}}</p>
<button @click="add">增加count</button>
<p>{{count}}</p>
<button @click="xiugaiArray">修改数组</button>
<p>{{arr}}</p>
<button @click="xiugaiObj">obj修改某个属性</button>
<button @click="addObjAttr">obj添加属性</button>
<p>{{obj.person.name}}</p>
<p>{{obj.animal}}</p>
<input type="text" v-model="inputModel">
{{inputModel}}
</div>
</body>
<script src="./手写Vue2.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'hello vue2',
count: 100,
arr: [1, 2, 3],
obj: {
person: {
name: 'hhh'
},
animal: {
dog: {
name: '小狗'
}
}
},
inputModel: '请输入'
},
beforeCreate() {
},
created() {
},
beforeMount() {
},
mounted() {
},
methods: {
add() {
this.count += 1
},
btn(e) {
this.msg = 'welcome vue2 双向绑定'
},
xiugaiArray() {
this.arr[0] = 9
console.log(this.arr);
},
xiugaiObj() {
this.obj = {
person: {
name: 'luo'
},
animal: {
dog: {
name: '小哈'
}
}
}
},
addObjAttr() {
this.obj.animal.dog.color = 'black'
}
},
})
</script>
</html>
class Vue {
constructor(options) {
this.$options = options
console.log(this.$options);
this.$watchEvent = {}
this.$data = options.data
this.proxyData()
this.observe()
this.$el = document.querySelector(options.el)
this.compile(this.$el)
}
proxyData() {
for (const key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(val) {
this.$data[key] = val
}
})
}
}
observe(obj) {
for (const key in this.$data) {
let value = this.$data[key]
let that = this
Object.defineProperty(this.$data, key, {
get() {
return value
},
set(val) {
value = val
if (that.$watchEvent[key]) {
that.$watchEvent[key].forEach((item, index) => {
item.update()
})
}
}
})
}
};
compile(node) {
node.childNodes.forEach((item, index) => {
if (item.nodeType == 1) {
if (item.hasAttribute('@click')) {
let vmKey = item.getAttribute('@click').trim()
item.addEventListener('click', (event) => {
this.eventFn = this.$options.methods[vmKey].bind(this)
this.eventFn(event)
})
}
if (item.hasAttribute('v-model')) {
let vmKey = item.getAttribute('v-model').trim()
if (this.hasOwnProperty(vmKey)) {
item.value = this[vmKey]
}
item.addEventListener('input', (event) => {
this[vmKey] = item.value
})
}
if (item.childNodes.length > 0) {
this.compile(item)
}
} else if (item.nodeType == 3) {
let reg = /\{\{(.*?)\}\}/g
let text = item.textContent
let that = this
const keys = [];
text.replace(reg, (match, $1) => {
keys.push($1.trim());
return match;
});
function getObjData(arr, data, item, vmKey) {
let firstKey = arr.shift()
firstKey = firstKey.trim()
let node = {}
node.value = data[firstKey]
if (data.hasOwnProperty(firstKey)) {
let watch = new watcher(that, firstKey, item, 'textContent', vmKey)
if (that.$watchEvent[firstKey]) {
that.$watchEvent[firstKey].push(watch)
} else {
that.$watchEvent[firstKey] = []
that.$watchEvent[firstKey].push(watch)
}
}
let res = data[firstKey]
if (arr.length !== 0) {
return getObjData(arr, res, item)
} else {
return JSON.stringify(res)
}
}
item.textContent = text.replace(reg, (match, vmKey) => {
vmKey = vmKey.trim()
if (this.hasOwnProperty(vmKey)) {
let watch = new watcher(this, vmKey, item, 'textContent', vmKey)
if (this.$watchEvent[vmKey]) {
this.$watchEvent[vmKey].push(watch)
} else {
this.$watchEvent[vmKey] = []
this.$watchEvent[vmKey].push(watch)
}
}
let index = vmKey.indexOf('.')
if (index !== -1) {
let objArr = vmKey.split('.')
return getObjData(objArr, this.$data, item, vmKey)
} else {
let res = JSON.stringify(this.$data[vmKey])
return res
}
})
}
});
}
}
class watcher {
constructor(vm, key, node, attr, vmKey) {
this.vm = vm;
this.key = key;
this.node = node;
this.attr = attr;
this.vmKey = vmKey;
}
update() {
console.log(this.vm);
console.log(this.key);
console.log(this.vmKey);
function getObjData(arr, data) {
let firstKey = arr.shift()
firstKey = firstKey.trim()
let res = data[firstKey]
if (arr.length !== 0) {
return getObjData(arr, res)
} else {
return res
}
}
let index = this.vmKey.indexOf('.')
let res
if (index !== -1) {
let objArr = this.vmKey.split('.')
res = JSON.stringify(getObjData(objArr, this.vm))
this.node[this.attr] = res
} else {
res = JSON.stringify(this.vm[this.key])
this.node[this.attr] = res
}
}
}
逐个分析函数
constructor(options)
将数据绑定到vue实例里的constructor
this.$options = options
this.$data = options.data
创建一个观察事件hash表,来监听每个node节点的变化,收集依赖相当于dep功能
this.$watchEvent = {}
获取el挂在的节点
this.$el = document.querySelector(options.el)
数据劫持,初始化
this.proxyData()
数据劫持,观察数据变化
this.observe()
模板解析,将html上的文本转为数据
this.compile(this.$el)
双向绑定,数据劫持,第一次初始化数据
proxyData() {
let that = this
for (const key in this.$data) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(val) {
this.$data[key] = val
}
})
}
}
双向绑定,数据劫持,观察更新,数据变化,触发watcher里的update函数
observe(obj) {
for (const key in this.$data) {
let value = this.$data[key]
let that = this
Object.defineProperty(this.$data, key, {
get() {
return value
},
set(val) {
value = val
if (that.$watchEvent[key]) {
that.$watchEvent[key].forEach((item, index) => {
item.update()
})
}
}
})
}
};
模板解析
compile(node) {
node.childNodes.forEach((item, index) => {
if (item.nodeType == 1) {
if (item.hasAttribute('@click')) {
let vmKey = item.getAttribute('@click').trim()
item.addEventListener('click', (event) => {
this.eventFn = this.$options.methods[vmKey].bind(this)
this.eventFn(event)
})
}
if (item.hasAttribute('v-model')) {
let vmKey = item.getAttribute('v-model').trim()
if (this.hasOwnProperty(vmKey)) {
item.value = this[vmKey]
}
item.addEventListener('input', (event) => {
this[vmKey] = item.value
})
}
if (item.childNodes.length > 0) {
this.compile(item)
}
} else if (item.nodeType == 3) {
let reg = /\{\{(.*?)\}\}/g
let text = item.textContent
let that = this
const keys = [];
text.replace(reg, (match, $1) => {
keys.push($1.trim());
return match;
});
function getObjData(arr, data, item, vmKey) {
let firstKey = arr.shift()
firstKey = firstKey.trim()
let node = {}
node.value = data[firstKey]
if (data.hasOwnProperty(firstKey)) {
let watch = new watcher(that, firstKey, item, 'textContent', vmKey)
if (that.$watchEvent[firstKey]) {
that.$watchEvent[firstKey].push(watch)
} else {
that.$watchEvent[firstKey] = []
that.$watchEvent[firstKey].push(watch)
}
}
let res = data[firstKey]
if (arr.length !== 0) {
return getObjData(arr, res, item)
} else {
return JSON.stringify(res)
}
}
item.textContent = text.replace(reg, (match, vmKey) => {
vmKey = vmKey.trim()
if (this.hasOwnProperty(vmKey)) {
let watch = new watcher(this, vmKey, item, 'textContent', vmKey)
if (this.$watchEvent[vmKey]) {
this.$watchEvent[vmKey].push(watch)
} else {
this.$watchEvent[vmKey] = []
this.$watchEvent[vmKey].push(watch)
}
}
如果是对象,则需要层层遍历对象的属性,找到obj下的key值,然后通过update赋值
let index = vmKey.indexOf('.')
if (index !== -1) {
let objArr = vmKey.split('.')
return getObjData(objArr, this.$data, item, vmKey)
} else {
let res = JSON.stringify(this.$data[vmKey])
return res
}
})
}
});
}
}
观察者:创建观察者,赋予update函数
class watcher {
constructor(vm, key, node, attr, vmKey) {
this.vm = vm;
this.key = key;
this.node = node;
this.attr = attr;
this.vmKey = vmKey;
}
update() {
function getObjData(arr, data) {
let firstKey = arr.shift()
firstKey = firstKey.trim()
let res = data[firstKey]
if (arr.length !== 0) {
return getObjData(arr, res)
} else {
return res
}
}
let index = this.vmKey.indexOf('.')
let res
if (index !== -1) {
let objArr = this.vmKey.split('.')
res = JSON.stringify(getObjData(objArr, this.vm))
this.node[this.attr] = res
} else {
res = JSON.stringify(this.vm[this.key])
this.node[this.attr] = res
}
}
}
vue2 双向绑定的缺陷
- 在 Vue 2 中,通过
Object.defineProperty进行数据劫持来实现双向绑定。但是这种方式对于已经创建的对象,只能劫持对象已有的属性,无法自动劫持新添加的属性。这是因为 JavaScript 的Object.defineProperty是在对象属性定义时设置访问器属性来进行数据劫持的
- 当给obj对象添加新的属性以及通过下边index给数组修改数据时,set方法无法劫持到数据,虽然get可以获取劫持的数据,但是无法通知update方法去对textContent进行修改,本来设想时可以通过对比之前的data数据,然后调用update方法,但是js会报错,显示超过栈的大小,所以也失败告终
- 虽然视图没有及时更新,但是数据更新了,而且也可以通过vue.$set方式去进行修改视图