本文最后更新于:2022年7月19日 上午
在 浅谈 ES6 中的 Proxy 用法 中我们提到,在 Vue2 中使用 Object.defineProperty
和 Vue3 中使用 Proxy
实现了双向绑定。那么它是如何具体实现的?以下,我们便探讨如何自己手写一个双向绑定。
Object.defineProperty
首先我们的 HTML 中渲染一个 <p>
标签 和一个 <input>
标签。
1 2 3 4
| <div id="app"> <p class="title-1">Vue Demo: Object.defineProperty()</p> <input class="input-1" /> </div>
|
如果我们通过 Object.defineProperty
来实现双向绑定,我们需要重写 data
中的 get
和 set
方法。示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| let data = { content: '' }
let el = { title: document.querySelector('.title-1'), input: document.querySelector('.input-1') }
Object.defineProperty(data,'content',{ get(){ return el.title.innerHTML }, set(val){ el.input.value = val el.title.innerHTML = val } })
|
但这还不够,既然是双向绑定,那当我们输入值的时候,输入框里面的值更改也会影响到 data.content
的值,所以我们还得对 input
加入一个 keyup
或者 input
的监听事件。如下:
1 2 3 4
| el.input.addEventListener('keyup',(e)=>{ data.content = e.target.value })
|
在线示例
CodePen.io
具体效果如下:
Proxy
然后我们使用 Proxy
来实现这个双向绑定。同理,我们在页面渲染一个 <p>
标签和一个 <input>
标签。
1 2 3 4
| <div id="app"> <p class="title-2">Vue Demo: Proxy</p> <input class="input-2" /> </div>
|
js代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let data = { content: '' }
let el = { title: document.querySelector('.title-2'), input: document.querySelector('.input-2') }
data = new Proxy(data,{ get(target,property,receiver){ return Reflect.get(target,property,receiver) }, set(target,property,value,receiver){ el.input.value = value el.title.innerHTML = value return Reflect.set(...arguments) } })
el.input.addEventListener('keyup',(e)=>{ data.content = e.target.value })
|
在线示例
CodePen.io
具体效果如下:
Demo
以上只是开胃菜,现在我们还实现一个稍微完整点的Demo。包含 v-bind
v-model
@click
等。
HTML代码如下:
1 2 3 4 5 6 7
| <div id="app"> <p v-bind="count"></p> <input type="text" v-model="count"/> <button @click="add">+</button> <button @click="reduce">-</button> <button @click="reset">Reset</button> </div>
|
js代码如下:
Vue类
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
| class Vue{ constructor(options){ const vm = this vm.$el = document.querySelector(options.el) vm.$methods = options.methods let data = options.data data = vm._data = typeof data === "function" ? data.call(vm,vm) : data || {} vm._binding = {} vm._observer(data) vm._compile(vm.$el) } _pushWatcher(watcher){ if(!this._binding[watcher.key]){ this._binding[watcher.key] = [] } this._binding[watcher.key].push(watcher) } _observer(data){ const vm = this const handler = { set(target,property,value){ const res = Reflect.set(...arguments) vm._binding[property].map(item => { item.update() }) return res } } vm._data = new Proxy(data,handler) } _compile(root){ const nodes = [...root.children] const data = this._data nodes.map(node => { if(node.children && node.children.length){ this._compile(node.children) } const $input = node.tagName.toLowerCase() === 'input' const $textarea = node.tagName.toLowerCase() === 'textarea' const $vmodel = node.hasAttribute('v-model') if($vmodel && ($input || $textarea)){ const key = node.getAttribute('v-model') this._pushWatcher(new Watcher(node,'value',data,key)) node.addEventListener('input',() =>{ data[key] = node.value }) } if(node.hasAttribute('v-bind')){ const key = node.getAttribute('v-bind') this._pushWatcher(new Watcher(node,'innerHTML',data,key)) } if(node.hasAttribute('@click')){ const name = node.getAttribute('@click') const method = this.$methods[name].bind(data) node.addEventListener('click',method) } }) } }
|
Watcher类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Watcher{ constructor(node,attr,data,key){ this.node = node this.attr = attr this.data = data this.key = key this.update() } update(){ let value = Reflect.get(this.data,this.key) Reflect.set(this.node,this.attr,value) } }
|
Vue实例
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 count = 99
new Vue({ el: "#app", data(){ return{ count: count } }, methods:{ add(){ this.count++ }, reduce(){ this.count-- }, reset(){ this.count = count } } })
|
在线示例
CodePen.io
具体效果如下:
THE END
综上便是一个 Vue 双向绑定的 Demo 实现。实际上 Vue 的源码层面还是稍微有点复杂的,在整个实例创建过程中还涉及到虚拟DOM、生命周期(Hook函数)等等操作。有兴趣可以直接前往 Vue 仓库查看具体代码实现。