首先推荐一个b站视频,时长是30分钟,需要耐心多看几遍:
蛋老师
数据双向绑定=数据劫持+订阅发布者模式
总:创建一个Vue类,构造器中传入需要监听的数据,调用模板解析器
1.数据劫持:监听实例的数据
对传入数据循环递归实现数据劫持,在get中添加订阅者,返回value
在set中对订阅者进行通知
⚠️:Object.definePropert(操作对象,操作属性,传入一个对象并在对象里实现监听)
2.数据应用到页面-模板解析器
获取页面元素-放入临时内存区域-应用vue实例-渲染页面
将{{name}}对应的属性值用正则解析出来
2-1:操作节点类型为3的文本节点,使用正则表达式修改插值表达式的内容,在修改内容时创建订阅者实例
2-2:操作节点类型为1的使用v-model输入框节点,创建订阅者实例,实现数据改变视图,对节点使用addEventListener实现输入框值回显到页面
3.上面中还用到关于订阅者的两个类,
一个是依赖,用于收集通知订阅者,一个是用进行数据的更新
<!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>mvvm数据双向绑定</title>
</head>
<body>
<div id="app">
<span>数据:{{name}}</span>
<input type="text" v-model="name">
<span>更多:{{more.like}}</span>
<input type="text" v-model="more.like">
</div>
<script src="./v-model.js"></script>
<script>
const vm = new Vue({
el:'#app',
data:{
name:'lisa',
more:{
like:'dance'
}
}
})
console.log(vm)
</script>
</body>
</html>
//数据双向绑定=数据劫持+发布订阅者模式
class Vue{
constructor(obj_instance){
this.$data=obj_instance.data
Observer(this.$data) //传入需要监听的数据
//在调用构造vue实例的时候就调用这个模板解析器,this是这个实例
Compile(obj_instance.el,this)
}
}
//数据劫持 --监听实例里面的数据
function Observer(data_instance){
//递归出口
if(!data_instance || typeof data_instance !=='object') return;
//new一个实例
const dependency = new Dependency()
Object.keys(data_instance).forEach(key =>{
let value = data_instance[key];
Observer(value) //子属性数据劫持
// defineProperty 参数操作对象,操作属性,传入一个对象并在对象里实现监听
Object.defineProperty(data_instance,key,{
//会被循环中列出
enumerable:true,
//可以被删除修改
configurable:true,
//访问属性值时触发
get(){
console.log(`访问了属性:${key} 值:->${value}`)
//订阅者加入依赖实例的数组
Dependency.temp && dependency.addSub(Dependency.temp)
console.log("Dependency.temp",Dependency.temp)
return value
},
//修改属性值时触发
set(newValue){
console.log(`属性 ${key} 的值 ${value}修改为-> ${newValue}`)
value=newValue
Observer(newValue) //将value改为对象时,进行数据劫持
dependency.notify()
}
})
})
}
//2.数据应用到页面
//获取页面元素-放入临时内存区域-应用vue数据--渲染页面
//html模板解析---替换DOM内
//两个参数,第一个元素,第二个实例,将vue数据解析到页面上
function Compile(element,vm){
//获取页面元素
vm.$el=document.querySelector(element);
//将子节点放入临时内存区域
const fragement =document.createDocumentFragment();
let child
while(child=vm.$el.firstChild){
fragement.append(child)
}
fragement_compile(fragement)
//修改fragement里面的内容,替换文档碎片内容,
//操作节点类型为3的文本节点
function fragement_compile(node){
//修改插值表达式内容,使用正则表达式
const pattern = /\{\{s*(\S+)\s*\}\}/
if(node.nodeType === 3){
const xxx= node.nodeValue
const result_regex=pattern.exec(node.nodeValue)
if(result_regex){
const arr=result_regex[1].split('.');
const value = arr.reduce(
(total,cur)=>total[cur],vm.$data
)
node.nodeValue=xxx.replace(pattern,value)
//创建订阅者实例,这个时候修改内容(替换文档碎片时)
new Watcher(vm,result_regex[1],newValue=>{
node.nodeValue=xxx.replace(pattern,newValue)
})
}
return
}
if(node.nodeType === 1 && node.nodeName === 'INPUT'){
const attr = Array.from(node.attributes)
attr.forEach(i=>{
if(i.nodeName ==='v-model'){
//对应属性名为i.nodeValue
const value =i.nodeValue.split('.').reduce(
(total,cur) =>total[cur],vm.$data
)
node.value=value
//创建订阅者实例,这个时候修改内容(替换文档碎片时)
//数据改变视图
new Watcher(vm,i.nodeValue,newValue=>{
node.value=newValue
})
node.addEventListener('input',e=>{
//['more','like']
const arr1 = i.nodeValue.split('.')
//['more']
const arr2 =arr1.slice(0,arr1.length-1)
//vm.$data.more
const final =arr2.reduce(
(total,cur) => total[cur],vm.$data
)
//vm.$data.more['like']=e.target.value
final[arr1[arr1.length-1]]=e.target.value
})
}
})
}
node.childNodes.forEach(child=>fragement_compile(child))
}
//将文档碎片应用到页面
vm.$el.append(fragement)
}
//3.数据变动进行及时更新:发布者-订阅者模式(发布者有内容更新就通知到订阅者)
//依赖---收集和通知订阅者
class Dependency{
constructor(){
//用来存放订阅者
this.subscribers=[]
}
addSub(sub){
this.subscribers.push(sub)
}
notify(){
this.subscribers.forEach(sub=>sub.update())
}
}
//订阅者
class Watcher{
//更新哪些数据,需要用到vue上的实例
// callback 记录如何更新文本内容
constructor(vm,key,callback){
//参数都负值给实例
this.vm=vm
this.key=key
this.callback=callback
//临时属性,触发getter
//可以在触发getter的时候来添加订阅者到订阅者数组里面
Dependency.temp=this
key.split('.').reduce((total,cur)=>total[cur],vm.$data)
// 防止订阅者多次加入依赖实例的数组
Dependency.temp=null
}
//更新适宜自己的内容,发布者通知订阅者可以更新了
update(){
//获取属性值
const value = this.key.split('.').reduce(
(total,cur) => total[cur],this.vm.$data
)
this.callback(value)
}
}