如何在vue2.0中实现双向数据绑定?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
//发布订阅模式 function Dep() { this.subs = []//收集依赖(也就是手机watcher实例), } Dep.prototype.addSub = function (sub) { //添加订阅者 this.subs.push(sub); //实际上添加的是watcher这个实例 } Dep.prototype.notify = function (sub) { //发布,这个方法的作用是遍历数组,让每个订阅者的update方法去执行 this.subs.forEach((sub) => sub.update()) } function Watcher(fn) { this.fn = fn; } Watcher.prototype.update = function () { //添加一个update属性让每一个实例都可以继承这个方法 this.fn(); } let watcher = new Watcher(function () { alert(1) });//订阅 let dep = new Dep(); dep.addSub(watcher);//添加依赖,添加订阅者 dep.notify();//发布,让每个订阅者的update方法执行
只是针对双向数据绑定做说明
<template> <div id="app"> <div>obj.text的值:{{obj.text}}</div> <p>word的值:{{word}}</p> <input type="text" v-model="word"> </div> </template> <script> new Vue({ el: "#app", data: { obj: { text: "向上", }, word: "学习" }, methods:{ // ... } }) </script>
Vue构造函数都干什么了?
function Vue(options = {}) { this.$options = options;//接收参数 var data = this._data = this.$options.data; observer(data);//对data中的数据进型循环递归绑定 for (let key in data) { let val = data[key]; observer(val); Object.defineProperty(this, key, { enumerable: true, get() { return this._data[key]; }, set(newVal) { this._data[key] = newVal; } }) } new Compile(options.el, this) };
在new Vue({…})构造函数时,首先获取参数options,然后把参数中的data数据赋值给当前实例的_data属性上(this._data = this.$options.data),重点来了,那下面的遍历是为什么呢?首先我们在操作数据的时候是this.word获取,而不是this._data.word,所以是做了一个映射,在获取数据的时候this.word,其实是获取的this._data.word的值,大家可以在自己项目中输出this查看一下
1.接下来看看observer方法干了什么
function observer(data) { if (typeof data !== "object") return; return new Observer(data);//返回一个实例 } function Observer(data) { let dep = new Dep();//创建一个dep实例 for (let key in data) {//对数据进行循环递归绑定 let val = data[key]; observer(val); Object.defineProperty(data, key, { enumerable: true, get() { Dep.target && dep.depend(Dep.target);//Dep.target就是Watcher的一个实例 return val; }, set(newVal) { if (newVal === val) { return; } val = newVal; observer(newVal); dep.notify() //让所有方法执行 } }) } }
Observer构造函数,首先let dep=new Dep(),作为之后的触发数据劫持的get方法和set方法时,去收集依赖和发布时调用,主要的操作就是通过Object.defineProperty对data数据进行循环递归绑定,使用getter/setter修改其默认读写,用于收集依赖和发布更新。
2.再来看看Compile具体干了那些事情
function Compile(el, vm) { vm.$el = document.querySelector(el); let fragment = document.createDocumentFragment(); //创建文档碎片,是object类型 while (child = vm.$el.firstChild) { fragment.appendChild(child); };//用while循环把所有节点都添加到文档碎片中,之后都是对文档碎片的操作,最后再把文档碎片添加到页面中,这里有一个很重要的特性是,如果使用appendChid方法将原dom树中的节点添加到fragment中时,会删除原来的节点。 replace(fragment); function replace(fragment) { Array.from(fragment.childNodes).forEach((node) => {//循环所有的节点 let text = node.textContent; let reg = /\{\{(.*)\}\}/; if (node.nodeType === 3 && reg.test(text)) {//判断当前节点是不是文本节点且符不符合{{obj.text}}的输出方式,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher) console.log(RegExp.$1); //obj.text let arr = RegExp.$1.split("."); //转换成数组的方式[obj,text],方便取值 let val = vm; arr.forEach((key) => { //实现取值this.obj.text val = val[key]; }); new Watcher(vm, RegExp.$1, function (newVal) { node.textContent = text.replace(/\{\{(.*)\}\}/, newVal) }); node.textContent = text.replace(/\{\{(.*)\}\}/, val); //对节点内容进行初始化的赋值 } if (node.nodeType === 1) { //说明是元素节点 let nodeAttrs = node.attributes; Array.from(nodeAttrs).forEach((item) => { if (item.name.indexOf("v-") >= 0) {//判断是不是v-model这种指令 node.value = vm[item.value]//对节点赋值操作 } //添加订阅者 new Watcher(vm, item.value, function (newVal) { node.value = vm[item.value] }); node.addEventListener("input", function (e) { let newVal = e.target.value; vm[item.value] = newVal; }) }) } if (node.childNodes) { //这个节点里还有子元素,再递归 replace(node); } }) } //这是页面中的文档已经没有了,所以还要把文档碎片放到页面中 vm.$el.appendChild(fragment); }
Compile(编译方法)
首先解释一下DocuemntFragment(文档碎片)它是一个dom节点收容器,当你创造了多个节点,当每个节点都插入到文档当中都会引发一次回流,也就是说浏览器要回流多次,十分耗性能,而使用文档碎片就是把多个节点都先放入到一个容器中,最后再把整个容器直接插入就可以了,浏览器只回流了1次。
Compile方法首先遍历文档碎片的所有节点,1.判断是否是文本节点且符不符合{{obj.text}}的双大括号的输出方式,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher),new Watcher(vm,动态绑定的变量,回调函数fn) 2.判断是否是元素节点且属性中是否含有v-model这种指令,如果满足条件说明它是双向的数据绑定,要添加订阅者(watcher),new Watcher(vm,动态绑定的变量,回调函数fn) ,直至遍历完成。
最后别忘了把文档碎片放到页面中
var uid=0; //发布订阅 function Dep() { this.id=uid++; this.subs = []; } Dep.prototype.addSub = function (sub) { //订阅 this.subs.push(sub); //实际上添加的是watcher这个实例 } Dep.prototype.depend = function () { // 订阅管理器 if(Dep.target){//只有Dep.target存在时采取添加 Dep.target.addDep(this); } } Dep.prototype.notify = function (sub) { //发布,遍历数组让每个订阅者的update方法去执行 this.subs.forEach((sub) => sub.update()) }
Dep构造函数内部有一个id和一个subs,id=uid++ ,id用于作为dep对象的唯一标识,subs就是保存watcher的数组。depend方法就是一个订阅的管理器,会调用当前watcher的addDep方法添加订阅者,当触发数据劫持(Object.defineProperty)的get方法时会调用Dep.target && dep.depend(Dep.target)添加订阅者,当数据改变时触发数据劫持(Object.defineProperty)的set方法时会调用dep.notify方法更新操作。
function Watcher(vm, exp, fn) { this.fn = fn; this.vm = vm; this.exp = exp // this.newDeps = []; this.depIds = new Set(); this.newDepIds = new Set(); Dep.target = this; //this是指向当前(Watcher)的一个实例 let val = vm; let arr = exp.split("."); arr.forEach((k) => { //取值this.obj.text val = val[k] //取值this.obj.text,就会触发数据劫持的get方法,把当前的订阅者(watcher实例)添加到依赖中 }); Dep.target = null; } Watcher.prototype.addDep = function (dep) { var id=dep.id; if(!this.newDepIds.has(id)){ this.newDepIds.add(id); this.newDeps.push(dep); if(!this.depIds.has(id)){ dep.addSub(this); } } } Watcher.prototype.update = function () { //这就是每个绑定的方法都添加一个update属性 let val = this.vm; let arr = this.exp.split("."); arr.forEach((k) => { val = val[k] //取值this.obj.text,传给fn更新操作 }); this.fn(val); //传一个新值 }
Watcher构造函数干了什么
1 接收参数,定义了几个私有属性( this.newDep ,this.depIds
,this.newDepIds)
2. Dep.target = this,通过参数进行data取值操作,这就会触发Object.defineProperty的get方法,它会通过订阅者管理器(dep.depend())添加订阅者,添加完之后再将Dep.target=null置为空;
3.原型上的addDep是通过id这个唯一标识,和几个私有属性的判断防止订阅者被多次重复添加
4.update方法就是当数据更新时,dep.notify()执行,触发订阅者的update这个方法, 执行发布更新操作。
vue2.0中双向数据绑定,简单来说就是Observer、Watcher、Dep三大部分;
1.首先用Object.defineProperty()循环递归实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;
2.在编译的时候,创建文档碎片,把所有节点添加到文档碎片中,遍历文档碎片的所有结点,如果是{{}},v-model这种,new Watcher()实例并向dep的subs数组中添加该实例
3.最后修改值就会触发Object.defineProperty()的set方法,在set方法中会执行dep.notify(),然后循环调用所有订阅者的update方法更新视图。
关于如何在vue2.0中实现双向数据绑定问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注亿速云行业资讯频道了解更多相关知识。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。