今天来实现一个简单的 Vue 数据响应式,最终达到两个基本效果:
- 当用户在 input 中输入内容时,文本节点会跟着改变
- 当直接更新 message 属性后,页面中 input 标签和文本节点的值会跟着改变
最终效果:
原理
Vue 实现数据响应式的核心原理是:借助发布/订阅模式 + 数据劫持 。
文件结构
1 2 3 4 5 6 7 8
| . ├── Compiler.js # 指令解析器 ├── Dep.js # 消息订阅器 ├── Observer.js # 观察者 ├── Vue.js # Vue的简单实现 ├── Watcher.js # 订阅者 ├── index.html └── main.js # 入口文件
|
具体代码
index.html
包含一个 input 标签和文本节点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <!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>Document</title> </head>
<body> <div id="app"> <input type="text" v-model="message" /> {{message}} </div> <script src="dist/main.js"></script> </body> </html>
|
Dep.js 消息订阅器
发布订阅者模式的简单实现。包含核心方法 listen
以及 notify
,listen
负责给属性添加订阅者,notify
负责在属性发生变化时通知所有该属性的订阅者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Dep { constructor() { console.log('【Dep】实例化了一个消息订阅器'); this.list = []; } listen(sub) { this.list.push(sub); } notify() { this.list.forEach(function (item, index) { item.update(); }); } } Dep.prototype.target = null;
export default Dep;
|
main.js 入口文件
该文件中 new 一个 Vue 实例,并把该实例挂载到 window 对象上。
1 2 3 4 5 6 7 8 9
| import Vue from './Vue'; var vm = new Vue({ el: '#app', data: { message: 'lance' } });
window.vm = vm;
|
Observer.js 观察者
深度遍历 data 下的所有属性,利用 Object.defineProperty
把属性转为 getter/setter
,并在此过程中给每个属性都绑定上发布/订阅模块。
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
| import Dep from './Dep'; class Observer { constructor(data) { this.data = data; Object.keys(this.data).forEach(key => { this._bind(data, key, data[key]); }); } _bind(data, key, val) { var myDep = new Dep(); Object.defineProperty(data, key, { get() { if (Dep.target) myDep.listen(Dep.target); return val; }, set(newValue) { if (newValue === val) return; val = newValue; myDep.notify(); } }); } }
export default Observer;
|
Watcher.js 订阅者
实现 update 方法,在属性发生变化时收到通知并执行它,从而更新组件数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import Dep from './Dep'; class Watcher { constructor(node, name, vm) { this.node = node; this.name = name; this.vm = vm; Dep.target = this; this.update(); Dep.target = null; } update() { if (this.node.nodeType === 1) { this.node.value = this.vm[this.name]; } this.node.nodeValue = this.vm[this.name]; } }
export default Watcher;
|
Compiler.js 指令解析器
负责解析挂载到 Vue 上的 html 模板,找出 Vue 相关的指令。例如遇到 input 标签上有 v-model
,则给绑定的属性添加订阅者(Watcher),并为 input 标签添加 input 事件,在用户输入 value 后更改 data 中相应属性的值。
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
| import Watcher from './Watcher';
const REG = /\{\{(.*)\}\}/; class Compiler { constructor(el, vm) { this.el = document.querySelector(el); this.vm = vm; this.frag = this._createFragment(); this.el.appendChild(this.frag); } _createFragment() { var frag = document.createDocumentFragment(); var child; while ((child = this.el.firstChild)) { this._compile(child); frag.appendChild(child); } return frag; } _compile(node) { var self = this; if (node.nodeType === 1) { var attr = node.attributes; if (attr.hasOwnProperty('v-model')) { var name = attr['v-model'].nodeValue; node.addEventListener('input', function (e) { self.vm[name] = e.target.value; }); node.value = this.vm[name]; new Watcher(node, name, this.vm); } } if (node.nodeType === 3) { if (REG.test(node.nodeValue)) { var name = RegExp.$1; name = name.trim(); console.log('【Compiler】解析到文本元素,给它创建一个watcher实例'); new Watcher(node, name, this.vm); } } } }
export default Compiler;
|
Vue.js
调用 Compiler
和 Observer
,实现数据响应式。
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
| import Observer from './Observer'; import Compiler from './Compiler';
class Vue { constructor(options) { this.$options = options; this.$el = this.$options.el; this._data = this.$options.data; Object.keys(this._data).forEach(key => { this._proxy(key); }); new Observer(this._data); new Compiler(this.$el, this); } _proxy(key) { var self = this; Object.defineProperty(this, key, { get() { return self._data[key]; }, set(value) { self._data[key] = value; } }); } }
export default Vue;
|
最后
根目录下运行 webpack main.js
进行打包,浏览器运行 index.html
文件后便能看到文章开头的实现效果,完。