这里什么说是vue2.0的双向绑定原理是因为和vue3.0的实现方式是有区别的。
下图是一个vue数据双向绑定的过程:
下面通过类来定义上图中的Observer,Dep,Watcher,Compile等来抽象出数据的双向绑定。
//定义一个容器类 来存放所有的订阅者
class Dep {
constructor() {
this.subs = [];
}
//订阅
addSub(watcher) {
this.subs.push(watcher);
}
//发布
notify() {
this.subs.forEach((watcher) => watcher.update());
}
}
//观察者:将数据劫持和页面联系起来
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
//默认存放一个老值
this.oldValue = this.get();
}
get() {
Dep.target = this; //先把自己放在全局上
//取值,把观察者和数据联系起来
let value = CompileUtil.getVal(this.vm, this.expr);
//不取消任何取值都会添加watcher
Dep.target = null;
return value;
}
update() {
//更新操作,数据变化后会调用观察者update方法
let newVal = CompileUtil.getVal(this.vm, this.expr);
if (newVal !== this.oldValue) {
this.cb(newVal);
}
}
}
//将data里的所有属性包括对象里的属性劫持
class Observer {
constructor(data) {
this.observer(data);
}
observer(data) {
if (data && typeof data == "object") {
for (let key in data) {
this.defineReactive(data, key, data[key]);
}
}
}
defineReactive(obj, key, value) {
//value还是对象的话要继续,才会给全部都赋予get和set方法
this.observer(value);
let dep = new Dep(); //给每个属性都加上一个发布订阅功能
Object.defineProperty(obj, key, {
get() {
//创建watcher时候,会取到对应内容,并且把watcher放到全局上
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newVal) {
//若赋值的是一个对象,还需要继续监控
if (newVal != value) {
this.observer(newVal);
value = newVal;
dep.notify();
}
},
});
}
}
class Compiler {
constructor(el, vm) {
//判断el属性
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
//把当前节点中的元素获取到,并放到内存中
let fragment = this.node2fragment(this.el);
//把节点中内容进行替换
//编译模板,用数据编译
this.compile(fragment);
//把内容塞回页面
this.el.appendChild(fragment);
}
//判断是不是指令
isDirective(attrName) {
return attrName.startsWith("v-"); //开头
}
//编译元素的方法
compileElement(node) {
let attributes = node.attributes; //类数组
[...attributes].forEach((attr) => {
let { name, value: expr } = attr;
if (this.isDirective(name)) {
//v-model v-html v-bind
let [, directive] = name.split("-"); //v-on:click
let [directiveName, eventName] = directive.split(":");
//调用不同指令来处理
CompileUtil[directiveName](node, expr, this.vm, eventName);
}
});
}
//编译文本的方法
compileText(node) {
//判断文本节点中是否包含{{}}
let content = node.textContent;
//(.+?)匹配一个大括号内的,一个及以上,到第一个大括号结束时候结束
if (/\{\{(.+?)\}\}/.test(content)) {
CompileUtil["text"](node, content, this.vm);
}
}
//编译的核心方法
compile(node) {
let childNodes = node.childNodes;
[...childNodes].forEach((child) => {
if (this.isElementNode(child)) {
this.compileElement(child);
this.compile(child); //递归,获得内层
} else {
this.compileText(child);
}
});
}
node2fragment(node) {
//创建一个文本碎片
let fragment = document.createDocumentFragment();
let firstChild;
while ((firstChild = node.firstChild)) {
//appendChild具有移动性
fragment.appendChild(firstChild);
}
return fragment;
}
isElementNode(node) {
//判断是否为元素节点
return node.nodeType === 1;
}
}
//绑定处理事件的各种方法
CompileUtil = {
//取得对应的数据
getVal(vm, expr) {
//vm.$data 'school.name'
//返回name
return expr.split(".").reduce((data, current) => {
return data[current]; //继续取值,取到name
}, vm.$data);
},
setValue(vm, expr, value) {
expr.split(".").reduce((data, current, index, arr) => {
if (index == arr.length - 1) {
return (data[current] = value);
}
return data[current];
}, vm.$data);
},
model(node, expr, vm) {
//node节点,expr是表达式,vm是当前实例
let fn = this.updater["modeUpdater"];
//给输入框加一个观察者,稍后数据更新就会触发此方法,将新值给输入框赋予值
new Watcher(vm, expr, (newVal) => {
fn(node, newVal);
});
node.addEventListener("input", (e) => {
let value = e.target.value; //获取用户输入的内容
this.setValue(vm, expr, value);
});
let value = this.getVal(vm, expr);
fn(node, value);
},
html(node, expr, vm) {
let fn = this.updater["htmlUpdater"];
new Watcher(vm, expr, (newVal) => {
fn(node, newVal);
});
let value = this.getVal(vm, expr);
fn(node, value);
},
getContentValue(vm, expr) {
//遍历一个表达式,将内容重新替换成一个完整的内容,返还回去
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getVal(vm, args[1]);
});
},
on(node, expr, vm, eventName) {
// v-on:click="change" expr就是change
node.addEventListener(eventName, (e) => {
vm[expr].call(vm, e); //this.change
});
},
text(node, expr, vm) {
let fn = this.updater["textUpdater"];
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
//给表达式每个{{}}都加个观察者
new Watcher(vm, args[1], (newVal) => {
fn(node, this.getContentValue(vm, expr)); //返回一个全的字符串
});
return this.getVal(vm, args[1]);
});
fn(node, content);
},
//更新视图
updater: {
//把数据插入节点当中
modeUpdater(node, value) {
node.value = value;
},
textUpdater(node, value) {
node.textContent = value;
},
htmlUpdater(node, value) {
//xss攻击
node.innerHTML = value;
},
},
};
// 基类,调度
class Vue {
constructor(options) {
this.$el = options.el;
this.$data = options.data;
let computed = options.computed;
let methods = options.methods;
if (this.$el) {
//把数据全部转换成用Object.defineProperty来定义,数据劫持
new Observer(this.$data);
//{{getNewName}} reduce 取值是vm.$data.getNewName
for (let key in computed) {
Object.defineProperty(this.$data, key, {
get: () => {
//注意this指向el实例
return computed[key].call(this);
},
});
}
for (let key in methods) {
Object.defineProperty(this, key, {
get: () => {
//注意this指向el实例
return methods[key];
},
});
}
//把数据获取操作vm上的取值操作都代理到vm.$data上
this.proxyVm(this.$data);
new Compiler(this.$el, this);
}
}
proxyVm(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
//相当于在$data上取值,进行转换操作,不需要深层代理
return data[key];
},
set(newVal) {
//设置代理方法
data[key] = newVal;
},
});
}
}
}
尝试将刚才写的代码引入到页面来使用,接下来就和Vue的基本使用一样了。
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vuetitle>
head>
<body>
<div id="app">
<input v-model="person.name"/>
<div>{{person.name}}div>
<div>{{person.age}}div>
div>
<script src="vue.js">script>
<script>
new Vue({
el: '#app',
data: {
info: {
name: 'goudan',
age: 20
},
},
})
script>
body>
html>
自己写一个简单版本的vue可以更好的理解它的原理, 提高自己的思维方式。我们用多了框架过后可能对原生的JS就有所遗忘,要知道框架都是原生的来构造的,所以阅读并理解源码对我们深入学习和技能提升有很大的帮助。