双向绑定原理
- 双向绑定
- 思考:
- 一句话描述原理
- DocuemntFragment(碎片化文档)
- Object.defineProperty(数据劫持)
- 发布订阅者模式
- Vue 双向绑定图示
- Vue 双向绑定完整实现代码
双向绑定
vue中 data定义的数据会添加双向绑定的功能,即数据更新后,页面内容会同步更新;页面内容更新后,数据也会同步更新。
思考:
- 初始化,如何将 data 中的数据更新到DOM模板中? - 碎片化文档
- 页面更新,如何更新数据? - input事件监听
- 数据变了更新页面,那如何知道数据变了呢? - 数据劫持,Object.defineProperty()
- 已知数据变了(发布者),如何更新跟这个数据相关的页面内容 {{}}、属性绑定、v-model(订阅者)呢? - 发布订阅者模式
一句话描述原理
Vue 数据双向绑定是通过数据劫持结合发布订阅者模式的方式来实现的。使用 DocuemntFragment(碎片化文档)获取所有子节点,将 v-model {{}} 类似语法的值进行填充,监听页面元素的 input 事件,当 val 变更时,更新 data 中的数据,给 data 通过 object.defineProperty 添加响应监听,当 val 变化时,会触发 set 方法,通过发布订阅模式,触发订阅者的更新方法,更新视图。
DocuemntFragment(碎片化文档)
function nodeToFragment(node){var fragment = document.createDocumentFragment();var child = null;while(child = node.firstChild){fragment.appendChild(child)}return fragment
}
Object.defineProperty(数据劫持)
var obj = {}; // 定义一个空对象
Object.defineProperty(obj, 'val', { // 定义要修改对象的属性get: function () {console.log('获取对象的值')},set: function (newVal) { console.log('设置对象的值:最新的值是'+newVal);}
});
obj.hello = 'hello world'
js通过Object.defineProperty方法简单的实现双向绑定:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body><input type="text" id="app"><span id="childSpan"></span>
</body>
<script>var obj = {}var initValue='初始值'Object.defineProperty(obj,'initValue',{get(){console.log('获取obj最新的值');return initValue},set(newVal){initValue = newValconsole.log('设置最新的值');// 获取到最新的值 然后将最新的值赋值给我们的spandocument.getElementById('childSpan').innerHTML = initValueconsole.log(obj.initValue);}})document.addEventListener('keyup', function (e) {obj.initValue = e.target.value; //监听文本框里面的值 获取最新的值 然后赋值给obj })</script>
</html>
发布订阅者模式
发布订阅者模式又叫 观察者模式,他定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得将得到通知。
// 实现发布订阅模式// 事件容器:
let handlers = {}// 添加事件:
handlers['onmsg'].push(fn1)// 触发事件:
this.handlers['onmsg'].forEach(handler => { handler(...params) })
学习参考:JavaScript设计模式 -发布订阅者模式
Vue 双向绑定图示
Vue 双向绑定完整实现代码
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title></head><body><div id="app">测试双向绑定demo<input type="text" v-model="text" /> {{text}}</div></body><script type="text/javascript">//编译函数function compile(node, vm) {var reg = /\{\{(.*)\}\}/; // 来匹配{{xxx}}中的xxx//如果是元素节点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中的值赋予给该nodenode.removeAttribute('v-model')}}}//如果是文本节点if(node.nodeType === 3) {if(reg.test(node.nodeValue)) {var name = RegExp.$1; //获取到匹配的字符串name = name.trim();node.nodeValue = vm[name]; //将data中的值赋予给该nodenew 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}// Vue构造函数 // 观察data中的所有属性值,注意增添了observefunction Vue(options) {this.data = options.data;observe(this.data, this)var id = options.el;var dom = nodeToFragment(document.getElementById(id), this)//处理完所有节点后,重新把内容添加回去document.getElementById(id).appendChild(dom)}//实现一个响应式监听属性的函数。一旦有赋新值就发生变化 function defineReactive(obj, key, val) {var dep = new 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;console.log('新值' + val);//一旦更新立马通知dep.notify();}})}//实现一个观察者,对于一个实例 每一个属性值都进行观察。function observe(obj, vm) {for(let key of Object.keys(obj)) {defineReactive(vm, key, obj[key]);}}// Watcher监听者function Watcher(vm, node, name) {Dep.target = this;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}}//dep构造函数function Dep() {this.subs = [] // 观察主题添加订阅者}Dep.prototype = {// 添加订阅者addSub(sub) {this.subs.push(sub)},// 发布通知notify() {this.subs.forEach(function(sub) {sub.update();})}}var vm = new Vue({el: 'app',data: {text: '赵刚'}})</script></html>