分享web开发知识

注册/登录|最近发布|今日推荐

主页 IT知识网页技术软件开发前端开发代码编程运营维护技术分享教程案例
当前位置:首页 > 软件开发

Vue.js双向绑定原理

发布时间:2023-09-06 01:32责任编辑:林大明关键词:js

Vue.js最核心的功能有两个,一个是响应式的数据绑定系统,另一个是组件系统。本文仅仅探究双向绑定是怎样实现的。先讲涉及的知识点,再用简化的代码实现一个简单的hello world示例。

一、访问器属性

访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义。

<!DOCTYPE html><html> ?<head> ???<meta charset="UTF-8"> ???<title></title> ?</head> ?<body> ???<script> ?????var obj = {}; ?????Object.defineProperty(obj, ‘hello‘, { ???????get: function() { ?????????console.log(‘get方法被调用了‘); ???????}, ???????set: function(val) { ?????????console.log(‘set方法被调用了,参数是‘ + val); ???????} ?????}); ?????obj.hello; //get方法被调用了 ?????obj.hello = ‘abc‘; //set方法被调用了,参数是abc ???</script> ?</body></html>
View Code

get和set方法内部的this都指向obj,这意味着get和set函数可以操作对象内部的值。另外,访问器属性的会“覆盖”同名的普通属性,因为访问器属性会被优先访问,与其同名的普通属性则会被忽略。

二、极简的双向绑定实现

<!DOCTYPE html><html> ?<head> ???<meta charset="UTF-8"> ???<title></title> ?</head> ?<body> ???<input type="text" id="a" /> ???<span id="b"></span> ???<script> ?????var obj = {}; ?????Object.defineProperty(obj, ‘hello‘, { ???????set: function(newval) { ?????????document.getElementById(‘a‘).value = newval; ?????????document.getElementById(‘b‘).innerHTML = newval ???????} ?????}); ?????document.addEventListener(‘keyup‘, function(e) { ???????obj.hello = e.target.value; ?????}) ???</script> ?</body></html>
View Code

此例实现的效果是:随着文本框输入文字的变化,span中会同步显示相同的内容。在js或者在控制台上显式的修改obj.hello的值,视图会相应的更新。这样就实现了model=>view以及view=>model的双向绑定。

以上就是Vue实现双向绑定的基本原理。

三、分解任务

上述示例仅仅是为了说明原理,我们最终要实现的是:

