双向数据绑定的实现原理(mini-vue)
采用数据劫持结合发布&订阅者模式,使用 Object.definePropert 给每个属性添加getter 和 setter ,在数据变动时发布消息个订阅者,触发响应的监听回调
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<div id="app">
<h3>名字:{{name}}</h3>
<h3>年龄:{{age}}</h3>
<input type="text" v-model='age'>
</div>
// 这是引入自己写的vue.js 文件
<script src="./vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
name: 'zs',
age: 20,
info: {
a: 'a',
c: 'c'
}
}
})
</script>
</body>
</html>
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(newVal){
this.$data[key]=newVal
},
})
})
Compile(options.el,this)
}
}
//数据劫持方法
function Observe(obj) {
if(!obj || typeof obj !=='object')return
const dep = new Dep()
//通过 object.key(obj)
Object.keys(obj).forEach(key=>{
let value = obj[key]
//递归
Observe(value)
// 需要为当前的 key 所对应的属性,添加 getter 和 setter
Object.defineProperty(obj,key,{
enumerable:true,//是否可配置
configurable:true,//是否可枚 举
get(){
Dep.target && dep.addSub(Dep.target)
return value
},
set(newVal){
value=newVal
Observe(value)
dep.notify()
},
})
})
}
// 对 html 结构进行模板编译
function Compile(el,vm,) {
//获取 el 对应的 DOM 元素
vm.$el = document.querySelector(el)
// 创建文档碎片 提高 DOM 操作性能
const fragment = document.createDocumentFragment()
while(childNode = vm.$el.firstChild){
fragment.appendChild(childNode)
}
// 进行模板编译
replace(fragment)
vm.$el.appendChild(fragment)
// 负责对 DOM 模板进行编译
function replace(node) {
//定义匹配差值表达式的正则
const regMustache = /\{\{\s*(\S+)\s*\}\}/
// 证明当前的 node 节点是一个文本子节点 需要进行正则替换
if(node.nodeType===3){
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)
// 在这个时候创建watcher类的实例
new Watcher(vm,execResult[1],(newVal)=>{
node.textContent = text.replace(regMustache,newVal)
})
}
//终止递归的条件
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){
// 获取当前 v-model属性的值 v-model='name.a'
const expStr = findResult.value
const value = expStr.split('.').reduce((newObj,k)=>newObj[k],vm)
node.value =value
new Watcher(vm,expStr,(newVal)=>{
node.value =newVal
})
//监听文本框的 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))
}
}
//依赖收集 Dep 订阅者的类
class Dep{
constructor(){
this.subs=[]
}
// 向 subs 数组中添加 watcher 方法
addSub(watcher){
this.subs.push(watcher)
}
//负责通知每个 watcher 的方法
notify(){
this.subs.forEach(watcher=>{
watcher.upData()
})
}
}
class Watcher{
// cb 回调函数中,记录着当前 watcher 如何更新自己的文本内容
// 但是,只知道如何更新自己还不行,还必须拿到最新的数据
// 因此,还需要在 new Wacher 期间,把vm也传进来
// 除此之外,还需要知道,在 vm 身上的数据中,哪个数据,才是自己所需要的数据
constructor(vm,key,cb,){
this.cb = cb
this.key = key
this.vm = vm
// 下面三行代码,负责把创建的 watcher 实例存到 Dep 实例的subs
Dep.target = this
key.split('.').reduce((newObj,k)=>newObj[k],vm)
Dep.target = null
}
upData(){
const value = this.key.split('.').reduce((newObj,k)=>newObj[k],this.vm)
this.cb(value)
}
}