前言:
对于传统的 dom 操作,当数据变动时更新视图须要先获取到指标节点,而后将扭转后的值放入节点中,视图发生变化时,须要绑定事件批改数据。双向数据恰好能解决这种简单的操作,当数据发生变化时会自动更新视图,视图发生变化时也会自动更新数据,极大的进步了开发效率。那双向数据绑定到底是怎么实现的了,上面来讲述双向数据绑定的原理。
1、Vue 双向数据绑定的原理。
Vue 实现双向数据绑定是采纳数据劫持和发布者 - 订阅者模式。数据劫持是利用 ES5 的 Object.defineProperty(obj,key,val)办法来劫持每个属性的 getter 和 setter,在数据变动是公布音讯给订阅者,从而触发相应的回调来更新视图,上面来一步步实现。
<div id="app">
用户名:<input type="text" v-model="name">
明码:<input type="text" v-model="passWord">
{{name}} {{passWord}}
<div><div>{{name}}</div></div>
</div>
<script>
function Vue(option){
this.data = option.data;
this.id = option.el;
var dom = nodeToFragment(document.getElementById(this.id), this);
document.getElementById(this.id).appendChild(dom);
}
var vm = new Vue({
el: "app",
data: {
name: "zhangsan",
passWord: "123456"
}
})
<script>
如上一段 html,想要实现双向数据绑定,咱们须要先解析这一段 html,找到带有 v -model 指令和 {{}} 的节点(此处节点包含元素节点和文本节点), 而后咱们定义了一个 Vue 的构造函数,在实例化创建对象 vm 时,传入 id=’app’ 和对应的数据 data,咱们当初须要实现的性能是,当实例化创建对象时,将对应的 ’name’ 和 ’passWord’ 属性渲染到页面上。
在解析下面一段模板时,须要先理解一下 DocuemntFragment(碎片化文档)这个概念,你能够把他认为是一个 dom 节点收容器,当你发明了 10 个节点,当每个节点都插入到文档当中都会引发一次浏览器的回流,也就是说浏览器要回流 10 次,非常耗费资源。而应用碎片化文档,也就是说我把 10 个节点都先放入到一个容器当中,最初我再把容器直接插入到文档就能够了!浏览器只回流了 1 次。
解析 html
// 此办法是将传入的 dom 节点转为文档碎片, 参数 node 是须要解析的 html 片段,vm 是 Vue 构造函数实例化的对象。function nodeToFragment(node, vm){
// 创立一个文档碎片
var fragment = document.createDocumentFragment();
var child;
// 获取到 node 中的第一个子节点,当有子节点时,执行循环体
while(child = node.firstChild){
// appendChild 会将参数中节点移除,因而此循环会将 node 中的节点一个个移除,挪动到 fragment 文档碎片中,直到 node 中没有节点,循环完结。fragment.appendChild(child);
}
// 此处 fragment 曾经获取到 node 中所有节点,loopNode 函数用来循环每一层的节点。loopNode(fragment.childNodes, vm);
return fragment;
}
function loopNode(nodes, vm){// 此处传入的 nodes 是一个类数组,应用 Array.from()办法将其转化为数组。Array.from(nodes).forEach((node) => {// 此处失去的 node 是 nodes 中的间接子节点,compile 函数是用来解析这些节点,如果是元素节点,解析是否有 v -model 指令,如果是文本节点,解析是否有{{}}。compile(node, vm);
// 如果 node 还有子节点,则持续解析
if(node.childNodes.length>0){loopNode(node.childNodes, vm);
}
})
}
function compile(node, vm){
// 如果是元素节点
if(node.nodeType === 1){
// 取得元素节点上所有的属性,以键值对的形式存储在 attrs 中,attrs 属于类数组
var attrs = node.attributes;
Array.from(attrs).forEach(element => {if(element.nodeName == "v-model"){
var name = element.nodeValue;
// 初始化带有 v -model 指令的元素的值
node.value = vm.data[name];
}
});
}
// 正则匹配到文本中有 {{}} 的文本
var reg = /\{\{([^}]*)\}\}/g;
var textContent = node.textContent;
// 如果是文本节点且文本中带有 {{}} 的节点
if(node.nodeType === 3 && reg.test(textContent)){
// 将文本内容寄存在以后节点的自定义属性上
node.my = textContent;
// 此处 node.textContent 和 node.my 的文本一样,如果上一步不将文本存储到自定义属性中,那么下次将无奈匹配到 {{}},replace 办法用来替换文本中的{{name}} 和{{passWord}}。node.textContent = node.my.replace(reg, function(){var attr = arguments[0].slice(2,arguments[0].length-2);
return vm.data[attr];
})
}
}
下面咱们曾经实现了将 data 中的属性填充到页面中,接下来咱们须要做的是,当 data 中属性值发生变化时,咱们须要监听到数据的变动,Vue 中对数据监听应用的是 Object.defineProperty(data,key,val)办法 (不分明该办法的可查阅),Object.defineProperty(data,key,val) 能够监听对象属性的变动,当获取 data 中某个属性的值时,会调用该属性的 get()办法,当批改某个属性的值时会调用以后属性的 set()办法。
function observe(data){if(typeof data != 'object' || !data){return}
Object.keys(data).forEach((key)=>{defineReactive(data, key, data[key]);
})
}
function defineReactive(data, key, val){
// data 中子属性是对象时,持续监听
observe(val);
Object.defineProperty(data, key, {get: function(){return val;},
set: function(newVal){if(newVal !== val){val = newVal;} else {return;}
}
})
}
批改 Vue 构造函数如下,当实例化 Vue 时,实现了对数据 data 的监听,并解析模板,将 data 中对应属性填充到页面中。
然而,当 data 中属性值发生变化时,页面并不会更新,那接下来咱们须要解决的就是,当 data 中属性发生变化时,自动更新视图,视图发生变化时,被动更新数据,连贯视图和数据咱们须要在定义一个构造函数 Watcher。
首先咱们来思考下当 data 中 name 属性发生变化时,咱们须要更新的视图有如下三个节点,一个元素节点和两个文本节点
当 data 中 passWord 属性变动时,须要更新的视图有两个节点
也就是说,当某个属性发生变化时,咱们可能要更新多个视图,那咱们如何去定位须要更新那些节点了?因而咱们须要将绑定了 data 中属性的节点保留到一个数组中,当 data 中对应属性发生变化时,循环数组,拿到节点,执行更新办法。
回顾一下咱们 compile 中的代码,下图中标记 1、2 处就是获取到 data 中的属性名。接下来咱们定义一个 Watcher 构造函数,在解析模板时实例化 Watcher,3、4 处是新增代码,实例化 Watcher。
Watcher 构造函数中有两个办法,一个 update 办法和一个 get 办法,实例化 Watcher 时调用 Watcher 中的 get 办法,此办法会触发 data 中对应的属性的 get 办法。
function Watcher(vm, node, name){
this.vm = vm;
this.node = node;
this.name = name;
this.value = this.get();}
Watcher.prototype.get = function(){
// 触发 data 中属性 get 办法前将以后实例化对象存入 target 属性中
Dep.target = this;
// 取 data 中的 this.name 属性,会触发该属性的 get 办法
var value = this.vm.data[this.name];
Dep.target = null;
return value;
}
Watcher.prototype.update = function(){}
上文中提到,咱们须要定义数组来存储对应属性的节点,也就是说,data 中每个属性都必须有一个数组来存储节点,上面咱们来定义一个 Dep 构造函数,用来收集节点。
function Dep(){
// 寄存 Watcher 的实例对象
this.subs = [];}
Dep.prototype.addSub = function(sub){this.subs.push(sub);
}
Dep.prototype.notify = function(){this.subs.forEach((sub)=>{sub.update();
})
}
每个属性都须要一个数组,因而咱们在监听 data 属性时实例化 Dep,Dep 的实例在闭包的状况下创立,咱们能够批改数据监听中的 get 办法,上文在实例化 Watcher 时,触发 get 办法,将 Watcher 的实例存入数组中,当批改 data 中属性值时,调用 set 办法,Dep 实例对象调用 notify 办法,实现更新。
// 批改后的 defineReactive 办法
function defineReactive(data, key, val){
// 为每个属性创立一个 Dep 实例
var dep = new Dep();
observe(val);
Object.defineProperty(data, key, {get: function(){
// 实例化 Watcher 时,触发了 get 办法,此时 Dep.target 为 Watcher 实例化对象
Dep.target && dep.addSub(Dep.target);
return val;
},
set: function(newVal){if(newVal !== val){
val = newVal;
// 当调用 set 办法时,告诉所有订阅者执行更新办法
dep.notify();} else {return;}
}
})
}
实现更新办法
function Watcher(vm, node, name){...}
Watcher.prototype.get = function(){...}
Watcher.prototype.update = function(){if(this.node.nodeType === 1){this.node.nodeValue = this.get();
} else {this.node.textContent = this.node.my.replace(/\{\{([^}]*)\}\}/g, function(){var attr = arguments[0].slice(2,arguments[0].length-2);
return this.vm.data[attr];
})
}
}
实现到这里,咱们就曾经实现了数据变动时自动更新视图,咱们来梳理一下流程。就拿下面例子来说,当咱们执行 vm.data[‘name’] = ‘lisi’ 时,便会触发 set 办法,set 办法中调用 Dep 实例的 notify 办法,此办法会遍历 this.subs 数组,这个数组中寄存的元素是 Watcher 的实例化对象,调用 sub.update()办法便会更新视图。
当视图发生变化时,须要批改相应数据,只须要给相应节点绑定事件即可,批改 compile 办法如下,给相应节点减少 input 事件。
if(node.nodeType === 1){
// 取得元素节点上所有的属性,以键值对的形式存储在 attr 中,attr 属于类数组
var attr = node.attributes;
Array.from(attr).forEach(element => {if(element.nodeName == "v-model"){
var name = element.nodeValue;
// 给带有 v -model 指令的元素绑定 input 事件
node.addEventListener('input', function(e){vm.data[name] = e.target.value;
})
// 初始化带有 v -model 指令的元素的值
node.value = vm.data[name];
new Watcher(vm, node, name);
}
});
}
到这里双向数据绑定就实现了,上面附上残缺代码。
<!DOCTYPE html>
<html lang="en">
<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>Vue 双向数据绑定 </title>
</head>
<body>
<div id="app">
用户名:<input type="text" v-model="name">
明码:<input type="text" v-model="passWord">
{{name}} {{passWord}}
<div>
<div>{{name}}</div>
</div>
</div>
<script>
function Vue(option) {
this.data = option.data;
this.id = option.el;
observe(this.data);
var dom = nodeToFragment(document.getElementById(this.id), this);
document.getElementById(this.id).appendChild(dom);
}
function nodeToFragment(node, vm) {
// 创立一个文档碎片
var fragment = document.createDocumentFragment();
var child;
// 获取到 node 中的第一个节点
while (child = node.firstChild) {
// appendChild 会将传入的节点移除,因而此循环会将 node 中的节点一个个移除,挪动到 fragment 文档碎片中。fragment.appendChild(child);
}
// console.dir(fragment);
loopNode(fragment.childNodes, vm);
return fragment;
}
function loopNode(nodes, vm) {
// 此处传入的 nodes 是一个类数组,将其转化为数组
Array.from(nodes).forEach((node) => {compile(node, vm);
// 如果 node 还有子节点,则持续解析
if (node.childNodes.length > 0) {loopNode(node.childNodes, vm);
}
})
}
function compile(node, vm) {
// 如果是元素节点
if (node.nodeType === 1) {
// 取得元素节点上所有的属性,以键值对的形式存储在 attr 中,attr 属于类数组
var attr = node.attributes;
Array.from(attr).forEach(element => {if (element.nodeName == "v-model") {
var name = element.nodeValue;
// 给带有 v -model 指令的元素绑定 input 工夫
node.addEventListener('input', function (e) {vm.data[name] = e.target.value;
})
// 初始化带有 v -model 指令的元素的值
node.value = vm.data[name];
new Watcher(vm, node, name);
}
});
}
// 正则匹配到文本中有 {{}} 的文本
var reg = /\{\{([^}]*)\}\}/g;
var textContent = node.textContent;
// 如果是文本节点且文本中带有 {{}} 的节点
if (node.nodeType === 3 && reg.test(textContent)) {
// 将文本内容寄存在以后节点的自定义属性上
node.my = textContent;
// 此处 node.textContent 和 node.my 的文本一样,如果上一步不将文本存储到自定义属性中,那么下次将无奈匹配到{{}}。node.textContent = node.my.replace(reg, function () {var attr = arguments[0].slice(2, arguments[0].length - 2);
new Watcher(vm, node, attr);
return vm.data[attr];
})
}
}
function observe(data) {if (typeof data != 'object' || !data) {return}
Object.keys(data).forEach((key) => {defineReactive(data, key, data[key]);
})
}
function defineReactive(data, key, val) {var dep = new Dep();
observe(val);
Object.defineProperty(data, key, {get: function () {Dep.target && dep.addSub(Dep.target);
return val;
},
set: function (newVal) {if (newVal !== val) {
val = newVal;
dep.notify();} else {return;}
}
})
}
function Dep() {this.subs = [];
}
Dep.prototype.addSub = function (sub) {this.subs.push(sub);
}
Dep.prototype.notify = function () {this.subs.forEach((sub) => {sub.update();
})
}
function Watcher(vm, node, name) {
this.vm = vm;
this.node = node;
this.name = name;
this.value = this.get();}
Watcher.prototype.get = function () {
Dep.target = this;
var value = this.vm.data[this.name];
Dep.target = null;
return value;
}
Watcher.prototype.update = function () {if (this.node.nodeType === 1) {this.node.nodeValue = this.get();
} else {this.node.textContent = this.node.my.replace(/\{\{([^}]*)\}\}/g, function () {var attr = arguments[0].slice(2, arguments[0].length - 2);
return this.vm.data[attr];
})
}
}
var vm = new Vue({
el: "app",
data: {
name: "lishibo",
passWord: "123456",
obj: {obj1: 'obj1'},
arr: ['arr1', 'arr2']
}
})
</script>
</body>
</html>