<div id="app"> ?<input type="text" v-model="text"> ?{{ text }}</div>var vm = new Vue({ ?el:‘#app‘, ?data:{ ???text:‘hello world‘ ?}})

首先将该任务分成几个子任务:

1、输入框以及文本节点与data中的数据绑定;

2、输入框内容变化时,data中的数据同步变化,即view =>model的变化;

3、data中的数据变化时,文本节点的内容同步变化,即model =>view的变化;

要实现任务1,需要对DOM进行编译,这里有一个知识点:DocumentFragment。

四、DocumentFragment

DocumentFragment(文档片段)可以看做节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过append方法,DOM中的节点会被自动删除)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。

<!DOCTYPE html><html> ?<head> ???<meta charset="UTF-8"> ???<title></title> ?</head> ?<body> ???<div id="app"> ?????<input type="text" id="a" /> ?????<span id="b"></span> ???</div> ???<script> ?????var dom = nodeToFragment(document.getElementById(‘app‘)); ?????console.log(dom); ?????function nodeToFragment(node) { ???????var flag = document.createDocumentFragment(); ???????var child; ???????while(child == node.firstChild) { ?????????flag.appendChild(child); //劫持node的所有子节点 ???????} ???????return flag; ?????} ?????document.getElementById(‘app‘).appendChild(dom); //返回到app中 ???</script> ?</body></html>
View Code

五、数据初始化绑定

<!DOCTYPE html><html lang="en"> ?<head> ???<meta charset="UTF-8"> ???<title>Two-way-data-binding</title> ?</head> ?<body> ???<div id="app"> ?????<input type="text" v-model="text"> {{ text }} ???</div> ???<script> ?????function compile(node, vm) { ???????var reg = /\{\{(.*)\}\}/; ???????// 节点类型为元素 ???????if(node.nodeType === 1) { ?????????var attr = node.attributes; ?????????// 解析属性 ?????????for(var i = 0; i < attr.length; i++) { ???????????if(attr[i].nodeName == ‘v-model‘) { ?????????????var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名 ?????????????node.value = vm[name]; // 将 data 的值赋给该 node ?????????????node.removeAttribute(‘v-model‘); ???????????} ?????????}; ???????} ???????// 节点类型为 text ???????if(node.nodeType === 3) { ?????????if(reg.test(node.nodeValue)) { ???????????var name = RegExp.$1; // 获取匹配到的字符串 ???????????name = name.trim(); ???????????node.nodeValue = vm.data[name]; //将data的值赋给该node ?????????} ???????} ?????} ?????function nodeToFragment(node, vm) { ???????var flag = document.createDocumentFragment(); ???????var child; ???????// 所有表达式必然会返回一个值,赋值表达式亦不例外 ???????// 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法 ???????// 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除 ???????// 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了 ???????while(child = node.firstChild) { ?????????compile(child, vm); ?????????flag.appendChild(child); // 将子节点劫持到文档片段中 ???????} ???????return flag; ?????} ?????function Vue(options) { ???????this.data = options.data; ???????var id = options.el; ???????var dom = nodeToFragment(document.getElementById(id), this); ???????// 编译完成后,将 dom 返回到 app 中 ???????document.getElementById(id).appendChild(dom); ?????} ?????var vm = new Vue({ ???????el: ‘app‘, ???????data: { ?????????text: ‘hello world‘ ???????} ?????}); ???</script> ?</body></html>
View Code

以上代码实现了任务一,我们可以看到,hello world已经呈现在输入框和文本节点中。

六、响应式的数据绑定

再来看任务2的是实现思路:当我们在输入框输入数据的时候,首先触发input事件或者keyup、change事件,在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。我们会利用defineProperty将data中的text设置为vm的访问器属性,因此给vm.text赋值就会触发set方法。在set方法中主要做两件事,第一是更新属性的值,第二留到任务3来说。

<!DOCTYPE html><html lang="en"> ?<head> ???<meta charset="UTF-8"> ???<title>Two-way-data-binding</title> ?</head> ?<body> ???<div id="app"> ?????<input type="text" v-model="text"> {{ text }} ???</div> ???<script> ?????function observe(obj, vm) { ???????Object.keys(obj).forEach(function(key) { ?????????defineReactive(vm, key, obj[key]); ???????}) ?????} ?????function defineReactive(obj, key, val) { ???????Object.defineProperty(obj, key, { ?????????get: function() { ???????????return val ?????????}, ?????????set: function(newVal) { ???????????if(newVal === val) return ???????????val = newVal; ???????????console.log(val); //方便看效果 ?????????} ???????}); ?????} ?????function nodeToFragment(node, vm) { ???????var flag = document.createDocumentFragment(); ???????var child; ???????// 所有表达式必然会返回一个值,赋值表达式亦不例外 ???????// 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法 ???????// 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除 ???????// 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了 ???????while(child = node.firstChild) { ?????????compile(child, vm); ?????????flag.appendChild(child); // 将子节点劫持到文档片段中 ???????} ???????return flag; ?????} ?????function compile(node, vm) { ???????var reg = /\{\{(.*)\}\}/; ???????// 节点类型为元素 ???????if(node.nodeType === 1) { ?????????var attr = node.attributes; ?????????// 解析属性 ?????????for(var i = 0; i < attr.length; i++) { ???????????if(attr[i].nodeName == ‘v-model‘) { ?????????????var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名 ?????????????node.addEventListener(‘input‘, function(e) { ???????????????// 给相应的 data 属性赋值,进而触发该属性的 set 方法 ???????????????vm[name] = e.target.value; ?????????????}); ?????????????node.value = vm[name]; // 将 data 的值赋给该 node ?????????????node.removeAttribute(‘v-model‘); ???????????} ?????????}; ???????} ???????// 节点类型为 text ???????if(node.nodeType === 3) { ?????????if(reg.test(node.nodeValue)) { ???????????var name = RegExp.$1; // 获取匹配到的字符串 ???????????name = name.trim(); ???????????node.nodeValue = vm[name]; //将data的值赋给该node ?????????} ???????} ?????} ?????function Vue(options) { ???????this.data = options.data; ???????var data = this.data; ???????observe(data, this); ???????var id = options.el; ???????var dom = nodeToFragment(document.getElementById(id), this); ???????// 编译完成后,将 dom 返回到 app 中 ???????document.getElementById(id).appendChild(dom); ?????} ?????var vm = new Vue({ ???????el: ‘app‘, ???????data: { ?????????text: ‘hello world‘ ???????} ?????}); ???</script> ?</body></html>
View Code

任务2也就完成了,text属性值会与输入框的内容同步变化(打开浏览器后台进行查看)。

七、订阅/发布模式(subscribe&publish)

text属性变化了,set方法触发了,但是文本节点的内容没有变化。如何让同样绑定到text的文本节点也同步变化呢?这里又有一个知识点:订阅发布模式。

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。发布者发出通知 =>主题对象收到通知并推送给订阅者 =>订阅者执行相应操作。

<!DOCTYPE html><html lang="en"> ?<head> ???<meta charset="UTF-8"> ???<title>Two-way-data-binding</title> ?</head> ?<body> ???<script> ?????//一个发布者publisher ?????var pub = { ???????publish: function() { ?????????dep.notify(); ???????} ?????} ???????????//三个订阅者subscribers ?????var sub1 = { ???????update: function() { ?????????console.log(1) ???????} ?????}; ?????var sub2 = { ???????update: function() { ?????????console.log(2) ???????} ?????}; ?????var sub3 = { ???????update: function() { ?????????console.log(3) ???????} ?????}; ???????????//一个主题对象 ?????function Dep() { ???????this.subs = [sub1, sub2, sub3]; ?????} ?????Dep.prototype.notify = function() { ???????this.subs.forEach(function(sub) { ?????????sub.update(); ???????}) ?????} ???????????//发布者发布消息,主题对象执行notify方法,进而触发订阅者执行update方法 ?????var dep = new Dep(); ?????pub.publish(); //1,2,3 ???</script> ?</body></html>
View Code

之前提到的,当set方法触发后做的第二件事就是作为发布者发出通知:“我是属性text,我变了”。文本节点则是作为订阅者,在收到消息后执行相应的更新操作。

八、双向绑定的实现

回顾一下,每当 new 一个 Vue,主要做了两件事:第一个是监听数据:observe(data),第二个是编译 HTML:nodeToFragement(id)。

在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。

在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。

我们已经实现:修改输入框内容 =>在事件回调函数中修改属性值 =>触发属性的set方法。接下来我们要实现的是:发出通知dep.notify() =>触发订阅者的update方法 =>更新视图。

这里的关键逻辑时:如何将watcher添加到关联属性的dep中。

 function compile(node, vm) { ??var reg = /\{\{(.*)\}\}/; ??// 节点类型为元素 ??if(node.nodeType === 1) { ????var attr = node.attributes; ????// 解析属性 ????for(var i = 0; i < attr.length; i++) { ??????if(attr[i].nodeName == ‘v-model‘) { ????????var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名 ????????node.addEventListener(‘input‘, function(e) { ??????????// 给相应的 data 属性赋值,进而触发该属性的 set 方法 ??????????vm[name] = e.target.value; ????????}); ????????node.value = vm[name]; // 将 data 的值赋给该 node ????????node.removeAttribute(‘v-model‘); ??????} ????}; ????new Watcher(vm, node, name, ‘input‘); ??} ??// 节点类型为 text ??if(node.nodeType === 3) { ????if(reg.test(node.nodeValue)) { ??????var name = RegExp.$1; // 获取匹配到的字符串 ??????name = name.trim(); ??????new Watcher(vm, node, name, ‘text‘); ????} ??} }
View Code

在编译HTML过程中,为每个与data关联的节点生成一个watcher,watcher函数中发生了什么呢?

function Watcher(vm, node, name, nodeType) { ?Dep.target = this; ?this.name = name; ?this.node = node; ?this.vm = vm; ?this.update(); ?Dep.target = null;}Watcher.prototype = { ?update: function() { ???this.get(); ???this.node.nodeValue = this.value; ?}, ?// 获取 data 中的属性值 ?get: function() { ???this.value = this.vm[this.name]; // 触发相应属性的 get ?}}
View Code

首先,将自己赋给了一个全局变量Dep.target;

其次,执行了update方法,进而执行了get方法,get的方法读取了vm的访问器属性,从而触发了访问器属性的get方法,get方法中将该watcher添加到了对应访问器属性的dep中;

接着,获取属性的值,然后更新视图。

最后,将Dep.target设为空,因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。

function defineReactive(obj, key, val) { ?var dep = new Dep(); ?Object.defineProperty(obj, key, { ???get: function() { ?????// 添加订阅者 watcher 到主题对象 Dep ?????if(Dep.target) dep.addSub(Dep.target); ?????return val; ???}, ???set: function(newVal) { ?????if(newVal === val) return ?????val = newVal; ?????// 作为发布者发出通知 ?????dep.notify(); ???} ?});}function Dep() { ?this.subs = []}Dep.prototype = { ?addSub: function(sub) { ???this.subs.push(sub); ?}, ?notify: function() { ???this.subs.forEach(function(sub) { ?????sub.update(); ???}); ?}}
View Code

至此,hello world双向绑定就基本实现了。文本内容会随输入框内容同步变化,在控制器中修改vm.text的值,会同步反映到文本内容中。以下是完整代码:

<!DOCTYPE html><html lang="en"> ?<head> ???<meta charset="UTF-8"> ???<title>Two-way-data-binding</title> ?</head> ?<body> ???<div id="app"> ?????<input type="text" v-model="text"> {{ text }} ???</div> ???<script> ?????function observe(obj, vm) { ???????Object.keys(obj).forEach(function(key) { ?????????defineReactive(vm, key, obj[key]); ???????}) ?????} ?????function defineReactive(obj, key, val) { ???????var dep = new Dep(); ???????Object.defineProperty(obj, key, { ?????????get: function() { ???????????// 添加订阅者 watcher 到主题对象 Dep ???????????if(Dep.target) dep.addSub(Dep.target); ???????????return val ?????????}, ?????????set: function(newVal) { ???????????if(newVal === val) return ???????????val = newVal; ???????????// 作为发布者发出通知 ???????????dep.notify(); ?????????} ???????}); ?????} ?????function nodeToFragment(node, vm) { ???????var flag = document.createDocumentFragment(); ???????var child; ???????// 所有表达式必然会返回一个值,赋值表达式亦不例外 ???????// 理解了上面这一点,就能理解 while (child = node.firstChild) 这种用法 ???????// 其次,appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除 ???????// 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了 ???????while(child = node.firstChild) { ?????????compile(child, vm); ?????????flag.appendChild(child); // 将子节点劫持到文档片段中 ???????} ???????return flag ?????} ?????function compile(node, vm) { ???????var reg = /\{\{(.*)\}\}/; ???????// 节点类型为元素 ???????if(node.nodeType === 1) { ?????????var attr = node.attributes; ?????????// 解析属性 ?????????for(var i = 0; i < attr.length; i++) { ???????????if(attr[i].nodeName == ‘v-model‘) { ?????????????var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名 ?????????????node.addEventListener(‘input‘, function(e) { ???????????????// 给相应的 data 属性赋值,进而触发该属性的 set 方法 ???????????????vm[name] = e.target.value; ?????????????}); ?????????????node.value = vm[name]; // 将 data 的值赋给该 node ?????????????node.removeAttribute(‘v-model‘); ???????????} ?????????}; ?????????new Watcher(vm, node, name, ‘input‘); ???????} ???????// 节点类型为 text ???????if(node.nodeType === 3) { ?????????if(reg.test(node.nodeValue)) { ???????????var name = RegExp.$1; // 获取匹配到的字符串 ???????????name = name.trim(); ???????????new Watcher(vm, node, name, ‘text‘); ?????????} ???????} ?????} ?????function Watcher(vm, node, name, nodeType) { ???????Dep.target = this; ???????this.name = name; ???????this.node = node; ???????this.vm = vm; ???????this.nodeType = nodeType; ???????this.update(); ???????Dep.target = null; ?????} ?????Watcher.prototype = { ???????update: function() { ?????????this.get(); ?????????if(this.nodeType == ‘text‘) { ???????????this.node.nodeValue = this.value; ?????????} ?????????if(this.nodeType == ‘input‘) { ???????????this.node.value = this.value; ?????????} ???????}, ???????// 获取 data 中的属性值 ???????get: function() { ?????????this.value = this.vm[this.name]; // 触发相应属性的 get ???????} ?????} ?????function Dep() { ???????this.subs = [] ?????} ?????Dep.prototype = { ???????addSub: function(sub) { ?????????this.subs.push(sub); ???????}, ???????notify: function() { ?????????this.subs.forEach(function(sub) { ???????????sub.update(); ?????????}); ???????} ?????} ?????function Vue(options) { ???????this.data = options.data; ???????var data = this.data; ???????observe(data, this); ???????var id = options.el; ???????var dom = nodeToFragment(document.getElementById(id), this); ???????// 编译完成后,将 dom 返回到 app 中 ???????document.getElementById(id).appendChild(dom); ?????} ?????var vm = new Vue({ ???????el: ‘app‘, ???????data: { ?????????text: ‘hello world‘ ???????} ?????}) ???</script> ?</body></html>
View Code

参考文章1:https://github.com/DDFE/DDFE-blog/issues/7

参考文章2:https://segmentfault.com/a/1190000006599500

原文地址

Vue.js双向绑定原理

原文地址:https://www.cnblogs.com/fengxiongZz/p/8120337.html

知识推荐

我的编程学习网——分享web前端后端开发技术知识。 垃圾信息处理邮箱 tousu563@163.com 网站地图
icp备案号 闽ICP备2023006418号-8 不良信息举报平台 互联网安全管理备案 Copyright 2023 www.wodecom.cn All Rights Reserved