先上文字描述:
vue.js是采用数据劫持
结合发布者-订阅者模式
的方式,通过Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。
具体步骤:
第一步: 需要observer(观察者)
对数据对象进行递归遍历
,包括子属性对象的属性,都加上 setter和getter
这样的话,给这个对象的某个值赋值
,就会触发setter
,那么就能监听到了数据变化
第二步: compile(模板解析器)
解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步: Watcher(订阅者)
是Observer
和Compile
之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep
)里面添加自己
2、自身必须有一个update
()方法
3、待属性变动dep.notice()
通知时,能调用自身的update()方法
,并触发Compile中绑定的回调
,则功成身退。
第四步: MVVM
作为数据绑定的入口,整合Observer、Compile和Watcher三者,
通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
回答以上内容即可,下方内容,可以帮助大家理解
代码实现vue2双向数据绑定
vue.js文件
class Vue{
constructor(options){
this.$data = options.data
// 调用数据劫持的方法
Observe(this.$data)
// 属性代理
Object.keys(this.$data).forEach(key=>{
Object.defineProperty(this,key,{
enumerable:true,
configurable:true,
get(){
return this.$data[key]
},
set(newValue){
this.$data[key] = newValue
}
})
})
// 调用模板编译的函数
Compile(options.el,this)
}
}
// 定义一个数据劫持的方法
function Observe(obj){
// 递归的终止条件
if(!obj || typeof obj !== 'object') return
const dep = new Dep()
// 通过Object.keys(obj) 获取到当前obj上的每个属性
Object.keys(obj).forEach(key=>{
// 当前被循环的key所对应的属性值
let value = obj[key]
// 把value这个子节点,进行递归
Observe(value)
// 需要为当前的key所对应的属性,添加getter和setter
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
Dep.target && dep.addSub(Dep.target)
console.log(`有人读取了${key}的值`);
return value
},
set(newVal){
value = newVal
Observe(value)
dep.notify()
},
})
})
}
// 对HTML结构进行模板编译的方法
function Compile(el,vm){
// 获取el对应的DOM元素
vm.$el = document.querySelector(el)
const fragment = document.createDocumentFragment()
while((childNode = vm.$el.firstChild)){
fragment.appendChild(childNode)
}
// 进行模板编译
replace(fragment)
vm.$el.appendChild(fragment)
function replace(node){
// 定义匹配插值表达式的正则
const regMustache = /\{\{\s*(\S+)\s*\}\}/
// 证明当前的node节点是一个文本子节点,需要进行正则的替换
if(node.nodeType===3){
// 注意:文本子节点,也是一个DOM对象,如果要获取文本子节点的字符串内容,需要调用textContent属性获取
const text = node.textContent
const execResult = regMustache.exec(text)
if(execResult){
const value = execResult[1].split('.').reduce((newObj,k)=>newObj[k],vm)
node.textContent = text.replace(regMustache,value)
new Watcher(vm,execResult[1],(newValue)=>{
node.textContent = text.replace(regMustache,newValue)
})
}
// 终止递归的条件
return
}
// 判断当前的node节点是否为input输入框
if(node.nodeType===1 && node.tagName.toUpperCase() === 'INPUT'){
const attrs = Array.from(node.attributes)
const findResult = attrs.find((x)=>x.name==='v-model')
if(findResult){
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj,k)=>newObj[k],vm)
node.value = value
new Watcher(vm,expStr,(newValue)=>{
node.value = newValue
})
// 监听文本框的input输入事件,拿到文本框最新的值,把最新的值,更新到vm上即可
node.addEventListener('input',(e)=>{
const keyArr = expStr.split('.')
const obj = keyArr.slice(0,keyArr.length-1).reduce((newObj,k)=>newObj[k],vm)
obj[keyArr[keyArr.length-1]] = e.target.value
})
}
}
// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach(child=>replace(child))
}
}
// 收集依赖/收集订阅者
class Dep{
constructor(){
// 今后,所有的watcher都要存到这个数组中
this.subs = []
}
// 向 subs 数组中,添加watcher的方法
addSub(watcher){
this.subs.push(watcher)
}
// 负责通知每个watcher的方法
notify(){
this.subs.forEach(watcher=>watcher.update())
}
}
class Watcher{
constructor(vm,key,cb){
this.vm = vm
this.key = key
this.cb = cb
Dep.target = this
key.split('.').reduce((newObj,k)=>newObj[k],vm )
Dep.target = null
}
update(){
const value = this.key.split('.').reduce((newObj,k)=>newObj[k],this.vm)
this.cb(value)
}
}
html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>姓名:{{name}}</h3>
<h3>年龄:{{age}}</h3>
<h3>info.a的值是:{{info.a}}</h3>
<div>name的值:<input type="text" v-model="name"></div>
<div>info.a的值:<input type="text" v-model="info.a"></div>
</div>
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el:"#app",
data:{
name:'zs',
age:20,
info:{
a:'a1',
c:'c1'
}
}
})
</script>
</body>
</html>