双向绑定一直是一个高频考点,今天就来实现一个简单的Vue双向绑定。设框架名称为Wue,双向绑定指令为w-model。主要方法是observer、watcher、complier。分别用来给属性绑定setter,连接observer与complier,根据模版语法添加对应的watcher并且更新模版中的值
observer思路
1 2 3 4 5 6 7 8 9
| start=>start: 开始 end=>end: 结束 op1=>operation: 遍历Wue.data中的所有属性,为每个属性添加getter/setter op2=>operation: 手动设置data中的属性值,触发setter op3=>operation: setter触发watcher
start->op1->op2->op3->end
|
watcher(在本文中更像是writer)相当于中间人。手动设置data属性后,setter触发对应data属性的watcher,watcher更新UI;input等加了双向绑定指令”w-model”的值更新后,触发对应绑定事件,在回调方法中触发watcher,更新data属性
watcher思路
1 2 3 4 5 6 7 8 9 10 11 12 13
| start=>start: 开始 end=>end: 结束 op1=>operation: 触发watcher,更新input的value op2=>operation: 触发watcher,更新data的属性 condition=>condition: data属性更新 dataIo=>inputoutput: data属性更新 inputIo=>inputoutput: input属性更新
start->condition condition(yes)->dataIo->op1->end condition(no)->inputIo->op2->end
|
watcher是在分析dom元素时添加的。先遍历dom节点,找到添加了w-model或使用指定的插值语法输入的属性,给它们添加watcher。这部分的实现者是complier
complier思路
1 2 3 4 5 6 7 8 9 10 11 12
| start=>start: 开始 end=>end: 结束 op1=>operation: 遍历dom节点 op2=>operation: input标签添加input事件,更新数据后更改data中的属性 op3=>operation: 给双大括号语法中的属性添加watcher,将其值替换为data属性中的值 condition=>condition: 是否有w-model或双大括号语法
start->op1->condition condition(yes)->op2->op3->end condition(no)->end
|
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <!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="root"> w-model-name: <input type="text" w-model="name"> <div>{{ name }}</div> w-model-age: <input type="text" w-model="age"> <div>{{ age }}</div> no w-model: <input type="text"> <input class="submit" type="button" value="setData"> </div> </body> </html>
|
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
|
import './index.html'; import observer from './observer'; import getElement from './complier';
function Wue(options = {}) { this.$el = document.querySelector(options.el); this.$data = options.data; this._watchers = {}; this._observer(this.$data); this._complier(this.$el); } Wue.prototype._observer = observer; Wue.prototype._complier = getElement;
const app = new Wue({ el: '#root', data: { name: 'Alice', age: 18 } }); window.app = app;
document.querySelector('.submit').addEventListener('click', function() { app.$data.name = 'Tom'; }, false)
|
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
|
import Watcher from './watcher';
function getElement(element) { const _this = this; const nodeList = element.children; for (let i = 0; i < nodeList.length; i += 1) { complier.call(_this, nodeList[i]); } } function complier(element) { const _this = this; if (element.children.length) { complier(element); } if (element.hasAttribute('w-model') && element.tagName === 'INPUT') { const attr = element.getAttribute('w-model'); _this._watchers[attr].push( new Watcher({ el: element, val: attr, vm: _this, attr: 'value', }), ); element.addEventListener( 'input', function() { _this.$data[attr] = element.value; }, false, ); } const tagReg = /^\{\{\s*(.*\S)\s*\}\}$/; let textNode = element.textContent; if (tagReg.test(textNode)) { textNode = textNode.replace(tagReg, (matched, matchedVal) => { let watcher = _this._watchers[matchedVal]; if (!watcher) { watcher = []; } watcher.push( new Watcher({ el: element, vm: _this, val: matchedVal, attr: 'innerHTML', }), ); }); } }
export default getElement;
|
observer.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
| function observer(obj) { const _this = this; if (!obj || typeof obj !== 'object') { return; } Object.keys(obj).forEach(key => { initWatcher.call(_this, key); defineObjProperty.call(_this, obj, key, obj[key]); }) }
function initWatcher(key) { this._watchers[key] = []; }
function defineObjProperty(obj, key, value) { observer(value); const watchersPool = this._watchers; Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { return value; }, set(newVal) { if (newVal !== value) { value = newVal; Object.keys(watchersPool).forEach(key => { watchersPool[key].forEach(item => { item.update(); }); }) } } }) } export default observer;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
function Watcher({el, vm, val, attr} = options) { this.el = el; this.vm = vm; this.val = val; this.attr = attr; this.update(); } Watcher.prototype.update = function () { this.el[this.attr] = this.vm.$data[this.val]; }
export default Watcher;
|