双向绑定原理及简单实现
Vue是采用数据劫持结合发布者-订阅者模式的方式,通过new Proxy()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Vue3.x与Vue2.x的区别仅是数据劫持的方式由Object.defineProperty更改为Proxy代理,其他代码不变
Vue最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统。本文仅探究双向绑定是怎样实现的。以及实现一个简化版的**Vue-lite
**
一、访问器属性
访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()
方法单独定义。
1 2 3 4 5 6 7 8 9 10 11 12
| const obj = {}; Object.defineProperty(obj, 'hello', { set: newVal => { console.log('set方法被调用了'); console.log('newVal='+newVal); }, get: () => { console.log('get方法被调用了'); } }); obj.hello = 'xxx'; obj.hello
|
其中**get(),set()**方法就是实现双向绑定的关键
二、极简双向绑定的实现
html部分:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue-lite</title> </head> <body> <input type="text" id="inputs" /> <p id="tips"></p> <button id="btn">设置新值</button> <script src="./js/Vue-lite.js"></script> </body> </html>
|
Vue-lite.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const inputs = document.querySelector('#inputs'); const tips = document.querySelector('#tips'); const btn = document.querySelector('#btn'); const obj = {}; Object.defineProperty(obj, 'hello', { set: newVal => { inputs.value = newVal; tips.innerHTML = newVal; }, get: () => { console.log('get方法被调用了'); return '123'; } }); obj.hello = 'xxx'; console.log(obj.hello); inputs.addEventListener('keyup', e=>{ obj.hello = e.target.value; }); btn.addEventListener('click', e=>{ obj.hello = '233'; });
|
此例实现的效果是:随文本框输入文字的变化,span 中会同步显示相同的文字内容;在js或控制台显式的修改 obj.hello 的值,视图会相应更新。这样就实现了 model => view 以及 view => model 的双向绑定,就是Vue实现双向绑定的最基本原理。
三、细节优化
上述示例仅仅是为了说明原理。我们最终要实现的是:
1 2 3 4 5 6 7
| <div id="app"> <div> <span>姓名: </span> <input type="text" name="name" v-mode="name" /> </div> <p>您输入的name是: {{name}}</p> </div>
|
1 2 3 4 5 6
| const vm = new Vue({ el: '#app', data: { name: 'rexhang' } });
|
首先做下需要实现功能点:
- 输入框以及文本节点与 data 中的数据绑定
- 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化
- data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化
html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <div id="app"> <input type="text" name="name" v-model="name" /> <br /> {{name}} </div> <script src="./js/Vue-lite.js"></script> <script> new Vue({ el: '#app', data: { name: 'rexhang' } }); </script>
|
Vue-lite.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
| function nodeToFragment(node, vm) { let virtualDOM = document.createDocumentFragment(); let child; while( child = node.firstChild){ compile(child, vm) virtualDOM.appendChild(child); } return virtualDOM; }
function compile (node, vm){ const reg = /\{\{(.*)\}\}/; console.log(node); console.log(node.nodeType); if (node.nodeType === 1){ const attr = node.attributes; for(let i = 0; i < attr.length; i++){ console.log(attr[i].nodeName);
if(attr[i].nodeName === 'v-model'){ const name = attr[i].nodeValue;
node.addEventListener('input', e=>{ vm[name] = e.target.value; }); node.value = vm[name]; node.removeAttribute('v-model'); } } } if (node.nodeType === 3){ if(reg.test(node.nodeValue)){ let name = node.nodeValue.match(reg)[1].trim(); console.log(vm, node, name); new Watcher(vm, node, name);
} } }
function defineReactive(obj, key, val){ const dep = new Dep(); Object.defineProperty(obj, key, { set: newVal => { if(newVal === val) return; val = newVal; console.log(val); dep.notify(); }, get: () => { if(Dep.target) dep.addSub(Dep.target); return val; } }) }
function observe (obj, vm) { Object.keys(obj).forEach(key=>{ defineReactive(vm, key, obj[key]); }); }
const dep = new Dep();
const pub = { publish: ()=>{ dep.notify(); } }
const sub1 = { update: function () { console.log(1); } } const sub2 = { update: function () { console.log(2); } } const sub3 = { update: function () { console.log(3); } }
function Dep () { this.subs = []; }
Dep.prototype.addSub = function(sub){ this.subs.push(sub) }
Dep.prototype.notify = function(){ this.subs.forEach(sub=>{ sub.update(); }) }
pub.publish();
function Watcher(vm, node, name){ 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; }, get: function(){ this.value = this.vm[this.name]; } }
function Vue(opt){ this.data = opt.data; observe(this.data, this); const rootDom = opt.el; const dom = nodeToFragment(document.querySelector(rootDom), this); document.querySelector(rootDom).appendChild(dom); }
|
最终实现效果:
四、总结
Object.defineProperty
来进行数据中转(劫持),从而实现事件的发布和后续触发订阅者的监听来实现数据绑定
- 实现一个监听器
observe
用来劫持并监听所有属性,如有变动,就通知订阅者
- 实现一个订阅者
Watcher
每个Watcher都绑定一个更新函数,可以把收到的属性变化通知并执行相应的函数,更新视图
- 实现一个解析器
compile
可以循环解析全部节点获取相关指令,初始化数据,初始化订阅
原文参考 Vue3.x双向绑定原理的实现参考