记录一下自己对vue双向绑定的理解
百度搜索vue双向绑定 你将会发现有一大堆的文章 之前我也看过了很多 发现大多都写的非常的专业
我并不是说这样不好 但是对于一个又菜又水的码畜来说 真的不易理解和学习
所以我将自己的理解记录下来 以后忘记的时候希望它就像一把钥匙一样能够打开这扇大门
进入主题
vue双向绑定:
<body>
<div id="app">
<input type="text" v-model="msg">
{{msg}}
<input type="text" v-model="text">
</div>
</body>
1丶这不多说 div(#app) vue作用域 input标签:测试的主角
2丶使用:
let v = new V({
el:'app',
data:{
msg:'it is msg',
text:'hello boss'
}
})
一般当你去探索类似这种源码的时候我习惯从使用的那段代码入手 这可以让人很清楚vue就是一个构造函数 并且这个函数接收一个对象 这个对象里面包含了很多信息
1)作用域标签: 即你创建的这个vue实例管的是哪片区域
2)数据: 这部分是你自己定义的数据 当然你可以手动更改
3)其他: 这里我只记录双向绑定 其他的参数不是必须要有的(其实我其他相关的不懂 哈哈)
3丶vue:
function V(opt){
this.$el = document.getElementById(opt.el)
this.$data = opt.data;
this.$vm = this;
let self = this; //保存上下文
Object.keys(opt.data).forEach(key=>{
//简化数据的获取 vm.$data.msg --> vm.msg
init(key)
//劫持每个属性 并创建订阅对象
Obersver(opt.data,key,opt.data[key]);
})
function init(key){
Object.defineProperty(self,key,{
set(val){
self.$data[key] = val;
},
get(){
return self.$data[key];
}
})
}
new Complier(this.$el,this.$vm) //解析模板
}
逐句分析代码的作用
1) this.$el = document.getElementById(opt.el) 将作用域元素挂载到实例上
2) this.$data = opt.data; 将数据挂载到实例上
3)
Object.keys(opt.data).forEach(key=>{
//简化数据的获取 vm.$data.msg --> vm.msg
init(key)
//劫持每个属性 并创建订阅对象
Obersver(opt.data,key,opt.data[key]);
})
这段代码就是将数据里的属性逐一取出来做处理 做了什么处理呢 主要分为两个方面
1> 注释上写的很清楚了 简化数据的获取 当我们创建了实例的时候想要去获取挂载在上面的数据我们可以通过
vm.$data.msg (假设vm是我们创建的实例) 而init函数就将这个过程变的更简单了 只需要vm.msg就可以获取了 我们来看下init函数的代码:
function init(key){
Object.defineProperty(self,key,{
set(val){
self.$data[key] = val;
},
get(){
return self.$data[key];
}
})
}
这里的self就是vue的实例 利用Object.defineProperty方法劫持实例的每个属性 注意这里不是数据的属性而是实例的属性 set方法设置的其实就是数据的属性 self.$data[key] = val; 而get方法获取的同样是数据的属性self.$data[key]; 这样就实现了之前的功能
2> Obersver(opt.data,key,opt.data[key]); 这又是一个函数 接收的参数为 数据 属性 和 对应的属性值
具体干嘛用呢 我感觉就是监听的作用 监听数据属性的值是否被更改 如果被更改就做相应的处理
function Obersver(data,key,val){
//创建发布者(存放订阅者)
var dep = new Dep();
Object.defineProperty(data,key,{
set(newVal){
if(val === newVal) return;
data[key] = newVal;
//发布 从而更新视图
dep.emit();
},
get(){
//因为dep在Obersver作用域下 所以添加订阅者的操作需要在该作用域下完成
//将订阅者对象绑定在Dep上
if(Dep.target){
dep.addListener(Dep.target)
}
//清空缓存
Dep.target = null
return val;
},
})
}
代码有点长不过没事 一句句来分割:
分为3块:
1> 首先需要明白发布订阅模式 这个往深了讲百度里一大堆 而且很晦涩 这里我不举其他的例子 只解释在这个案例中具体发布订阅扮演的角色 首先看发布者 就是一个构造器实例化的对象 构造函数如下:
function Dep(){
let self = this;
//存放订阅者的数组
this.listenerList = [];
//增加订阅者
this.addListener = function(obj){
self.listenerList.push(obj)
}
//发布
this.emit = function(){
self.listenerList.forEach(obj=>{
obj.update();
})
}
}
逻辑很清楚
1 存放订阅者的数组
2 增加订阅者方法
3 一个方法: 当发生了某个事件(这里指的就是属性值的改变)执行 然后做相应的处理(改变页面)
订阅者:这里就先不看订阅者 后面马上就会遇到 你只需要明白 这个订阅者也是一个对象 它是和input标签相关的对象 因为绑定的双方就是数据的属性和input元素 当属性改变时 会触发相关的方法去改变input的value
我们继续往下:
Object.defineProperty(data,key,{})同样是这个方法 劫持对象属性和值
set(newVal){
if(val === newVal) return;
data[key] = newVal;
//发布 从而更新视图
dep.emit();
},
当属性值发送改变时 dep.emit(); 改变input的value值 后面再讨论
get(){
//因为dep在Obersver作用域下 所以添加订阅者的操作需要在该作用域下完成
//将订阅者对象绑定在Dep上
if(Dep.target){
dep.addListener(Dep.target)
}
//清空缓存
Dep.target = null
return val;
},
这里有个比较难懂的地方 get是获取对象的属性值 但是在这里做了其他的操作 就是将订阅者添加进了发布者的数组里面 为什么要在这里完成呢 因为发布者是在函数Obersver里面被实例化的 是在Obersver这个函数的作用域内 所以在这里访问dep对象才行 同样将订阅者对象绑定在Dep上也是同理 因为只有这样才能拿到这个订阅者对象 至于Dep.target = null 清空缓存就很好理解了
new Complier(this.$el,this.$vm) //解析
这个就很好理解了 具体过程可以自己实现 因为有了作用域元素对象 遍历里面的元素找到设置了v-model的那个input标签 然后实例化一个订阅者对象 将相关的input的 value 和 v-model 具体绑定的哪个属性挂载到上面即可
function Watcher(node,key,vm){
this.node = node; //保存重要属性
//更新视图方法
this.update = function(){
this.node.value = vm[key]; //获取最新的数据
}
this.get = function (){
Dep.target = this;
return vm[key]; //由此触发属性get函数添加订阅者
}
this.value = this.get();
}
很容易就可以明白 订阅者对象的node属性就是input标签 当发生数据的属性值改变的时候 就执行update方法
更改了value值
this.get = function (){
Dep.target = this;
return vm[key]; //由此触发属性get函数添加订阅者
}
这个get方法倒是值得一讲 也非常巧妙 首先将订阅者对象即自己挂载到Dep上 去访问vue实例的数据的属性值来触发 之前通过Object.defineProperty(data,key,{})方法里的get方法 将订阅者加进了发布者对象里面
this.value = this.get();执行这个方法
当然这里只是实现了一个绑定就是数据到标签的绑定 而对于标签到数据 这就非常easy了 通过input事件就可以做到 回调里面直接去修改vue里的数据的属性值
模板解析方法我也同样贴了出来 可以自己看看哈
//模板解析方法
function Complier(dom,vm){
this.dom = dom; //作用域标签
this.vm = vm; //vue实例
//dom片段
let fragment = document.createDocumentFragment();
let child;
//明白firstChild属性 appendChild属于剪切效果
while(child = dom.firstChild){
fragment.appendChild(child)
}
//解析
complie(fragment);
//解析完毕 将dom片段重新加载进页面
this.dom.appendChild(fragment);
//解析指令 (需要递归解析)
function complie(fragment){
let childs = fragment.childNodes; //类数组
childs = Array.from(childs)
childs.forEach(item=>{
//nodeType 3:文本节点 1:元素节点
if(item.nodeType == 1){
//获取所有元素属性
let attrs = Array.from(item.attributes)
//这个例子只解析v-model
attrs.forEach(attr=>{
if(attr.name == 'v-model'){
let val = item.getAttribute('v-model')
//将这个标签定义为订阅者 这里val就是msg
new Watcher(item,val,vm); //创建一个订阅者
item.value = vm[val]; //数据替换模板 这是第一次解析 后面则通过监听实现
//视图更新数据 (1绑 视图到数据)
item.addEventListener('input',function(e){
vm[val] = e.target.value;
})
}
})
}else if(item.nodeType == 3){ //文本节点解析 (问题:{{msg}}+{{text}}解析)
let reg = /\{\{(.+)\}\}/
if(reg.test(item.nodeValue)){
item.nodeValue = vm[RegExp.$1]
}
}
})
}
}
总结:
利用作用域元素解析模板 找到绑定的input标签 创建一个订阅者 创建的时候直接添加进发布者的数组里面
监听数据的每个属性 一旦发送改变就让发布者执行发布命令 修改订阅者对象里包含的input标签的相关值(这里指的就是value值)
比较巧妙的就是Object.defineProperty(data,key,{})的get方法将订阅者添加进来 在实例化订阅者的时候去触发这个方法 将自己添加进去