1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > 深入理解Vue双向数据绑定

深入理解Vue双向数据绑定

时间:2021-12-25 20:05:21

相关推荐

深入理解Vue双向数据绑定

**

MVVM

**

Vue的双向数据绑定是指model(模型,也就是vue实例中的数据)和view(视图)的双向绑定,即一个发生改变,另一个也会改变。

首先了解一下什么是MVVM(model view viewmodel),在 MVVM 架构中,引入了 ViewModel 的概念,viewmodel相当于一个中间件。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。

以 Vue 框架来举例,ViewModel 就是组件的实例。View 就是模板,Model 的话在引入 Vuex 的情况下是完全可以和组件分离的。

整个变化到渲染的过程,大致就是:

首先通过数据劫持来监听data的数据变化,一旦监听到了数据发生变化,订阅发布开始获取数据,然后调用Dep的notify方法发布通知到每个订阅,订阅根据最新的数据渲染Dom。

- 三大核心

实现双向数据绑定主要是依据以下三大核心:

(1)observer 监听器

(2)watcher 订阅者

(3)compile 解析器

先创建一个vue实例:

const vm=new Vue({data() {return {msg: '111111',name: 'qwert',a: {b: 'bbbbb'}}},methods: {setName() {this.msg = '222';}},created() {this.msg = '212121';console.log('实例初始化完成')},mounted() {console.log('DOM挂载完成')}}).$mount('#app');

**

observer监听器

**

observer中有一个非常重要的函数Object.defineProperty(obj,prop,descriptor),它有三个参数:

• obj:要在其上定义属性的对象。

• prop:要定义或修改的属性的名称。

• descriptor:将被定义或修改的属性描述符

该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

我们通过Object.defineProperty( )设置了对data属性劫持,对其get和set进行重写操作,get就是我们在读取这个属性值时触发一个函数,我们一般会将watcher添加过程放到这个函数,set就是在设置data属性这个值触发函数。

组件挂载时,会遍历data中所有的属性,然后给每个属性利用Object.defineProperty()函数进行劫持,并对每个属性对应一个new Dep(),Dep是专门收集依赖、删除依赖、向依赖发送消息的,也就是对订阅者watcher做管理,毕竟一个data可以在多个地方使用,当数据改变那对应的不同的watcher都需要修改。

