vue 数据双向绑定实现
之前每件事都差不多,直到现在才发现差很多。现在才发现理清一件事的原委是多么快乐的一件事,我们共同勉励。
纸上得来终觉浅,绝知此事要躬行
懒得扯淡,直接正题
PS: 文章略长。
本文分3个部分来介绍:
-
model
->view
- 编译器
-
view
->model
model -> view
其基于 订阅者-发布者模式,简单的讲就是订阅者订阅数据,一旦订阅的数据变更过后,更新绑定的view视图。
这里有明确的分工,分别是监听器、发布器和订阅器,这3者相互协作,各司其职。
- 监听器:负责 创建 发布器;发布器 添加 订阅器;发布器 通知 订阅器
- 发布器:负责 添加 订阅器;通知 订阅器
- 订阅器:负责 更新视图
目标效果
过2s数据更改,更新到视图
监听器
顾名思义,监听器,监听器,监听的就是数据的变化
创建 发布器;发布器 添加 订阅器;发布器 通知 订阅器
需要解决订阅者的添加和发布器通知订阅器的时机
Object.difineProperty为我们提供了方便,其语法如下:
var obj = {};
Object.defineProperty(obj, 'a', {
enumerable: true,
configurable: true,
value: 'a'
});
console.log(obj.a); // 输出a
definePropery
除了可以定义数值以外,还可以定义 get 和 set 访问器,如下:
var obj = {};
var value = 'a';
Object.defineProperty(obj, 'a', {
enumerable: true,
configurable: true,
get: function () {
console.log('获取 key 为 "a" 的值');
return value;
},
set: function (val) {
console.log('修改 key 为 "a" 的值');
value = val;
}
});
console.log(obj.a);
obj.a = 'b';
console.log(obj.a);
运行结果如下所示:
数据的变化无非就是读和写,由此,我们可以得出 订阅器的添加和发布器通知订阅器
的时机,就是属性值的获取和重置。
具体代码
function observe(data) {
if (!data || typeof data !== 'object') {
return ;
}
Object.keys(data).forEach((val, key) => {
defineReactive(data, val, data[val]);
})
}
function defineReactive(data, key, val) {
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(`获取参数${key},值为${val}`);
return val;
},
set: function (newValue) {
console.log(`修改参数${key},变为${val}`);
val = newValue;
}
});
}
var obj = {
type: 'object',
data: {
a: 'a'
}
}
observe(obj);
发布器
添加 订阅器;通知 订阅器
function Dep () {
this.subs = [];
}
Dep.prototype.addSub = function(sub){
this.subs.push(sub);
};
Dep.prototype.notify = function(){
this.subs.forEach(function(sub, index) {
sub.update();
});
};
发布器代码写好了,我们再重新修改一下监听器代码。主要修改点为:添加订阅器和发布器通知订阅器
function defineReactive(data, key, val) {
+ var dep = new Dep();
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log(`获取参数${key},值为${val}`);
+ dep.addSub(<watch>);
return val;
},
set: function (newValue) {
+ dep.notify();
console.log(`修改参数${key},变为${val}`);
val = newValue;
}
订阅器
更新视图
function Watcher (vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
this.value = this.get();
}
Watcher.prototype.update = function(){
var oldValue = this.value;
var value = this.vm.data[this.key];
if (oldValue !== value) {
this.value = value;
this.cb.call(this, this.vm.data[this.key]);
}
};
Watcher.prototype.get = function(){
Dep.target = this;
var value = this.vm.data[this.key];
Dep.target = null;
return value;
};
订阅器代码写好了,我们再重新修改一下监听器代码。主要修改点为:如何添加订阅器
enumerable: true,
configurable: true,
get: function () {
- dep.addSub(<watch>);
+ if (Dep.target) {
+ dep.addSub(Dep.target);
+ }
return val;
},
set: function (newValue) {
vue 实现
function Vue (data, dom, key) {
this.data = data;
observe(data);
dom.innerHTML = data[key];
var watcher = new Watcher(this, key, function (name) {
dom.innerHTML = name;
});
}
实际使用
<div id="root"></div>
var vm = new Vue({
name: 'mumu'
}, document.getElementById('root'), 'name');
setTimeout(() => {
vm.data.name = 'yiyi';
}, 2000);
还有一点,通常数据的变更是直接使用vm.name,而非vm.data.name,其实也很简单,直接使用代理,vm.name读取和写都代理到vm.data.name上即可。
+ var self = this;
+ Object.keys(this.data).forEach(function(property, index) {
+ self.proxyProperty(property);
+ });
+
var watcher = new Watcher(this, key, function (name) {
dom.innerHTML = name;
});
-}
+}
+
+Vue.prototype.proxyProperty = function(property){
+ Object.defineProperty(this, property, {
+ configurable: true,
+ get: function () {
+ return this.data[property];
+ },
+ set: function (value) {
+ this.data[property] = value;
+ }
+ });
+};
详细代码参考github项目
$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout model2view
编译器
上面的实例看起来,有点问题,我们是写死监听的数据,然后修改dom上的innerHTML,实际中,肯定不会这样,需要在dom中绑定数据,然后动态监听数据变化。
首先需要明确编译器
有哪些工作需要做
- 解析 dom 结构,对 text 节点中绑定数据做一次模板字符串到数据的替换
- 数据绑定时,添加对应的
订阅器
目标效果
视图绑定数据,2s后数据更新,更新到视图
实现
对dom结构的解析这里使用 文档片段
,其dom操作性能优于其他。
function Compile (options, vm) {
this.compile = this;
this.vm = vm;
this.domEle = document.getElementById(options.el);
this.fragment = this.createElement(this.domEle);
this.compileElement(this.fragment);
this.viewRefresh();
}
Compile.prototype.createElement = function (ele) {
var fragment = document.createDocumentFragment();
var child = ele.firstChild;
while (child) {
fragment.appendChild(child);
child = ele.firstChild;
}
return fragment;
}
Compile.prototype.compileElement = function (el) {
var childNodes = el.childNodes;
[].slice.apply(childNodes).forEach((node) => {
var reg = /\{\{(\w+)\}\}/;
var text = node.textContent;
if (reg.test(text)) {
this.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length > 0) {
this.compileElement(node);
}
});
}
Compile.prototype.compileText = function (node, key) {
var text = this.vm[key];
var self = this;
self.updateText(node, text);
new Watcher(this.vm, key, function (newText) {
self.updateText(node, newText);
});
}
Compile.prototype.updateText = function (node, text) {
node.textContent = text;
}
Compile.prototype.viewRefresh = function(){
this.domEle.appendChild(this.fragment);
};
详细代码参考github项目
$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout compile
view -> model
这个比较简单,在compile 解析时,判断是否是元素节点,如果元素节点中包含指令v-model
,从中读取监听的数据属性,再从 model中读取,除此以外还要绑定一个input事件,用于view -> model
目标效果
详细代码参考github项目
$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout view2model
事件
目标效果
详细代码参考github项目
$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout event
生命周期
比如说创建、初始化、更新、销毁等。
详细代码参考github项目
$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout lifecircle
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。