Vue双向绑定
Obejct.defineProperty
的setter
/getter
和发布订阅
Vue双向绑定原理
- 1.实现一个数据监听器
Observer()
,能够对数据对象的所有属性进行监听,如有变动可以拿到最新值并通知订阅者 - 2.实现一个指令解析器
Compile()
,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数 - 3.实现一个
Watcher()
,作为连接Observer
和Compile
的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
Vue双向绑定实现
1.实现Observer
- 利用
Obeject.defineProperty()
来监听属性变动 - 需要将observer的数据进行递归遍历,包括子属性对象的属性,都加上
setter
和getter
- 给某个对象赋值,就会触发
setter
,那么就能监听到数据变化,通过notify()
发布出去
function observer(data, vm){
if(typeof data !== 'object') return
Object.keys(data).forEach(function(key){
defineReactive(vm, key, data[key])
})
}
function defineReactive(obj, key ,val){
var dep = new Dep()
Object.defineProperty(obj, key, {
get: function(){
// alert('属性监听 get '+Dep.target)
// // Watcher的实例调用了getter 添加订阅者watcher
if(Dep.target) dep.addSub(Dep.target)
return val
},
set: function(newVal){
// alert('属性监听 set'+newVal)
if(newVal === val){
return
}else{
val = newVal
//作为发布者发出通知
dep.notify()
}
}
})
}
这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,就是一个数组用来收集订阅者,数据变动触发notify
,再调用订阅者的update
方法
function Dep(){
//定义subs数组存储watcher
this.subs = []
}
Dep.prototype.addSub = function(sub){
this.subs.push(sub)
}
Dep.prototype.notify = function(){
this.subs.forEach(function(sub){
sub.update()
})
}
2.实现Compile
-
compile
主要是解析模板指令,将模板的变量替换成数据,然后初始化渲染页面视图 -
并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变化,收到通知,更新视图
function compile(node, vm){
//指令 v- 模板引擎 {{}}
var reg = /\{\{(.*)\}\}/;
// 判断节点类型 nodeType 元素1 文本3
if(node.nodeType === 1){
var attr = node.attributes
for(var i = 0; i < attr.length; i++){
if(attr[i].nodeName == 'v-model'){
var name = attr[i].nodeValue//获取v-model绑定的属性名
// console.log(name)//text
node.addEventListener('input', function(e){
console.log(vm)
vm[name] = e.target.value
})
// 给相应的data属性赋值,触发该属性的set方法
node.value = vm.data[name] //将data值赋值给node
node.removeAttribute('v-model')
}
}
}
//节点类型是文本
if(node.nodeType === 3){
if(reg.test(node.nodeValue)){
var name = RegExp.$1 //获取过来{{text}} =>text
name = name.trim()//trim
// node.nodeValue = vm.data[name] //将data.text => {{text}}
new Watcher(vm, node, name); //这里改成订阅者形式,
}
}
}
3.实现Watcher
Watcher
订阅者为Observer
和Compile
之间通信的桥梁,主要做的事
- 在
Compile
中实例化时 - 往属性订阅器(dep)里添加在自己
- 自身必须有一个
update()
方法 - 当数据变动是接到
dep
属性订阅器的notify
发布通知时,能够调用自身的update()
方法,从而触发get
方法去更新数据
//定义wactch
function Watcher(vm, node, name){
// console.log(this)
this.vm = vm
this.node = node
this.name = name
this.update()
}
Watcher.prototype = {
update: function () {
this.get();
this.node.nodeValue = this.value;
},
// 获取data中的属性值 加入到dep中
get: function () {
Dep.target = this
this.value = this.vm[this.name];
Dep.target = null
}
}
Vue双向绑定效果
vue双向绑定完整代码
HTML
<div id='app'>
<input v-model='text'>
{{ text }}
<span></span>
</div>
<script src="vue.0.0.1.js"></script>
<script >
var app = new Vue({
el: 'app',
data: {
text: "this is test text",
message: {
name: "weiong"
}
}
})
JS
'use strict';
(function(global, factory){
global.Vue = factory();
})(this, function(){
var Vue = function(options){
//挂载的节点
var id = options.el||"body";
// console.log(id)
//模板数据
this.data = options.data || {}
var data = this.data
//监听模板数据
observer(data, this)
//节点劫持到一个DOM容器
var dom = nodeTodocumentfragment(document.getElementById(id), this)
//最后挂载
document.getElementById(id).appendChild(dom)
}
function Dep(){
//定义subs数组存储watcher
this.subs = []
}
Dep.prototype.addSub = function(sub){
this.subs.push(sub)
}
Dep.prototype.notify = function(){
this.subs.forEach(function(sub){
sub.update()
})
}
//定义wactch
function Watcher(vm, node, name){
// console.log(this)
this.vm = vm
this.node = node
this.name = name
this.update()
}
Watcher.prototype = {
update: function () {
// alert('Watcher update')
this.get();
this.node.nodeValue = this.value;
},
// 获取data中的属性值 加入到dep中
get: function () {
// alert('Watcher get')
Dep.target = this
this.value = this.vm[this.name]; // 触发相应属性的getter,从而添加订阅者
Dep.target = null
}
}
//数据监听 obj是data对象
function observer(data, vm){
if(typeof data !== 'object') return
Object.keys(data).forEach(function(key){
defineReactive(vm, key, data[key])
})
}
function defineReactive(obj, key ,val){
var dep = new Dep()
Object.defineProperty(obj, key, {
get: function(){
// alert('属性监听 get '+Dep.target)
// // Watcher的实例调用了getter 添加订阅者watcher
if(Dep.target) dep.addSub(Dep.target)
return val
},
set: function(newVal){
// alert('属性监听 set'+newVal)
if(newVal === val){
return
}else{
val = newVal
//作为发布者发出通知
dep.notify()
}
}
})
}
//节点劫持
//documentfragment DOM 容器
function nodeTodocumentfragment(obj, vm){
var flag = document.createDocumentFragment()
var child
// appendChild 成功后,会把节点从原来的节点位置移除;
// 中转站
while(child = obj.firstChild){
// console.log(child)
// 扫描 节点劫持 model数据模板编译
compile(child, vm)
flag.appendChild(child)
}
// console.log(flag)
return flag
}
// compile扫描每一个子节点
function compile(node, vm){
//指令 v- 模板引擎 {{}}
var reg = /\{\{(.*)\}\}/;
// 判断节点类型 nodeType 元素1 文本3
if(node.nodeType === 1){
var attr = node.attributes
for(var i = 0; i < attr.length; i++){
if(attr[i].nodeName == 'v-model'){
var name = attr[i].nodeValue//获取v-model绑定的属性名
// console.log(name)//text
node.addEventListener('input', function(e){
console.log(vm)
vm[name] = e.target.value
})
// 给相应的data属性赋值,触发该属性的set方法
node.value = vm.data[name] //将data值赋值给node
node.removeAttribute('v-model')
// alert('节点赋值')
}
}
}
//节点类型是文本
if(node.nodeType === 3){
if(reg.test(node.nodeValue)){
var name = RegExp.$1 //获取过来{{text}} =>text
name = name.trim()//trim
// node.nodeValue = vm.data[name] //将data.text => {{text}}
// alert('文本赋值 new Watcher')
new Watcher(vm, node, name); //这里改成订阅者形式,
}
}
}
return Vue
})
网友评论