vue双向数据绑定

更新时间:2023-06-09 10:50:11 阅读: 评论:0

mockingbirdvue双向数据绑定
本⽂能帮你做什么?
1、了解vue的双向数据绑定原理以及核⼼代码模块
2、缓解好奇⼼的同时了解如何实现双向绑定
为了便于说明原理与实现,本⽂相关代码主要摘⾃, 并进⾏了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也难免存在⼀些问题,欢迎⼤家指正。不过这些并不会影响⼤家的阅读和理解,相信看完本⽂后对⼤家在阅读vue源码的时候会更有帮助<
本⽂所有相关代码均在github上⾯可找到
相信⼤家对mvvm双向绑定应该都不陌⽣了,⼀⾔不合上代码,下⾯先看⼀个本⽂最终实现的效果吧,和vue⼀样的语法,如果还不了解双向绑定,猛戳
发布订阅者模式
dep类中有两个⽅法,
1添加订阅,2触发订阅(循环数组中的每⼀项)
watcher类中:
构造函数中挂载了⼀个更新数据⽅法回调函数cb,(在实例构造函数时需要传递进来),最新的数据对象vm,需要更新的属性key
并定义了⼀个更新函数,调⽤上⾯的回调函数cb
在什么时候创建watcher实例呢?
在编译器中当数据变化进⾏更新的时候,需要创建⼀个watchers实例
怎么将订阅者添加到订阅器中呢?
在watcher类的构造函数中添加如下三⾏代码:1.订阅器的target属性等于当前的watcher
2.对当前的值key通过第⼆⾏代码进⾏获取(此时会调⽤get函数)
3.另dep.target=null
4.在get函数中判断dep.target如果存在,则调⽤添加订阅者函数
编译器:
获取vue实例中el的位置,将data中的数据填充进el中,然后将结果渲染出来
vue双向数据绑定原理
根据最新的数据渲染⾃⾝的结构
complie编译器:1.将dom中的节点放⼊,⽂档碎⽚中
2.在⽂档碎⽚中进⾏对应的处理编写处理函数replace(),2.1.递归的获取⽂档碎⽚节点中的⽂本节点(需要更新的节点 如)
2.2.对⽂本⼦节点进⾏正则匹配与提取  ,替换
3.将⽂档碎⽚重新移⼊dom节点,进⾏渲染
频繁对dom节点进⾏操作会不停的触发重绘和重排,所以需要使⽤⽂档碎⽚(将节点元素存⼊内存中,这样页⾯上就没有dom元素了,随意修改它也不会触发重绘和重排,
对⽂档碎⽚进⾏编译,再将结果重新渲染到页⾯上),
正则表达式中:空⽩字符⽤/s* , *表⽰0个或多个
⾮空⽩字符⽤/S表⽰
<div id="mvvm-app">
<input type="text" v-model="word">
<p>{{word}}</p>
<button v-on:click="sayHi">change model</button>
</div>
<script src="./js/obrver.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
var vm = new MVVM({
el: '#mvvm-app',
data: {
word: 'Hello World!'
},
methods: {
sayHi: function() {
this.word = 'Hi, everybody!';
}
}
});
</script>
过期效果:
⼏种实现双向绑定的做法
⽬前⼏种主流的mvc(vm)框架都实现了单向数据绑定,⽽我所理解的双向数据绑定⽆⾮就是在单向绑定的基础上给可输⼊元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多⾼深。所以⽆需太过介怀是实现的单向或双向绑定。
绳子英文实现数据绑定的做法有⼤致如下⼏种:
发布者-订阅者模式(backbone.js)
脏值检查(angular.js)
数据劫持(vue.js)
发布者-订阅者模式: ⼀般通过sub, pub的⽅式实现数据和视图的绑定监听,更新数据⽅式通常做法是v
m.t('property', value),这⾥有篇⽂章讲的⽐较详细,有兴趣可点
这种⽅式现在毕竟太low了,我们更希望通过vm.property = value 这种⽅式更新数据,同时⾃动更新视图,于是有了下⾯两种⽅式
脏值检查: angular.js 是通过脏值检测的⽅式⽐对数据是否有变更,来决定是否更新视图,最简单的⽅式就是通过tInterval()定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进⼊脏值检测,⼤致如下:
DOM事件,譬如⽤户输⼊⽂本,点击按钮等。( ng-click )
XHR响应事件 ( $http )
浏览器Location变更事件 ( $location )
Timer事件( $timeout , $interval )
执⾏ $digest() 或 $apply()
数据劫持: vue.js 则是采⽤数据劫持结合发布者-订阅者模式的⽅式,通过Object.defineProperty()来劫持各个属性的tter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
思路整理
已经了解到vue是通过数据劫持的⽅式来做数据绑定的,其中最核⼼的⽅法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的⽬的,⽆疑这个⽅法是本⽂中最重要、最基础的内容之⼀,如果不熟悉defineProperty,猛戳
整理了⼀下,要实现mvvm的双向绑定,就必须要实现以下⼏点:
1、实现⼀个数据监听器Obrver,能够对数据对象的所有属性进⾏监听,如有变动可拿到最新值并通知订阅者
2、实现⼀个指令解析器Compile,对每个元素节点的指令进⾏扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现⼀个Watcher,作为连接Obrver和Compile的桥梁,能够订阅并收到每个属性变动的通知,执⾏指令绑定的相应回调函数,从⽽更新视图
4、mvvm⼊⼝函数,整合以上三者
上述流程如图所⽰:
1、实现Obrver
ok, 思路已经整理完毕,也已经⽐较明确相关逻辑和模块功能了,let's do it
冰雪奇缘2彩蛋我们知道可以利⽤Obeject.defineProperty()来监听属性变动
那么将需要obrve的数据对象进⾏递归遍历,包括⼦属性对象的属性,都加上tter和getter
这样的话,给这个对象的某个值赋值,就会触发tter,那么就能监听到了数据变化。。相关代码可以是这样:
var data = {name: 'kindeng'};
obrve(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq
function obrve(data) {
if (!data || typeof data !== 'object') {
return;
}
// 取出所有属性遍历,object.keys(data)得到对象的属性名数组,通过forEach()遍历属性名,属性名为参数key,data还是原来的对象
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
};
function defineReactive(data, key, val) {
obrve(val); // 监听⼦属性
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: fal, // 不能再define
guarantee是什么意思
get: function() {
寒暄return val;
},
//newVal为改变后的值,t⽅法中默认传递的参数
t: function(newVal) {
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
}
});
unemployed
}
这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来
我们需要实现⼀个消息订阅器,很简单,维护⼀个数组,⽤来收集订阅者,数据变动触发notify,再调⽤订阅者的update⽅法,代码改善之后是这样:
// ...val为data[key]对应的值
function defineReactive(data, key, val) {
var dep = new Dep();
obrve(val); // 监听⼦属性
Object.defineProperty(data, key, {
// ... 省略
t: function(newVal) {
if (val === newVal) return;
console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
val = newVal;
}
});
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上⾯的思路整理中我们已经明确订阅者应该是Watcher, ⽽且var dep = new Dep();是在defineReactive⽅法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在getter⾥⾯动⼿脚:
// Obrver.js
// ...省略
Object.defineProperty(data, key, {
get: function() {
// 由于需要在闭包内添加watcher,所以通过Dep定义⼀个全局target属性,暂存watcher, 添加完移除
Dep.target && dep.addSub(Dep.target);
return val;
}
// ... 省略
});
// Watcher.js
Watcher.prototype = {
get: function(key) {
Dep.target = this;
this.value = data[key];    // 这⾥会触发属性的getter,从⽽添加订阅者
Dep.target = null;
}
}
这⾥已经实现了⼀个Obrver了,已经具备了监听数据和数据变化通知订阅者的功能,。那么接下来就是实现Compile了
2、实现Compile
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页⾯视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,⼀旦数据有变动,收到通知,更新视图,如图所⽰:
因为遍历解析的过程有多次操作dom节点,为提⾼性能和效率,会先将跟节点el转换成⽂档碎⽚fragment进⾏解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中
function Compile(el) {
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
摇滚青春歌曲
if (this.$el) {
this.$fragment = de2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
init: function() { pileElement(this.$fragment); },
node2Fragment: function(el) {
var fragment = ateDocumentFragment(), child;
// 将原⽣节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
};
compileElement⽅法将遍历所有节点及其⼦节点,进⾏扫描解析编译,调⽤对应的指令渲染函数进⾏数据渲染,并调⽤对应的指令更新函数进⾏绑定,详看代码及注释说明:
Compile.prototype = {
// ... 省略
i too
compileElement: function(el) {
var childNodes = el.childNodes, me = this;
[].slice.call(childNodes).forEach(function(node) {
var text = Content;
var reg = /\{\{(.*)\}\}/;    // 表达式⽂本
// 按元素节点⽅式编译
if (me.isElementNode(node)) {
} el if (me.isTextNode(node) && st(text)) {
}
/
/ 遍历编译⼦节点
if (node.childNodes && node.childNodes.length) {
}
});
},
compile: function(node) {
var nodeAttrs = node.attributes, me = this;
[].slice.call(nodeAttrs).forEach(function(attr) {
// 规定:指令以 v-xxx 命名
// 如 <span v-text="content"></span> 中指令为 v-text
var attrName = attr.name;    // v-text
if (me.isDirective(attrName)) {
var exp = attr.value; // content
var dir = attrName.substring(2);    // text
if (me.isEventDirective(dir)) {
// 事件指令, 如 v-on:click
compileUtil.eventHandler(node, me.$vm, exp, dir);
} el {
// 普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
}
});
}
};
// 指令处理集合
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
},
// ...省略
bind: function(node, vm, exp, dir) {
var updaterFn = updater[dir + 'Updater'];
// 第⼀次初始化视图
updaterFn && updaterFn(node, vm[exp]);
// 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
new Watcher(vm, exp, function(value, oldValue) {
// ⼀旦属性值有变化,会收到通知执⾏此更新函数,更新视图
updaterFn && updaterFn(node, value, oldValue);
});
}
};
// 更新函数
var updater = {
textUpdater: function(node, value) {
}
// ...省略bothand
};

本文发布于:2023-06-09 10:50:11,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/90/139133.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:数据   订阅   绑定   属性   实现
相关文章
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图