实现一个双向绑定效果
期望最终实现一个数据与 DOM 双向绑定的效果,使用方式类似于 vue,如下所示。
const vm = new SimpleVue({
el: '#app',
data: {
value: '',
},
mounted() {
setTimeout(() => {
this.value = '12';
}, 1000);
},
methods: {
handleClick() {
alert(this.value);
},
},
});
直接上代码,其主要用到观察与订阅设计模式,其中observe
是观察者,Watcher
属于订阅者,代码中注释写的比较详细,就不赘述了。
SimpleVue.js
首先是主入口 SimpleVue.js 的代码。
export default class SimpleVue {
constructor(options) {
this.data = options.data;
this.methods = options.methods;
Object.keys(this.data).forEach(this._proxyKey.bind(this));
observe(this.data);
// 解析与编译模板
new Compile(options.el, this);
// 调用mounted钩子
options.mounted.call(this);
}
// 将data中的属性挂载到this上
_proxyKey(key) {
Object.defineProperty(this, key, {
get: () => {
return this.data[key];
},
set: (value) => {
this.data[key] = value;
},
});
}
}
// 观察者,主要是用来监听data的改变
// 在这里主要起到了数据Model与Dom操作之间的桥梁作用
// 在数据获取的时候添加订阅,订阅中Watcher处理DOM更新相关的事情
// 在设置数据时遍历订阅列表,执行订阅列表中的订阅事件,相应的,Watcher就能收到通知,执行回调
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, value) {
const dep = new Dep();
// 递归观察数据
observe(value);
Object.defineProperty(data, key, {
get: () => {
// Dep.target其实是一个Watcher,在获取数据时将Watcher添加到订阅数组中
if (Dep.target) {
dep.add(Dep.target);
}
return value;
},
set: (newValue) => {
// 更新data数据时,会从订阅列表中取出所有订阅,通知Watcher执行相关的更新操作
if (newValue !== value) {
value = newValue;
dep.notify();
}
},
});
}
Dep.js
主要用来添加订阅和通知。
export default class Dep {
constructor() {
// 订阅列表
this.subs = [];
}
// 添加订阅
add(sub) {
this.subs.push(sub);
}
// 遍历订阅列表,执行Watcher动作
notify() {
this.subs.forEach((sub) => {
// 执行watcher中的update
sub.update();
});
}
}
Watcher.js
订阅的之后的动作怎么触发,Watcher 登场。
// Watcher是订阅列表中的每一个动作
// 举个例子,比如微信订阅公众号,当作者在公众号发布了文章后,订阅过该公众号的读者将会收到文章更新的推送,那么这里的文章推送可以看做是订阅主体执行的动作,即这里的Watcher
export default class Watcher {
constructor(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
// 在实例化时强制执行get
this.value = this.get();
}
update() {
this.run();
}
run() {
const value = this.vm.data[this.exp];
const oldValue = this.value;
// 当value值不一样的时候再执行相关动作
if (value !== oldValue) {
this.value = value;
// Watcher的动作触发
this.cb.call(this.vm, value);
}
}
// 在执行get的时候会将当前的Watcher实例赋值给Dep.target,之后在执行data属性值的获取
// 在获取value的时候,会触发data的get,那么SimpleVue类中data的这条数据就会被添加订阅,即dep.add(Dep.target)
get() {
Dep.target = this;
const value = this.vm.data[this.exp];
Dep.target = null;
return value;
}
}
Compile.js
一切都准备就绪了,Model 数据变化了,我们需要将它更新到 DOM 中,Compile 来负责处理。
export default class Compile {
constructor(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init();
}
init() {
if (this.el) {
// 因为遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点 el 转换成文档碎片 fragment 进行解析编译操作,解析完成,再将 fragment 添加回原来的真实 dom 节点中
this.fragment = this.nodeToFragment(this.el);
this.compileElement(this.fragment);
this.el.appendChild(this.fragment);
} else {
throw new Error('Dom element not exist');
}
}
nodeToFragment(el) {
const fragment = document.createDocumentFragment();
// 从第一个子元素开始遍历
let child = el.firstChild;
// 遍历所有的子元素,将其碎片化处理
while (child) {
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
}
// 解析与编译节点元素,比如{{}}语法,v-model,v-on等等
compileElement(el) {
const childNodes = el.childNodes;
Array.prototype.forEach.call(childNodes, (node) => {
const reg = /\{\{(.*)\}\}/;
const text = node.textContent;
if (this.isElementNode(node)) {
// 如果是一个html元素,那么需要解析这个html元素
this.compile(node);
} else if (this.isTextNode(node) && reg.test(text)) {
// 如果是一个文本节点并且是相关的语法结构,即:{{}},就解析文本
this.compileText(node, reg.exec(text)[1]);
}
// 递归处理所有节点
if (node.childNodes && node.childNodes.length) {
this.compileElement(node);
}
});
}
compile(node) {
const nodeAttrs = node.attributes;
// 因为是一个普通的html元素,那么这些指定是添加在节点属性上的,所以需要遍历所有属性,找出匹配项
// 判断符合条件的几种情况,指令,如v-on,v-model等
Array.prototype.forEach.call(nodeAttrs, (attr) => {
const attrName = attr.name;
const exp = attr.value;
const dir = attrName.substring(2);
if (this.isDirective(attrName)) {
if (this.isEventDirective(dir)) {
this.compileEvent(node, this.vm, exp, dir);
} else {
this.compileModel(node, this.vm, exp);
}
// 内部的指令解析后在html节点上删除,因为这些属性并不一定是html标准的属性
node.removeAttribute(attrName);
}
});
}
compileText(node, exp) {
const initText = this.vm[exp];
// 初始化
this.updateText(node, initText);
// 注册监听,监听的是this.vm[exp]的变化
new Watcher(this.vm, exp, (value) => {
// 订阅事件(sub)触发后的动作
// Dep中的sub.update执行后,最终会执行这里
this.updateText(node, value);
});
}
compileEvent(node, vm, exp, dir) {
// 解析v-on:click形式的事件绑定等
// 解析后给节点绑定上相关事件监听
const eventType = dir.split(':')[1];
const cb = vm.methods && vm.methods[exp];
if (eventType && cb) {
node.addEventListener(eventType, cb.bind(vm), false);
}
}
compileModel(node, vm, exp) {
let val = vm[exp];
this.modelUpdater(node, val);
// v-model内部其实是监听input事件和设置input的value实现的双向绑定
node.addEventListener('input', (e) => {
const newValue = e.target.value;
// 实现 view 到 model 的绑定
this.vm[exp] = newValue;
});
// 注册监听,监听的是this.vm[exp]的变化
new Watcher(vm, exp, (value) => {
// 当data中exp值变化的时候,也希望同时反馈到node节点的value中
node.value = typeof value === 'undefined' ? '' : value;
});
}
modelUpdater(node, value) {
node.value = typeof value === 'undefined' ? '' : value;
}
updateText(node, value) {
node.textContent = typeof value === 'undefined' ? '' : value;
}
isEventDirective(dir) {
return dir.indexOf('on:') === 0;
}
isDirective(attr) {
return attr.indexOf('v-') === 0;
}
isElementNode(node) {
return node.nodeType === 1;
}
isTextNode(node) {
return node.nodeType === 3;
}
}
实际效果
那么实际运行效果如何?如下所示。
如果您觉得本文对您有用,欢迎捐赠或留言~
- 本博客所有文章除特别声明外,均可转载和分享,转载请注明出处!
- 本文地址:https://www.leevii.com/?p=2971