Vue-2.+
采用数据劫持结合发布者-订阅者模式的方式,使用Object.defineProperty()方法对象通过 递归+遍历 的方式,重写属性的setter,getter方法来实现对数据的监控的。在数据变动时发布消息给订阅者,触发相应的监听回调。
缺点:
- 监听数组的方法不能触发Object.defineProperty方法中的set操作(如果要监听的到话,需要重新编写数组的方法:push、pop、shift、unshift、splice、sort、reverse)。
- 必须遍历每个对象的每个属性,如果对象嵌套很深的话,需要使用递归调用。
Vue-3.0
Proxy
const obj = new Proxy(data, handler);
当外界每次对obj进行操作时,就会执行handler对象上的一些方法。handler中常用的对象方法如下:
-
get(target, propKey, receiver)
-
set(target, propKey, value, receiver)
-
has(target, propKey)
-
construct(target, args):
-
apply(target, object, args)
-
需要实现一个数据监听器 Observer, 能够对所有数据进行监听,如果有数据变动的话,拿到最新的值并通知订阅者Watcher.
-
需要实现一个指令解析器Compile,它能够对每个元素的指令进行扫描和解析,根据指令模板替换数据,以及绑定相对应的函数。
-
需要实现一个Watcher, 它是链接Observer和Compile的桥梁,它能够订阅并收到每个属性变动的通知,然后会执行指令绑定的相对应
的回调函数,从而更新视图。
<!DOCTYPE html>
<html>
<head>
<title>MyVue</title>
</head>
<body>
<div id="app">
<input id="input1" type="text" v-model="inputValue" style="margin-top: 50px"/>
<button id="btn1" type="button" v-click="add">add</button>
<span id="span1" v-bind="inputValue" style="margin-left: 30px"></span>
<br/>
<input type="text" v-model="inputValue2" style="margin-top: 50px"/>
<span v-bind="inputValue2" style="margin-left: 30px"></span>
<br/>
<input type="text" v-model="inputValue3" style="margin-top: 50px"/>
<span v-bind="inputValue3" style="margin-left: 30px"></span>
<span style="margin-left: 30px;color:red">{{inputValue3}}</span>
</div>
</body>
<!--<script>-->
<!-- let data = {-->
<!-- inputValue: '',-->
<!-- }-->
<!-- // Vue无法监听到对象属性的添加和删除-->
<!-- // Object.defineProperty(data, 'inputValue', {-->
<!-- // get() {-->
<!-- // return data.inputValue-->
<!-- // },-->
<!-- // set(newVal) {-->
<!-- // document.getElementById('span1').innerHTML = newVal-->
<!-- // }-->
<!-- // })-->
<!-- const handler = {-->
<!-- get: function(target, key) {-->
<!-- console.log('handler=get===>', target, key)-->
<!-- return target[key];-->
<!-- },-->
<!-- set: function(target, key, newVal) {-->
<!-- console.log('handler=set===>', target, key, newVal)-->
<!-- target[key] = newVal;-->
<!-- document.getElementById('span1').innerHTML = newVal-->
<!-- }-->
<!-- };-->
<!-- data = new Proxy(data, handler);-->
<!-- document.getElementById('input1').addEventListener('input', (e)=>{-->
<!-- data.inputValue = e.target.value-->
<!-- })-->
<!-- document.getElementById('btn1').addEventListener('click', (e)=>{-->
<!-- data.inputValue = 'q'-->
<!-- })-->
<!--</script>-->
<script>
let data = ['1'];
const handler = {
set: function(target, key, newVal) {
const res = Reflect.set(target, key, newVal);
console.log('handler=set===>', target)
document.getElementById('span1').innerHTML = JSON.stringify(target)
return res
}
};
data = new Proxy(data, handler);
document.getElementById('input1').addEventListener('input', (e)=>{
console.log('input===>', e.target.value)
data.push(e.target.value)
})
</script>
<script>
class MyVue {
_binding
constructor(options) {
this._init(options)
}
_init(options) {
this.$options = options // options 为上面使用时传入的结构体,包括el,data,methods
this.$el = document.querySelector(options.el) // el是 #app, $el是id为app的Element元素
this.$data = options.data
this.$methods = options.methods
this.$watch = options.watch || {}
console.log('myVue=_init==>', this)
// 初始化_binding,保存着model与view的映射关系,也就是我们前面定义的Watcher的实例。当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
this._binding = {}
this._observer()
// this._proxyObserver()
console.log('myVue=_obsever==>', this)
// this._compile()
// console.log('myVue=_compile==>', this)
const dom = this._nodeToFragment(this.$el)
this.$el.appendChild(dom)
}
// 数据劫持:更新数据
_observer() {
const vm = this
const obj = vm.$data
const mWatch = vm.$watch
Object.keys(obj).map(key => {
let value = obj[key]
// if (typeof value == 'object') { //如果值还是对象,则继续遍历
// vm._observer(value)
// }
// 对对象已有的属性添加数据描述
Object.defineProperty(vm.$data, key, {
configurable: true, // 能否使用delete、能否需改属性特性、或能否修改访问器属性,false为不可重新定义,默认值为true
enumerable: true, // 对象属性是否可通过for-in循环,false为不可循环,默认值为true
get: () => { // 重写get
console.log('myVue=get===>', key, value)
return value
},
set: (newVal) => { // 重写set
console.log('myVue=set===>', key, newVal, value)
if (value !== newVal) {
if (mWatch[key]) {
mWatch[key](newVal, value)
}
//利用闭包的特性,修改value,get取值时也会变化
//不能使用obj[key]=newVal
//因为在set中继续调用set赋值,引起递归调用
value = newVal
// obj[key] = newVal
// 当value更新时,出发_binding中绑定的watcher
vm._binding[key].map(item => {
item.update() // 顶用watcher中的update方法更新dom
})
}
}
})
})
}
_proxyObserver() {
const vm = this
const mWatch = vm.$watch
const handler = {
get(target, key) {
console.log('myVue=get===>', target, key, target[key])
return Reflect.get(target, key)
},
set(target, key, newVal) {
console.log('myVue=set===>', target, key, newVal)
if (mWatch[key]) {
mWatch[key](newVal, target[key])
}
// 当value更新时,出发_binding中绑定的watcher
const res = Reflect.set(target, key, newVal)
vm._binding[key].map(item => {
item.update() // 顶用watcher中的update方法更新dom
})
return res
}
}
// 把代理器返回的对象代理到this.$data,即this.$data是代理后的对象,外部每次对this.$data进行操作时,实际上执行的是这段代码里handler对象上的方法
vm.$data = new Proxy(vm.$data, handler)
}
// 将view与model进行绑定,解析指令(v-bind,v-model,v-click)等
_compile() {
const vm = this
const el = vm.$el
let nodes = Array.prototype.slice.call(el.children) // 将伪数组转成数组
console.log('_compile====>', el.children)
console.log('_compile1====>', nodes)
nodes.map(node => {
console.log('node=====>', node)
if (node.children && node.children.length > 0) { // 对所有元素进行遍历,并进行处理
vm._compile(node)
}
// 解析属性
if (node.hasAttribute('v-click')) {
// v-click绑定的attrVal为methods里的方法名
const attrVal = node.getAttribute('v-click')
node.onclick = vm.$methods[attrVal].bind(vm.$data) // // bind是使data的作用域与method函数的作用域保持一致
}
if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
// v-model绑定的为 data里的key
const attrVal = node.getAttribute('v-model')
if (!vm._binding[attrVal]) vm._binding[attrVal] = []
vm._binding[attrVal].push(new Watcher(node, 'value', vm.$data, attrVal))
node.addEventListener('input', () => {
vm.$data[attrVal] = node.value
})
}
if (node.hasAttribute('v-bind')) {
const attrVal = node.getAttribute('v-bind')
if (!vm._binding[attrVal]) vm._binding[attrVal] = []
vm._binding[attrVal].push(new Watcher(node, 'innerHTML', vm.$data, attrVal))
}
})
}
_nodeToFragment(node, flag){
flag = flag || document.createDocumentFragment()
let child
while (child = node.firstChild) {
this._compile2(child)
// appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
// 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
flag.appendChild(child)
if (child.firstChild) {
this._nodeToFragment(child, flag)
}
}
return flag
}
_compile2(node) {
console.log('node=====>', node, node.nodeType, node.nodeValue)
const vm = this
if (node.nodeType === 1) {
const attrs = node.attributes
Object.keys(attrs).map(i => {
const attr = attrs[i]
// console.log('node====>', attr)
if (!attr) return
if (attr.nodeName === 'v-model' ) {
const key = attr.nodeValue // 获取 v-model 绑定的属性名
node.value = vm.$data[key]
node.addEventListener('input', (e) => {
vm.$data[key] = e.target.value
})
if (!vm._binding[key]) vm._binding[key] = []
vm._binding[key].push(new Watcher(node, 'value', vm.$data, key))
node.removeAttribute('v-model')
}
if (attr.nodeName === 'v-bind') {
const key = attr.nodeValue // 获取 v-model 绑定的属性名
node.innerHTML = vm.$data[key]
console.log('v-bind===>', node, node.innerHTML)
if (!vm._binding[key]) vm._binding[key] = []
vm._binding[key].push(new Watcher(node, 'innerHTML', vm.$data, key))
node.removeAttribute('v-bind')
}
})
} else if (node.nodeType === 3) {
const reg = /\{\{(.*)\}\}/
if (reg.test(node.nodeValue)) {
const key = RegExp.$1.trim() // 获取匹配到的字符串
if (!vm._binding[key]) vm._binding[key] = []
vm._binding[key].push(new Watcher(node, 'nodeValue', vm.$data, key))
} else {
console.log('qqqqqqqqq====>', node)
}
}
}
}
// 订阅者,用于监听更新dom
class Watcher {
el
attr
data
key
constructor(el, attr, data, key) {
this.el = el
this.attr = attr
this.data = data
this.key = key
this.update()
}
update() {
this.el[this.attr] = this.data[this.key]
}
}
const myVue = new MyVue({
el: '#app',
data: {
inputValue: '',
inputValue2: '',
inputValue3:''
},
watch: {
inputValue(newV, oldV) {
console.log('watch=inputValue===>', newV, oldV)
}
},
methods: {
add() {
console.log('myVue=add===>')
this.inputValue += '1'
},
add2() {
console.log('myVue=add2===>')
this.inputValue2 += '2'
}
}
})
</script>
</html>
网友评论