什么是双向绑定?
简单来说就是把数据绑定到视图层。也就是随着数据改变,视图层对应的值会改变。一般做到这里就是单向绑定了。
如果当视图层值(比如input标签里面的值)改变时候,它对应绑定的数据值也会改变,那么就实现了双向绑定。
简单概括来就是:
- 数据变化后更新视图
- 视图变化后更新数据
分部实现双向绑定
- 数据劫持
- 发布订阅监听
- 数据渲染
数据劫持,数据渲染。就是用正则表达式,遍历节点找到差值表达式{{}}.找到对应数据值。去替换,渲染。
发布订阅用来实时监听,一旦数据发生改变就进行数据劫持,数据渲染。当然可以用addEventListening,去监听视图层的变化。
废话不多说。直接上代码;
HTML部分
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<span>黑黑:{{name}}</span>
<input type="text" v-model="name">
<span>黑黑的npy(from future)like:{{npy.like}}</span>
<input type="text" v-model="npy.like">
</div>
<script src="./myvue.js"></script>
//引用自己实现的vue
<script >
//创建vue实例
let vm = new Myvue(
{
el:'#app',
data:{
name:'heihei',
npy:{
like:'dance'
}
}
}
)
</script>
</body>
</html>
html文件中没用太复杂的东西。下面来看myvue的实现。
如图所示为大致框架。
vue类
//初始化时获取vue实例,赋值给$data,然后调用Observer,Compile函数;
class Myvue {
constructor(vue_instance){
this.$data = vue_instance.data;
//监听数据
Observer(this.$data)
//解析模版
Compile(vue_instance.el,this);
}
}
Observer函数
如下所示,主要采用Object.defineProperty中set,get对数据进行操作。vue3中则采用proxy(),代理模式对数据处理。同时采用递归dfs,对data中深层次数据进行监控。
function Observer(data){
//递归出口
if(typeof data !== 'object'|| !data ) return;
const dependency = new Dependency();
//这里是调用了发布者订阅,创建了实例。
Object.keys(data).forEach(
(item)=>{
let value = data[item];
//递归调用
Observer(value)
Object.defineProperty(data,item,{
enumerable:true,
configurable:true,
get(){
//初始化的时候就会添加订阅,加入到订阅者队列。
Dependency.temp && dependency.addSub(Dependency.temp)
return value
},
set(newvalue){
//修改时候监听新数据
Observer(newvalue)
value = newvalue
//修改的时候通知订阅者,实时修改。
dependency.notify()
}
})
}
)
}
发布者
//发布者
class Dependency{
constructor(){
//订阅者队列
this.subscribers = [];
}
addSub(sub){
//添加订阅者
this.subscribers.push(sub)
}
notify(){
//通知所有订阅者,修改数据
this.subscribers.forEach(sub => {
sub.update()
})
}
}
订阅者
//订阅者
class Watcher {
constructor(vm,key,callback){
//初始化,没个订阅者自身有三个数据,vue实例,data的某个属性值(这个属性应当是唯一的),回调函数
this.vm = vm;
this.key = key;
this.callback = callback;
//注意⚠️
Dependency.temp = this;
//这里为了触发get,从而触发订阅加到数组里面;
key.split('.').reduce(
(total,current) => total[current],vm.$data
)
//加完立即清除确保单例
Dependency.temp = null
}
update(){
//更新数据函数,reduce去解决
let value = this.key.split('.').reduce(
(total,current) => total[current],this.vm.$data
)
this.callback(value);
}
}
注意1:这里是为了添加订阅者到订阅数组,而我们的添加方法只在get里面调用,但是又不能重复添加,所以设置变量,这个变量消除之前遍历一边data的值。从而达成每个监听的变量只会放入到订阅者数组中一次。
模版解析
遍历整个html,在遍历过程中,去劫持数据,增加监听。
function Compile(root,vm){
vm.$el = document.querySelector(root);
const fragment = document.createDocumentFragment();
let child;
while(child = vm.$el.firstChild ){
fragment.appendChild(child)
}
//上面部分是获取所有节点
fragment_instead(fragment);
//解析节点函数
function fragment_instead(node){
const pattern = /\{\{\s*(\S+)\s*\}\}/
//3是文本节点
if(node.nodeType === 3){
const oldValue = node.nodeValue
const res = node.nodeValue.match(pattern)
if(res){
const arr = res[1].split('.');
const value =arr.reduce(
(total,current) => total[current],vm.$data
)
node.nodeValue = oldValue.replace(pattern,value);
//就是这里创建了订阅者实例
new Watcher(vm,res[1],(newvalue)=>{
node.nodeValue = oldValue.replace(pattern,newvalue)
})
}
return
}
//以上是获取差值表达式内容,用数据去替换,结合发布订阅实现数据响应。
if(node.nodeType===1 && node.nodeName ==='INPUT'){
const ary =Array.from(node.attributes)
ary.forEach((i)=>{
if(i.nodeName ==='v-model'){
let value = i.nodeValue.split('.').reduce(
(total,current)=>total[current],vm.$data
)
node.value =value
new Watcher(vm,i.nodeValue,(newvalue)=>{
node.value = newvalue
})
//监听视图层数据,并修改data(这里以input为例)
node.addEventListener('input',e=>{
const arr1 = i.nodeValue.split('.');
const arr2 = arr1.slice(0,arr1.length-1);
const final =arr2.reduce((total,current)=>total[current],vm.$data)
final[arr1[arr1.length-1]] = e.target.value;
})
}
})
}
node.childNodes.forEach(child=> fragment_instead(child))
//递归调用,处理深层次数据。
}
//最后将操作好的节点重新渲染
vm.$el.appendChild(fragment)
}
结果
初视图
修改后的图