class Observer{constructor(data){this.$data = data;this.observer(this.$data);}observer(obj){// 如果data不是一个对象的话,提示错误,// 因为只有对象才能调用Object.definePropertyif(typeof obj !== 'object') return;Object.keys(obj).forEach(key =>{this.defineReactive(obj, key, obj[key]);//遍历对象的每一个属性都调用defineReactive方法进行劫持})}defineReactive(obj, key, value){// 判断如果当前的值还是对象的话,递归劫持if(typeof value === 'object') this.observer(value); let dep = new Dep();//新建一个订阅器,管理订阅者watcher们Object.defineProperty(obj, key, {get(){if(window.target){//当前Dep.target是指的Watcher(订阅者)实例,是来自于下面的Watcher类里的,//这里又将watcher放入订阅器dep中进行管理,实现了将监听这个数据变化的订阅者放到订阅者数组中dep.addSubs(); // 向dep实例中添加Watcher实例}return value;},set: (newVal) =>{if(value === newVal) return;// 防止 newVal为对象的情况,需要重新将对象中的属性变为响应式this.observer();//新旧值不同时 ,重新调用observe劫持数据value = newVal;//设置新的值dep.notify();// dep实例通知订阅者进行修改}})}}

借助Object.defineProperty的set,可是实现从view到data的响应式绑定,即可以获取页面变化的数据,然后将这个数据更新到data中,再渲染到需要的地方。

对于dep,它主要有两个作用:1.添加订阅者,2.通知订阅者们修改数据

class Dep{constructor(){// Dep将要执行的函数统一存储在一个数组中管理this.subs = [];}addSubs(){// 在Dep实例上添加订阅者,我们在observer中get方法里调用此方法this.subs.push(window.target);}notify(){// 通知订阅者进行修改,我们在observer中的set中调用此方法// 遍历所有的订阅者,调用订阅者上的update方法进行修改。this.subs.forEach(watcher => watcher.update());}}

**

watcher订阅者

**

Watcher就是一个订阅者。它属于Observer和Compile桥梁,它将接收到的Observer产生的数据变化,并将Observer发来的update消息处理,并根据Compile提供的指令执行Watcher绑定的更新函数,进行视图渲染,使得数据变化从而促使视图变化。

Watcher的第一个方法作用是更新且渲染节点。在首次渲染过程,会自动调用Dep方法来收集依赖,收集完成后组件中每个数据都绑定上该依赖。当数据变化时就会在seeter中通知对应的依赖进行更新。

在更新过程中要先读取数据,就会触发Wacther的第二个方法。一触发就再次自动调用Dep方法收集依赖,同时在此函数中运行patch(diff运算)来更新对应的DOM节点,完成了双向绑定。

class Watcher{constructor(vm, expr, cb){this.$vm = vm;//vm是Vue中的new Vue的一个对象this.expr = expr; // 保存需要修改的属性,可以是node节点的v-model或v-on:click等指令的属性值。this.cb = cb;// 保存属性修改时需要触发的回调函数this.getter(); // 保存属性的初始值,并将当前订阅者(自己)添加到订阅器dep上}update(){//这个方法由dep.notify调用let newVal;if(typeof this.expr === 'function'){newVal = this.expr();} else {newVal = compileUtil.getValue(this.expr, this.$vm);}// let newVal = compileUtil.getValue(this.expr, this.$vm);if(this.value === newVal) return; this.value = newVal;this.cb();}getter(){window.target = this;//Dep.target指向了自己,也就是Watcher对象,相当于将当前watcher实例放入到tartget中if(typeof this.expr === 'function'){//判断是否为函数this.value = this.expr();} else {this.value = compileUtil.getValue(this.expr, this.$vm);// 获取到当前的属性值}window.target = null;// 在Dep发布者的静态属性上清除当前 watcher//因为Dep中没有target这个属性,所以在使用完之后,记得释放该没有必要的内存空间 Dep.target = null;}}

**

compile 解析器

**

Compile主要的作用是对于绑定的dom节点(也就是el标签绑定的id),去遍历该节点的所有子节点,找出其中所有的v-指令和" {{}} ".

(1)如果子节点含有v-指令,即是元素节点,则对这个元素添加监听事件。(如果是v-on,则node.addEventListener(‘click’),如果是v-model,则node.addEventListener(‘input’))。接着初始化模板元素,创建一个Watcher绑定这个元素节点。

(2)如果子节点是文本节点,即" {{ data }} “,则用正则表达式取出” {{ data }} "中的data,用一个新的变量去替代其中的data。

class Compile{constructor(el, vm){this.$el = this.isElementNode(el) ? el: document.querySelector(el);this.$vm = vm;// 在内存中创建一个和 $el相同的元素节点let fragment = this.node2fragment(this.$el);// 解析模板($el节点)pile(fragment);// 将解析后的节点重新挂载到DOM树上this.$el.appendChild(fragment);}// 判断node是否为元素节点isElementNode(node) {return node.nodeType === 1;}// 判断是否为v-开头的Vue指令isDirective(attr) {return attr.startsWith('v-');}isSpecialisDirective(attr){return attr.startsWith('@');}compile(fragment){// 获取根节点的子节点let childNodes = fragment.childNodes;[...childNodes].forEach(child =>{if(this.isElementNode(child)){// 解析元素节点的属性,查看是否存在Vue指令pileElement(child);// 如果子节点也是元素节点,则递归执行该函数pile(child);}else{// 解析文本节点,查看是否存在"{{}}"pileText(child);}})}// 编译元素compileElement(node){// 获取元素节点的所有属性let attrs = node.attributes;// 遍历所有属性,查找是否存在Vue指令[...attrs].forEach(attr =>{// name: 属性名, expr: 属性值let {name, value:expr} = attr; // 判断是不是指令if(this.isDirective(name)){let [,directive] = name.split('-');// 如果为指令则去设置该节点的响应式函数 compileUtil[directive](node, expr, this.$vm);}if(this.isSpecialisDirective(name)){let eventName = name.substr(1);compileUtil['on'](node, eventName, expr, this.$vm);}})}// 编辑文本compileText(node){let content = node.textContent;// 匹配 {{xxx}}if(/\{\{(.+?)\}\}/.test(content)){compileUtil['contentText'](node, content, this.$vm);}}// 把节点移动到内存中node2fragment(node){// 创建文档碎片let fragment = document.createDocumentFragment();let firstChild;while(firstChild = node.firstChild){// appendChild具有移动性fragment.appendChild(firstChild);}return fragment;}}const compileUtil = {getValue(expr, vm){let valOrFn = expr.split('.').reduce((totalValue, key) =>{if(!totalValue[key]) return null;return totalValue[key];}, vm)return typeof valOrFn === 'function' ? valOrFn.call(vm) : valOrFn;},setValue(expr, vm, value){return expr.split('.').reduce((totalValue, key, index, arr) =>{if(index === arr.length - 1) totalValue[key] = value;return totalValue[key];}, vm.$data)},getContentValue(content, vm){return content.replace(/\{\{(.+?)\}\}/g, (...args) =>{return this.getValue(args[1], vm); })},contentText(node, content, vm){let fn = () =>{this.textUpdater(node, this.getContentValue(content, vm));}let resText = content.replace(/\{\{(.+?)\}\}/g, (...args) =>{// args[1] 为{{xxx}}中的xxxnew Watcher(vm, args[1], fn);return this.getValue(args[1], vm);});// 首次解析直接替换文本内容this.textUpdater(node, resText);},text(node, expr, vm){let value = this.getValue(expr, vm);this.textUpdater(node, value);let fn = () =>this.textUpdater(node, this.getValue(expr, vm));new Watcher(vm, expr, fn);},textUpdater(node, value){node.textContent = value;},html(node, expr, vm){let value = this.getValue(expr, vm);this.htmlUpdater(node, value);let fn = () =>this.htmlUpdater(node, this.getValue(expr, vm));new Watcher(vm, expr, fn);},htmlUpdater(node, value){node.textContent = value;},model(node, expr, vm){let value = this.getValue(expr, vm);this.modelUpdater(node, value);let fn = () => this.modelUpdater(node, this.getValue(expr, vm));node.addEventListener('input', ()=>{this.setValue(expr, vm, node.value);})new Watcher(vm, expr, fn)},modelUpdater(node, value){node.value = value;},on(node, eventName, expr, vm){// 改变this为vm实例let fn = vm.$option.methods[expr].bind(vm);// 添加事件node.addEventListener(eventName, fn);}}

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。