面试问了好几次vue的双向绑定。。。我思来想去,单向绑定是拦截器加上发布订阅者模式实现的,双向绑定不就是额外绑定一个input事件?
在文章开头,先声明这篇文章是深度参考剖析Vue实现原理 - 如何实现双向绑定mvvm的产物。并且这只是一个demo,极度精简,不会考虑太多只是为了实现双向绑定。
本文代码
其他的先不说,先放张大纲图
根据这张图我们可以看出,vue实现双向绑定的流程。
- 先通过观察者Observer劫持监听data所有属性,getter添加订阅者,setter通知订阅者。
- Compile初始化dom,解析指令,将dom中与vm实例有联系的节点更新函数,作为订阅者,存入依赖管理器Dep中。
- 当vm实例的数据发生变化时,观察者将变化通知给依赖管理器。
- 依赖管理器调用与该变化相关观察者的更新函数。
- 当dom中的input或textarea发生变化时,通过监听input事件,将input,textarea标签的value赋值到vm实例上。
接下来我们详细讲下每个过程。
Dep
首先我们先来完成一个依赖管理器。根据我们的需求,我们需要这个依赖管理器可以添加订阅者,和通知订阅者更新
由于我们是通过拦截每个属性的getter来添加订阅者,因此为了能确定属性的dep应该添加那个订阅者,我们还需要单独维护一个target变量,用来确定订阅者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
let uid = 0; export class Dep { constructor() { this.id = uid ++; this.subs = []; }
addSub(watcher) { this.subs.push(watcher); }
notify() { this.subs.forEach(function(watcher) { watcher.update(); }) } }
Dep.target = null;
|
Watcher
现在我们来编写一个订阅者类,根据需求我们需要将订阅者添加到依赖管理器的功能,和依赖管理器通知变化后的更新功能。
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
| import { Dep } from './Dep.js' export class Watcher { constructor(vm, expFn) { this.expFn = expFn; this.vm = vm; this.depIds = new Set(); this.get(); } addDep(dep) { if (!this.depIds.has(dep.id)) { this.depIds.add(dep.id); dep.addSub(this); } }
update() { this.expFn.call(this.vm); }
get() { Dep.target = this; this.expFn.call(this.vm); Dep.target = null; } }
|
Observer
接下来我们完成一个观察者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 32 33 34 35
| import { Dep } from './Dep.js'
class Observer { constructor(obj) { Object.keys(obj).forEach(key => { defineReactive(obj, key); }) } }
export function observe(obj) { if (typeof obj === 'object') { new Observer(obj); } }
export function defineReactive(obj, key) { let val = obj[key]; const dep = new Dep(); Object.defineProperty(obj, key, { get() { if (Dep.target) { Dep.target.addDep(dep); } return val; }, set(v) { if (val !== v) { val = v; dep.notify(); } } }) }
|
Compile
接下来我们实现一个解析模版指令的函数,它的作用是解析类似`v-model`,`{{}}`,等模版指令(这里我们只做`{{}}`,`v-model`的解析。)同时将设置了`v-model`的input和textarea标签绑定上input事件,实现双向绑定。
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
| import { Watcher } from './Watcher.js' export function compile(vm, root) { traverse(root, node => { if (isTextNode(node)) { const text = node.textContent; let match = text.match(/\{\{.+\}\}/g) if (!match) { return ; } match = match.map(name => { return name.replace(/\{|\}/g, '').trim(); }); match.forEach(item => { if (vm[item]) { const re = new RegExp(`\\{\\{ {0,}${item} {0,}\\}\\}`, 'g'); const origin = node.textContent; new Watcher(vm, () => { node.textContent = origin.replace(re, vm[item]);
}) } })
} else if (isElementNode(node)) { Object.keys(compileUtil).forEach(key => { const name = node.getAttribute(`v-${key}`); if (name) { compileUtil[key](vm, node, name); } }) } })
}
function traverse(root, callback) { if (!root) { return; } callback(root); if (root.childNodes) { Array.from(root.childNodes).forEach(node => { traverse(node, callback); }) } }
function isTextNode(node) { if (node.nodeType === 3) { return true; } }
function isElementNode(node) { return node.nodeType == 1; }
const compileUtil = { model(vm, ele, name) { if (ele.tagName === 'INPUT' || ele.tagName === 'TEXTAREA') { ele.addEventListener('input', e => { vm[name] = ele.value; }) new Watcher(vm, () => { ele.value = vm[name]; }) } } }
|
入口类(vm)
在写一个入口类,将Watcher
Dep
Observer
Compile
封装起来。
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
| import { observe } from './Observer.js' import { compile } from './Compile.js' class Vue { constructor(config) { if (!config) { return null; } const { el, data } = config; this.$el = document.querySelector(el); if (data) { this.$data = { ...data }; observe(this.$data); Object.keys(this.$data).forEach(key => { proxy(this.$data, key, this); }) } compile(this, this.$el); } }
function proxy(obj, key, goalObj) { const descriptor = Object.getOwnPropertyDescriptor(obj, key); Object.defineProperty(goalObj, key, { get() { if (descriptor.get) { return descriptor.get.call(obj); } else { return descriptor.value; } }, set(v) { if (descriptor.set) { descriptor.set.call(obj, v); } } }) }
|
最后测试一下吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>vue2双向绑定demo测试</title> </head> <body> <div id="App"> <input type="text" v-model="text"> {{ text }} </div> </body> <script type="module" src="./dist.js"></script> </html>
|
1 2 3 4 5 6 7 8 9
| import Vue from './js/index.js'
new Vue({ el: '#App', data: { text: 'HelloWorld', } })
|