您的位置:首页 > Web前端 > Vue.js

面试官:VUE双向数据绑定原理&&实现,你知否?

2020-07-14 04:39 225 查看

敲黑板划重点,这是考点。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这样把批量操作延时到一次更新,一次做完所有数据变更,然后整体应用到界面上)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: