了解过vue绑定原理的人都知道。双向绑定的原理是利用数据劫持结合发布者–订阅者模式的方式,通过Object.defineProperty来劫持各个属性setter、getter,在数据发生变动时发布消息给订阅者,触发响应的回调函数。
简单了解一下Object.defineProperty,具体用法查看MDN
var obj = {};
Object.defineProperty(obj, 'name', {
get: function(val) {
console.log('获取值被修改的值')
return val;
},
set: function (newVal) {
console.log('我被设置了'+ newVal)
}
})
obj.name = '隔壁老王';//在给obj设置name属性的时候,触发了set这个方法
var val = obj.name;//在得到obj的name属性,会触发get方法
这样我们就可以在get和set中触发其他函数,从而来实现监听数据变动的目的。
根据以上描述,我们可以实现一个简单的双向绑定:代码如下
Document
这样就实现了一个简单的双向绑定。
原理图镇楼:
原理图解析:
1、observer的作用:通过object.defineProperty()循环劫持vue中data的所有属性值,从而利用get和set来通知订阅者Dep,从而来更新视图。
2、指令解析:我们都知道在vue中实现双向绑定的常用指令有:v-model,v-text,{{}}等等。也就是说在渲染html节点时,碰到这些指令的时候会进行指令解析。每碰到一个指令,就会在Dep中增加一个订阅者,这个订阅者只是更新自己指令对应的数据。每当set方法触发,就会循环触发Dep中对应的订阅者。
实现一个observer监听器,通过递归的方法遍历所有的对象以及对象中的对象也就是属性值,从而来监听所有的属性
Document
以上代码实现了对象属性值的劫持,下面通过解析指令实现对view和model的绑定
/** 解析指令,实现对view和model的绑定*/
compile(root,vm){
// var _this = this
var nodes =root.children
// 节点类型为元素
for (let i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node.children.length) {
vm.compile(node,vm)
}
if (node.hasAttribute('v-click')) {
node.onclick=(function(e){
var attrval = nodes[i].getAttribute('v-click')
console.log(attrval)
return vm.methods[attrval].bind(vm.data)
})()
}
if (node.hasAttribute('v-model')&&(node.tagName == 'INPUT' || node.tagName == 'TEXTAREA' )) {
node.addEventListener('input',(function(e){
var name= node.getAttribute('v-model')
new watcher(vm, node, name, 'value')
return function(){
vm.data[name] = nodes[e].value
}
})(i))
}
if (node.hasAttribute('v-bind')) {
var name = node.getAttribute('v-bind')
new watcher(vm, node, name, 'innerHTML')
}
}
}
}
创建一个可以容纳订阅者的消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数。所以显然订阅器需要有一个容器,这个容器就是list,将上面的Observer稍微改造下,植入消息订阅器:
defineReactive (obj,key,val){
const dep = new Dep()
Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get:function(){
/*****进行依赖收集(需要一个方法)将Dep.target(即当前的Watcher对象存入dep的subs中)******/
if (Dep.target) {
dep.addsub(Dep.target)
}
return val
},
set:function(newVal){
if (newVal === val) return
val = newVal
dep.notify()
}
})
}
// 构造订阅者Dep
class Dep {
constructor(){
/* 用来存放Watcher对象的数组 */
this.subs = []
}
/*在subs中添加一个watch对象*/
addsub(sub){
this.subs.push(sub)
}
/*通知所有对象更新视图*/
notify(){
this.subs.forEach((sub) =>{
sub.update()
})
}
}
从代码上看,我们将订阅器Dep添加一个订阅者设计在getter里面,这是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者,至于具体设计方案,下文会详细说明的。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。到此为止,一个比较完整Observer已经实现了,接下来我们开始设计Watcher。
我们知道,监听器Observer是在get函数执行了添加订阅者Wather的操作的,所以我们只要在订阅者Watcher初始化的时候触发对应的get函数去执行添加订阅者操作即可。而触发get函数只要获取对应的属性值就可以了。核心原因就是因为我们使用了Object.defineProperty( )进行数据监听。
class watcher{
constructor(vm, node, name, type){
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this
this.name = name //指令对应的值
this.node = node //节点
this.vm = vm //指令所属Vue
this.type = type //绑定的属性值,本例为InnerHTML
this.update()
Dep.target = null
}
update() {
this.get()
// this.node.nodeValue = this.value
this.node[this.type] = this.value
}
get() {
this.value = this.vm.data[this.name]
}
}
到此为止,vue双向绑定的原理基本实现。这篇文章只是粗略的的概述了一下vue双向绑定的原理。本文的完整代码请参考这里。如果你觉得还行的话点个赞就行。如果发现有什么不足的话,欢迎指出。