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

vue数据双向绑定原理及简单实现

2020-04-20 12:44 357 查看

数据双向绑定原理简单概括的话就是:

View层影响Model层是通过对 ‘keyup’ 等事件的监听。

Model层影响View层是通过 Object.defineProperty( ) 方法劫持数据并结合发布订阅者模式的方式来实现数据的双向绑定。
(vue 3.0版本里用 Proxy 替代 Object.defineProperty)

当然不能只掌握到这个层面,下面介绍如何进行数据劫持以及发布订阅者模式:

1、Object.defineProperty( ) 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

Object.defineProperty(obj, prop, descriptor)

  • obj:要定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:要定义或修改的属性描述符。

这里我们主要是通过重定义属性描述符里的 get 和 set 方法进行数据劫持

get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。该函数的返回值会被用作属性的值。

set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。

总结:当我们修改数据层(Model)某个属性值(数据)时,就会触发重定义过的 set 方法去同步修改视图层(View)的数据

2、发布订阅者模式的应用

订阅者(Watcher)把自己想订阅的数据或事件添加到订阅者收集器(Dep),当某事件(set或get等)触发时,发布者(Observer)发布该事件到订阅者收集器,由订阅者收集器统一通知有订阅该事件的订阅者(Watcher)去执行相应的更新函数从而更新视图。(参考下图流程)

(图片转自参考文章一)

到此应该有个大概的思路了,下面根据原理图来介绍整个流程:

1、首先使用Object.defineProperty()中的 getter/setter 作为一个Observer(劫持器)去劫持data对象中的所有属性,在属性 set 的时候通知Dep(订阅者收集器)去通知相关订阅者。

2、实现一个 Watcher(订阅者),Watcher 就是收到 Dep 数据变化的通知后,会去执行相对应的更新函数来更新视图,同一个数据可能在多处被使用,所以订阅者不止一个;这也是 Dep 存在的意义,对 Watcher 集中起来统一管理。

3、Dep(订阅者收集器),里面存放每个数据对应的所有 Watcher,当Observer 的 set 方法被触发时,就调用 Dep 里面的的notify(通知)方法,逐条去通知所有的 Watcher 。

4、Complier是一个编译器,作用是解析模板指令,扫描和解析 vue 代码中每一个节点,先将节点转换为碎片化文档 DocumentFragment(性能优化,减少重排),再一次性 append 所有节点至目标 element 内,完成视图的初始化;同时编译器还担当着初始化 Watcher 的任务,即给 Watcher 绑定相关的更新函数 ,最终使 Watcher 添加到 Dep 中去。

简单实现(不包括Dep、Complier的实现):

<body>
<div id="app">
<input type="text" id="txt">
<span id="show-txt"></span>
</div>
<script>
var obj = {}
Object.defineProperty(obj, 'val', {
get: function () {
return val
},
set: function (newValue) {
document.getElementById('txt').value = newValue
document.getElementById('show-txt').innerHTML = newValue
}
})
document.addEventListener('keyup', function (e) {
obj.val = e.target.value
})
</script>
</body>

掌握以上内容已经算是上道了(面试时解释完上面内容也差不多了),下面是较为完整的实现(加深理解):

1.首先实现一个劫持器,对数据进行劫持

function defineReactive(obj, key, val) {

var dep = new Dep();  //创建dep订阅者收集器

Object.defineProperty(obj, key, {

get: function() {
if(Dep.target) {
dep.addSub(Dep.target)
}
return val
},

set: function(newVal) {
if(newVal === val) {
return
}
val = newVal;
dep.notify();  //执行dep的通知函数去通知相关订阅者
}
})
}

2、实现一个观察者,对于一个实例的每一个属性值都进行观察

function Observer(obj, vm) {
for(let key of Object.keys(obj)) {
defineReactive(vm, key, obj[key]);
}
}

3、实现dep的构造函数

function Dep() {
this.subs = [] //用来收集订阅者
}
Dep.prototype = {
addSub(sub) {  //添加订阅者的方法
this.subs.push(sub)
},
notify() {  //通知相关的所有订阅者执行更新函数
this.subs.forEach(function(sub) {
sub.update();
})
}
}

4、实现Watcher订阅者

function Watcher(vm, node, name) {

Dep.target = this; //辨识订阅者者要添加到哪个dep收集器里

this.vm = vm;
this.node = node;
this.name = name;
this.update();

Dep.target = null;

}

Watcher.prototype = {

update() {
this.get();
this.node.nodeValue = this.value //更新函数
},
get() {
this.value = this.vm[this.name] //触发相应的get
}

}

5、实现编译器Complier

function compile(node, vm) {

var reg = /\{\{(.*)\}\}/; // 用正则来匹配{{messeage}}

if(node.nodeType === 1) { //如果是元素节点

var attr = node.attributes;

//解析元素节点的所有属性

for(let i = 0; i < attr.length; i++) {

if(attr[i].nodeName == 'v-model') {

var name = attr[i].nodeValue //看看是与哪一个数据相关

node.addEventListener('input', function(e) { //将与其相关的数据改为最新值
vm[name] = e.target.value
})

node.value = vm.data[name]; //将data中的值赋予给该node

node.removeAttribute('v-model')

}

}

}
//如果是文本节点,即{{messeage}}情况

if(node.nodeType === 3) {

if(reg.test(node.nodeValue)) {

var name = RegExp.$1; //获取到匹配的字符串

name = name.trim();

node.nodeValue = vm[name]; //将data中的值赋予给该node

new Watcher(vm, node, name) //绑定一个订阅者
}

}

}

//在向碎片化文档中添加节点时,每个节点都处理一下

function nodeToFragment(node, vm) {

var fragment = document.createDocumentFragment();

var child;

while(child = node.firstChild) {

compile(child, vm);

fragment.appendChild(child);

}

return fragment

}

6、 Vue构造函数

function Vue(options) {

this.data = options.data;

observe(this.data, this)   //给data中的所有属性值增添了observe

var id = options.el;

var dom = nodeToFragment(document.getElementById(id), this)

//处理完所有节点后,重新把内容添加回去,减少重排
document.getElementById(id).appendChild(dom)

}

(有兴趣更进一步研究的话,去看源码吧!)

参考文章:
VUE双向数据绑定原理及简单实现
Vue数据双向绑定原理及简单实现
理解VUE双向数据绑定原理和实现—赵佳乐

  • 点赞 1
  • 收藏
  • 分享
  • 文章举报
CC_Together 发布了29 篇原创文章 · 获赞 22 · 访问量 591 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: