面试官:VUE双向数据绑定原理&&实现,你知否?
敲黑板划重点,这是考点。vue带给我们便利,我们也要知其然知其所以然,才能称对得起码农菜鸟这个称谓,才能和面试官闲话把vue家常。接下来,请集中注意力,我们来抽丝剥茧。
一、原理
先来看js对象的基本方法defineProperty():
[code]var obj = {}; Object.defineProperty(obj, 'name', { get: function() { console.log('我获取了name属性') return val; }, set: function (newVal) { console.log('我设置了name属性为:' + newVal) } }) obj.name = '魔丸';//在设置obj的name属性时,触发了set方法 var val = obj.name;//在获取obj的name属性时,触发了get方法
相信这个方法大家都了解,没错,vue就是运用了该方法实现的双向数据绑定。唠叨:vue.js 采用数据劫持结合发布者-订阅者模式的方式,通过
Object.defineProperty()来劫持各个属性的
setter,
getter,在数据变动时发布消息给订阅者,触发相应的监听回调。也就是说数据和视图同步,数据发生变化,视图跟着变化,视图变化,数据也随之发生改变,大家都是拴在一条绳子上的蚂蚱。是不是似懂非懂,别急,继续上网图:
原理图讲解:
1 .observer(数据监听器/观察者):用来实现对vue的data中定义的每个属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知watcher(订阅者),watcher会触发它的update方法,对视图进行更新。
2.指令解析器Compile: 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,并绑定相应的更新函数。
3 .订阅者:
- 连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
- 在vue中v-model,v-name,{{}}等都可以对数据进行显示,假如一个属性同时绑定了这三个指令,那么当这个属性值改变时,这三个指令对应的html视图都要改变。每当用到这样一个指令,就在Dep中增加一个订阅者。订阅者只是更新自己的指令对应的数据,也就是 v-model='name' 和 {{name}} 有两个对应的订阅者,各自管理自己的地方。
4.消息订阅器Dep:收集订阅者,数据变动后会触发notify,调用订阅者的update方法。
5.mvvm入口函数: 整合以上三者。
二、just do it
1.Observer实现思路:observe对被监听数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发setter,进而监听到数据变化。
[code]<!DOCTYPE html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>双向绑定</title> </head> <body> <div id="app"> <input type="text" class="name1" v-model="name"> <div class="name2">{{name}}</div> </div> </body> <script> /** Vue构造函数 * @param {*} param * */ function Vue(options) { this.data = options.data; observe(this.data) this.$compile = new Compile(document.querySelector(options.el), this) } window.onload = function() { var app = new Vue({ el:'#app', data: { name: '魔丸' } }) } function observe(data) { if(!data || typeof data !== 'object') { return; } // 遍历所有属性 Object.keys(data).forEach(function(key) { defineProp(data, key, data[key]); }); }; /** description * @data {*} 被修改data对象 * @key {*} 被修改data对象的属性 * @val {*} 被修改data对象的值 * */ function defineProp(data, key, val) { observe(val); // 监听子属性 //定义要修改对象的属性 Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: false, // 不能再define get: function() { return val; }, set: function(newVal) { console.log('监听到了,新属性值变化为 ', val, ' --> ', newVal); val = newVal; } }); } </script> </html>
2. compile订阅器实现:接下来我们需要订阅器去接收订阅者。当属性值变化时执行对应订阅者的更 新函数。显然订阅器是个数组容器。
设计思路:
- Dep类定义在defineProp()函数中:每个属性对应多个Watcher,它们需要放在一个订阅器,当该属性值变化时,遍历并执行订阅器中的所有订阅者的update方法。
- 添加订阅者操作放置在getter里面:让Watcher初始化时触发(需要判断是否需要添加订阅者)。
- 通知watcher更新的操作放在在setter里面:若数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。
[code]function defineProp(data, key, val) { var dep = new Dep(); observe(val); // 监听子属性 //定义要修改对象的属性 Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: false, // 不能再define get: function() { //添加订阅者watcher到主题对象Dep if (Dep.currentWatcher) { dep.addWatcher(watcher); } return val; }, set: function(newVal) { console.log('监听到了,新属性值变化为 ', val, ' --> ', newVal); val = newVal; dep.notify(); // 通知所有订阅者 } }); } // 消息订阅器 function Dep() { this.watcherList = []; } Dep.prototype = { addWatcher: function(watcher) { this.watcherList.push(watcher); }, notify: function() { this.watcherList.forEach(function(watcher) { watcher.update(); }); } };
三. Watcher实现:
设计思路:
1、在自身实例化时往属性订阅器(dep)里面添加自己。
2、自身必须有一个update()方法:待属性变动,订阅器调用notice()通知时,能调用自身的update()方法。
[code]/**订阅者 * @param {*} vm 指令所属vue实例 * @param {*} exp 指令对应的值 * @param {*} dataItem 指令对应的data中的属性 * */ function Watcher(vm, node, dataItem) { // 将当前订阅者指向自己,标记订阅者是当前watcher实例 Dep.currentWatcher = this; this.vm = vm; //当前vue实例 this.node = node;//指令对应的DOM元素 this.dataItem = dataItem; //指令对应的data中的属性 this.value = this.get(); // 此处为了触发属性的getter,从而在dep添加自己 // 添加完毕,释放对象。 Dep.currentWatcher 设为空。因为它是全局变量, // 也是 watcher 与 dep 关联的唯一桥梁,任何时刻都必须保证 Dep.currentWatcher 只有一个值。 Dep.currentWatcher = null; } Watcher.prototype = { // 属性值变化收到通知 update: function() { var newValue = this.get(); // 最新值 var oldVal = this.value; if (newValue !== oldVal) { this.value = newValue; this.node.nodeValue = newValue; //更改节点内容的关键 } }, get: function() { // 强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例, var value = this.vm.data[this.dataItem]; return value; } };
四.compile
设计思路
- 为了减少页面渲染DOM元素的次数,需先将文档碎片化,等Dom节点渲染完毕,再将Dom内容插入原来的文档流中。
- 需遍历所有节点及其子节点,扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定。
[code]/** 解析器 * @author liuyun 2020年06月08日 12:43:42' * @param {*} el id为app的Element元素 * @param {*} vm vue实例 * */ function Compile(el,vm) { // 将文档碎片化 this.fragment = document.createDocumentFragment(); let child; while (child = el.firstChild) { this.fragment.appendChild(child); } // 遍历所有节点及其子节点,扫描解析编译,调用对应的指令渲染函数进行数据渲染,调用对应的指令更新函数进行绑定 this.compileElement(this.fragment,vm); //处理完所有节点后,重新把内容添加回去 el.appendChild(this.fragment); } Compile.prototype = { compileElement: function(el,vm) { let _this = this; [].slice.call(el.childNodes).forEach(function(node) { var text = node.textContent; var reg = /\{\{(.*)\}\}/; // 表达式文本 // 如果是元素节点 if (node.nodeType == 1) { for (let i = 0; i < node.attributes.length; i++) { let attr = node.attributes[i]; if (attr.nodeName == 'v-model') { let dataItemName = attr.nodeValue; node.addEventListener('input', function(e) { // 如果有v-model属性,则监听它的input事件 vm.data[dataItemName] = e.target.value; // 给相应的data属性赋值,进而触发该属性的set方法 }) new Watcher(vm, node, dataItemName) //在消息订阅器中添加一个订阅者 node.value = vm.data[dataItemName]; //将data中的值赋予给该node node.removeAttribute('v-model') } } } else if (node.nodeType == 3 && reg.test(node.nodeValue)) { //若是文本节点 var name = RegExp.$1; // 获取匹配到的字符串 name = name.trim(); new Watcher(vm, node, name); node.nodeValue = vm.data[name]; } // 遍历编译子节点 if (node.childNodes && node.childNodes.length) { _this.compileElement(node,vm); } }); } }
动图效果:
getter/setter方法拦截数据的不足
需要vm.$set/Vue.set和vm.items.splice(newLength)解决,具体参看官方说明
1.增删对象时,是监控不到的。比如:data={name:"哪吒"},此时若再设置data.alias="魔丸",是监控不到的。因为属性的getter/setter方法是在observe初始化数据时遍历已有属性添加的,后面设置的alias没有设置getter/setter,所以检测不到变化。同样的,删除对象属性时,getter/setter会跟着属性一起被删除掉,拦截不到变化。
需要vm.$set/Vue.set和vm.$delete/Vue.delete这样的api来解决这个问题
2.getter/setter是针对对象的,像数组的修改(如push(),pop(),shift())导致arr发生了变化,同样需要更新视图,但是arr的getter/setter拦截不到变化(只有在赋值的时候才会调用setter,比如:arr=[1,2,3])。
对于这种情况,vue通过改写Array的默认方法,在调用这些方法的时候发布更新消息。一般无需关注。但是对于如下两种情况:
- 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue。
- 当你修改数组的长度时,例如:vm.items.length = newLength。
需要vm.$set/Vue.set和vm.items.splice(newLength)解决,具体参看官方说明
3.每次给数据设置值的时候,都会调用setter函数,这个时候就会发布属性更新消息,即使数据的值没有变。从性能方便考虑我们肯定希望值没有变化的时候,不更新模板。(像Angular这样把批量操作延时到一次更新,一次做完所有数据变更,然后整体应用到界面上)
- 面试题:vue实现双向数据绑定的原理(附源代码)
- vue.js双向数据绑定原理解析及模拟demo的实现
- Vue双向数据绑定实现原理
- vue实现数据双向绑定的原理
- vue数据双向绑定的原理及其实现
- vue实现数据双向绑定的原理
- 【vue】vue数据双向绑定实现原理
- Vue实现双向绑定的原理以及响应式数据
- vue数据双向绑定的实现原理
- vue.js双向数据绑定实现原理
- vue数据双向绑定原理及简单实现
- vue实现数据双向绑定原理剖析
- 【前端面试vue】vue响应式(双向数据绑定)原理及实现简例
- Vue数据双向绑定底层实现原理
- vue中数据双向绑定的实现原理
- Angular和Vue双向数据绑定的实现原理(重点是vue的双向绑定)
- vue双向数据绑定的实现原理
- vue实现数据双向绑定原理
- Vue数据双向绑定原理及简单实现方法
- vue数据双向绑定实现原理