typeof data !== "object") {
return;
}
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
});
}
function Dep() {
this.subs = [];
}
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
}
Dep.prototype.notify = function () {
console.log('属性变化通知 Watcher 执行更新视图函数');
this.subs.forEach(sub => {
sub.update();
})
}
Dep.target = null;
以上我们就创建了一个监听器 Observer,我们现在可以尝试一下给一个对象添加监听然后改变属性会有何变化。
var modeng = {
age: 18
}
observer(modeng);
modeng.age = 20;我们可以看到浏览器控制台打印出 “属性变化通知 Watcher 执行更新视图函数” 说明我们实现的监听器没毛病,既然监听器有了,我们就可以通知属性变化了,那肯定是需要 Watcher 的时候了。
订阅者 Watcher
Watcher 主要是接受属性变化的通知,然后去执行更新函数去更新视图,所以我们做的主要是有两步:
把 Watcher 添加到 Dep 容器中,这里我们用到了 监听器的 get 函数
接收到通知,执行更新函数。
function Watcher(vm, prop, callback) {
this.vm = vm;
this.prop = prop;
this.callback = callback;
this.value = this.get();
}
Watcher.prototype = {
update: function () {
const value = this.vm.$data[this.prop];
const oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.callback(value);
}
},
get: function () {
Dep.target = this; //储存订阅器
const value = this.vm.$data[this.prop]; //因为属性被监听,这一步会执行监听器里的 get方法
Dep.target = null;
return value;
}
}这一步我们把 Watcher 也给弄了出来,到这一步我们已经实现了一个简单的双向绑定了,我们可以尝试把两者结合起来看下效果。
function Mvue(options, prop) {
this.$options = options;
this.$data = options.data;
this.$prop = prop;
this.$el = document.querySelector(options.el);
this.init();
}
Mvue.prototype.init = function () {
observer(this.$data);
this.$el.textContent = this.$data[this.$prop];
new Watcher(this, this.$prop, value => {
this.$el.textContent = value;
});
}这里我们尝试利用一个实例来把数据与需要监听的属性传递进来,通过监听器监听数据,然后添加属性订阅,绑定更新函数。
<p id="app">{{name}}</p>
const vm = new Mvue({
el: "#app",
data: {
name: "我是摩登"
}
}, "name");我们可以看到数据已经正常的显示在页面上,那么我们在通过控制台去修改数据,发生变化后视图也会跟着修改。

到这一步我们我们基本上已经实现了一个简单的双向绑定,但是不难发现我们这里的属性都是写死的,也没有指令模板的解析,所以下一步我们来实现一个模板解析器。
Compile 解析器
Compile 的主要作用一个是用来解析指令初始化模板,一个是用来添加添加订阅者,绑定更新函数。
因为在解析 DOM 节点的过程中我们会频繁的操作 DOM, 所以我们利用文档片段(DocumentFragment)来帮助我们去解析 DOM 优化性能。
function Compile(vm) {
this.vm = vm;
this.el = vm.$el;
this.fragment = null;
this.init();
}
Compile.prototype = {
init: function () {
this.fragment = this.nodeFragment(this.el);
},
nodeFragment: function (el) {
const fragment = document.createDocumentFragment();
let child = el.firstChild;
//将子节点,全部移动文档片段里
while (child) {
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
}
}然后我们就需要对整个节点和指令进行处理编译,根据不同的节点去调用不同的渲染函数,绑定更新函数,编译完成之后,再把 DOM 片段添加到页面中。
Compile.prototype = {
compileNode: function (fragment) {
let childNodes = fragment.childNodes;
[...childNodes].forEach(node => {
let reg = /\{\{(.*)\}\}/;
let text = node.textContent;
if (this.isElementNode(node)) {
this.compile(node); //渲染指令模板
} else if (this.isTextNode(node) && reg.test(text)) {
let prop = RegExp.$1;
this.compileText(node, prop); //渲染{{}} 模板
}
//递归编译子节点
if (node.childNodes && node.childNodes.length) {
this.compileNode(node);
}
});
},
compile: function (node) {
let nodeAttrs = node.attributes;
[...nodeAttrs].forEach(attr => {
let name = attr.name;
if (this.isDirective(name)) {
let value = attr.value;
if (name === "v-model") {
this.compileModel(node, value);
}
node.removeAttribute(name);
}
});
},
//省略。。。
}因为代码比较长如果全部贴出来会影响阅读,我们主要是讲整个过程实现的思路,文章结束我会把源码发出来,有兴趣的可以去查看全部代码。
到这里我们的整个的模板编译也已经完成,不过这里我们并没有实现过多的指令,我们只是简单的实现了 v-model 指令,本意是通过这篇文章让大家熟悉与认识 Vue 的双向绑定原理,并不是去创造一个新的 MVVM 实例。所以并没有考虑很多细节与设计。
现在我们实现了 Observer、Watcher、Compile,接下来就是把三者给组织起来,成为一个完整的 MVVM。
创建 Mvue
这里我们创建一个 Mvue 的类(构造函数)用来承载 Observer、Watcher、Compile 三者。
function Mvue(options) {
this.$options = options;
this.$data = options.data;
this.$el = document.querySelector(options.el);
this.init();
}
Mvue.prototype.init = function () {
observer(this.$data);
new Compile(this);
}然后我们就去测试一下结果,看看我们实现的 Mvue 是不是真的可以运行。
<p id="app">
<h1>{{name}}</h1>
</p>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/index.js"></script>
<script>
const vm = new Mvue({
el: "#app",
data: {
name: "完全没问题,看起来是不是很酷!"
}
});
</script>
我们尝试去修改数据,也完全没问题,但是有个问题就是我们修改数据时时通过 vm.$data.name 去修改数据,而不是想 Vue 中直接用 vm.name 就可以去修改,那这个是怎么做到的呢?其实很简单,Vue 做了一步数据代理操作。

数据代理
我们来改造下 Mvue 添加数据代理功能,我们也是利用 Object.defineProperty 方法进行一步中间的转换操作,间接的去访问。
function Mvue(options) {
this.$options = options;
this.$data = options.data;
this.$el = document.querySelector(options.el);
//数据代理
Object.keys(this.$data).forEach(key => {
this.proxyData(key);
});
this.init();
}
Mvue.prototype.init = function () {
observer(this.$data);
new Compile(this);
}
Mvue.prototype.proxyData = function (key) {
Object.defineProperty(this, key, {
get: function () {
return this.$data[key]
},
set: function (value) {
this.$data[key] = value;
}
});
}
到这里我们就可以像 Vue 一样去修改我们的属性了,非常完美。完全自己动手实现,你也来试试把,体验下自己动手写代码的乐趣。
总结
本文主要是对 Vue 双向绑定原理的学习与实现。
主要是对整个思路的学习,并没有考虑到太多的实现与设计的细节,所以还存在很多问题,并不完美。
源码地址,整个过程的全部代码,希望对你有所帮助。
以上就是带你详细实现vue双向绑定的详细内容,更多请关注php中文网其它相关文章!
网站建设是一个广义的术语,涵盖了许多不同的技能和学科中所使用的生产和维护的网站。
关键词:带你详细完成vue双向绑定