实现一个 Vue 的双向绑定
sisterAn opened this issue · comments
瓶子君 commented
测试html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<!-- <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> -->
<script src="./vue.js"></script>
<body>
<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" @click="increment">增加</button>
</form>
<h3 v-text="number"></h3>
</div>
<script>
const vm = new Vue({
el:'#app',
data: {
number: 0
},
methods: {
increment: function() {
this.number ++;
},
}
})
</script>
</body>
</html>
Vue双向绑定实现:
class Observer{
constructor(data){
this.$data = data;
this.observer(this.$data);
}
observer(obj){
if(typeof obj !== 'object') return;
Object.keys(obj).forEach(key =>{
this.defineReactive(obj, key, obj[key]);
})
}
defineReactive(obj, key, value){
if(typeof value === 'object') this.observer(value);
let dep = new Dep();
Object.defineProperty(obj, key, {
get(){
if(window.target){
dep.addSubs();
}
return value;
},
set: (newVal) =>{
if(value === newVal) return;
// 防止 newVal为对象的情况,需要重新将对象中的属性变为响应式
this.observer();
value = newVal;
dep.notify();
}
})
}
}
class Dep{
constructor(){
this.subs = [];
}
addSubs(){
this.subs.push(window.target);
}
notify(){
this.subs.forEach(watcher => watcher.update());
}
}
class Watcher{
constructor(vm, expr, cb){
this.$vm = vm;
this.expr = expr;
this.cb = cb;
this.getter();
}
update(){
let newVal;
if(typeof this.expr === 'function'){
newVal = this.expr();
} else {
newVal = compileUtil.getValue(this.expr, this.$vm);
}
// let newVal = compileUtil.getValue(this.expr, this.$vm);
if(this.value === newVal) return;
this.value = newVal;
this.cb();
}
getter(){
window.target = this;
if(typeof this.expr === 'function'){
this.value = this.expr();
} else {
this.value = compileUtil.getValue(this.expr, this.$vm);
}
window.target = null;
}
}
// Class版
class Vue{
constructor(options){
this.$data = options.data;
this.$el = options.el;
this.$option = options;
if(this.$el){
// 将数据变为响应式
new Observer(this.$data)
// 代理$data
this.proxyVm(this.$data)
// 代理computed
this.proxyVm(this.$option.computed)
// 编译模板
new Compile(this.$el, this)
}
}
proxyVm(data) {
for(let key in data){
Object.defineProperty(this, key, {
get(){
return data[key];
},
set(newVal){
data[key] = newVal;
}
})
}
}
}
class Compile{
constructor(el, vm){
this.$el = this.isElementNode(el) ? el: document.querySelector(el);
this.$vm = vm;
// 在内存中创建一个和 $el相同的元素节点
let fragment = this.node2fragment(this.$el);
// 解析模板($el节点)
this.compile(fragment);
// 将解析后的节点重新挂载到DOM树上
this.$el.appendChild(fragment);
}
// 判断node是否为元素节点
isElementNode(node) {
return node.nodeType === 1;
}
// 判断是否为v-开头的Vue指令
isDirective(attr) {
return attr.startsWith('v-');
}
isSpecialisDirective(attr){
return attr.startsWith('@');
}
compile(fragment){
// 获取根节点的子节点
let childNodes = fragment.childNodes;
[...childNodes].forEach(child =>{
if(this.isElementNode(child)){
// 解析元素节点的属性,查看是否存在Vue指令
this.compileElement(child);
// 如果子节点也是元素节点,则递归执行该函数
this.compile(child);
}else{
// 解析文本节点,查看是否存在"{{}}"
this.compileText(child);
}
})
}
// 编译元素
compileElement(node){
// 获取元素节点的所有属性
let attrs = node.attributes;
// 遍历所有属性,查找是否存在Vue指令
[...attrs].forEach(attr =>{
// name: 属性名, expr: 属性值
let {name, value:expr} = attr;
// 判断是不是指令
if(this.isDirective(name)){
let [,directive] = name.split('-');
// 如果为指令则去设置该节点的响应式函数
compileUtil[directive](node, expr, this.$vm);
}
if(this.isSpecialisDirective(name)){
let eventName = name.substr(1);
compileUtil['on'](node, eventName, expr, this.$vm);
}
})
}
// 编辑文本
compileText(node){
let content = node.textContent;
// 匹配 {{xxx}}
if(/\{\{(.+?)\}\}/.test(content)){
compileUtil['contentText'](node, content, this.$vm);
}
}
// 把节点移动到内存中
node2fragment(node){
// 创建文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = node.firstChild){
// appendChild具有移动性
fragment.appendChild(firstChild);
}
return fragment;
}
}
const compileUtil = {
getValue(expr, vm){
let valOrFn = expr.split('.').reduce((totalValue, key) =>{
if(!totalValue[key]) return null;
return totalValue[key];
}, vm)
return typeof valOrFn === 'function' ? valOrFn.call(vm) : valOrFn;
},
setValue(expr, vm, value){
return expr.split('.').reduce((totalValue, key, index, arr) =>{
if(index === arr.length - 1) totalValue[key] = value;
return totalValue[key];
}, vm.$data)
},
getContentValue(content, vm){
return content.replace(/\{\{(.+?)\}\}/g, (...args) =>{
return this.getValue(args[1], vm);
})
},
contentText(node, content, vm){
let fn = () =>{
this.textUpdater(node, this.getContentValue(content, vm));
}
let resText = content.replace(/\{\{(.+?)\}\}/g, (...args) =>{
// args[1] 为{{xxx}}中的xxx
new Watcher(vm, args[1], fn);
return this.getValue(args[1], vm);
});
// 首次解析直接替换文本内容
this.textUpdater(node, resText);
},
text(node, expr, vm){
let value = this.getValue(expr, vm);
this.textUpdater(node, value);
let fn = () =>this.textUpdater(node, this.getValue(expr, vm));
new Watcher(vm, expr, fn);
},
textUpdater(node, value){
node.textContent = value;
},
html(node, expr, vm){
let value = this.getValue(expr, vm);
this.htmlUpdater(node, value);
let fn = () =>this.htmlUpdater(node, this.getValue(expr, vm));
new Watcher(vm, expr, fn);
},
htmlUpdater(node, value){
node.textContent = value;
},
model(node, expr, vm){
let value = this.getValue(expr, vm);
this.modelUpdater(node, value);
let fn = () => this.modelUpdater(node, this.getValue(expr, vm));
node.addEventListener('input', ()=>{
this.setValue(expr, vm, node.value);
})
new Watcher(vm, expr, fn)
},
modelUpdater(node, value){
node.value = value;
},
on(node, eventName, expr, vm){
// 改变this为vm实例
let fn = vm.$option.methods[expr].bind(vm);
// 添加事件
node.addEventListener(eventName, fn);
}
}