一、前言
Vue有一核心就是数据驱动(Data Driven),允许我们采用简洁的模板语法来声明式的将数据渲染进DOM,且数据与DOM是绑定在一起的,这样当我们改变Vue实例的数据时,对应的DOM元素也就会改变了。
如下:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="test">
{{name}}
</div>
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<script>
var vm = new Vue({
el: '#test',
data: {
name: 'Monkey'
}
});
</script>
</body>
</html>复制代码
当我们在chrome控制台,更改vm.name时,页面中的数据也随之改变,但我们并没有与DOM直接接触,效果如下:
好了,今儿的核心就是模拟上述Demo中的数据驱动。
二、模拟Vue之数据驱动
通过粗浅地走读Vue的源码,发现达到这一效果的核心思路其实就是利用ES5的defineProperty方法,监听data数据,如果数据改变,那么就对页面做相关操作。
有了大体思路,那么我们就开始一步一步实现一个简易版的Vue数据驱动吧,简称SimpleVue。
Vue实例的创建过程,如下:
var vm = new Vue({
el: '#test',
data: {
name: 'Monkey'
}
});复制代码
因此,我们也依瓢画葫芦,构建SimpleVue构造函数如下:
function SimpleVue(obj){
this.$el = document.querySelector(obj.el);
this.$options = obj;
this._data = Object.create(null);
//入口
this.init();
obj = null;
};
SimpleVue.prototype = {
constructor: SimpleVue,
init: function(){
//TODO
}
};复制代码
接下来,我们在SimpleVue原型上编写一个watchData方法,通过利用ES5原生的defineProperty方法,监听data中的属性,如果属性值改变,那么我们就进行相关的页面处理。
如下:
SimpleVue.prototype = {
//监听data属性
watchData: function(){
var data = this.$options.data,//得到data对象
keys = Object.keys(data),//data对象上全部的自身属性,返回数组
that = this;
keys.forEach(function(elem){//监听每个属性
Object.defineProperty(that, elem, {
enumerable: true,
configurable: true,
get: function(){
return that._data[elem];
},
set: function(newVal){
that._data[elem] = newVal;
that.update();//数据变化,更新页面
}
});
that[elem] = data[elem];//初次进入改变that[elem],从而触发update方法
});
}
};复制代码
好了,如果我们检测到数据变化了呢?
那么,我们就更新视图嘛。
但是,怎么更新呢?
简单的实现方式就是,在初次构建SimpleVue实例时,就将页面中的模板保存下来,每次实例数据一改变,就通过正则替换掉原始的模板,即双括号中的变量,如下:
SimpleVue.prototype = {
//初始化SimpleVue实例时,就将原始模板保留
getTemplate: function(){
this.template = this.$el.innerHTML;
},
//数据改变更新视图
update: function(){
var that = this,
template = that.template,
reg = /(.*?)\{\{(\w*)\}\}/g,
result = '';
result = template.replace(reg, function(rs, $1, $2){
var val = that[$2] || '';
return $1 + val;
});
this.$el.innerHTML = result;
console.log('updated');
}
};复制代码
好了,整合上述js代码,完整的SimpleVue如下:
function SimpleVue(obj){
this.$el = document.querySelector(obj.el);
this.$options = obj;
this._data = Object.create(null);
//入口
this.init();
obj = null;
};
SimpleVue.prototype = {
constructor: SimpleVue,
init: function(){
this.getTemplate();
this.watchData();
},
//初始化SimpleVue实例时,就将原始模板保留
getTemplate: function(){
this.template = this.$el.innerHTML;
},
//监听data属性
watchData: function(){
var data = this.$options.data,//得到data对象
keys = Object.keys(data),//data对象上全部的自身属性,返回数组
that = this;
keys.forEach(function(elem){//监听每个属性
Object.defineProperty(that, elem, {
enumerable: true,
configurable: true,
get: function(){
return that._data[elem];
},
set: function(newVal){
that._data[elem] = newVal;
that.update();//数据变化,更新页面
}
});
that[elem] = data[elem];
});
},
//数据改变更新视图
update: function(){
var that = this,
template = that.template,
reg = /(.*?)\{\{(\w*)\}\}/g,
result = '';
result = template.replace(reg, function(rs, $1, $2){
var val = that[$2] || '';
return $1 + val;
});
this.$el.innerHTML = result;
console.log('updated');
}
};复制代码
测试代码如下:
<!DOCTYPE html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="test">
<div>{{name}}</div>
</div>
<script src="./SimpleVue.js"></script>
<script>
var vm = new SimpleVue({
el: '#test',
data: {
name: 'Monkey'
}
});
</script>
</body>
</html>复制代码
效果如下:
三、优化
上述实现效果,还不错哦。
但是,我们走读下上述代码,感觉还可以优化下。
(1)、在watchData方法中监听每个data属性时,如果我们设置相同值,页面也会更新的,因为set是监听赋值的,它又不知道是不是同一个值,因此,优化如下:
(2)、在上述基础,我们加入了新旧值判断,但是如果我们频繁更新data属性呢?那么也就会频繁调用update方法。例如,当我们给vm.name同时赋值两个值时,页面就会更新两次,如下:
怎么解决呢?
利用节流,即可:
SimpleVue.throttle = function(method, context, delay){
clearTimeout(method.tId);
method.tId = setTimeout(function(){
method.call(context);
}, delay);
};复制代码
好了,将优化点整合到原有代码中,得下:
代码太长,请自行打开
且,为了让我们使用更加方便,我们可以在上述代码基础上,加入一个created钩子(当然,你可以加入更多),完整代码见github。
好了,简单的数据驱动,我们算 实现了,也优化了,但,其实上述简易版Vue有很多问题,例如:
1)、监听的属性是个对象呢?且对象里又有其他属性,不就监听不成功了么?如下:
2)、通过上述1)介绍,如果监听的属性是个对象,那么又该如何渲染DOM呢?
3)、渲染DOM我们采用的是innerHTML,那么随着DOM的扩大,性能显而易见,又该如何解决?
等等问题,我们将在后续随笔通过精读源码,一步一步完善。
一、前言
在随笔“模拟Vue之数据驱动1”结尾处,我们说到如果监听的属性是个对象呢?那么这个对象中的其他属性岂不就是监听不了了吗?
如下:
倘若user中的name、age属性变化,如何知道它们变化了呢?
今儿,就来解决这一问题。
通过走读Vue源码,发现他是利用Observer构造函数为每个对象创建一个Observer对象,来监听数据的,如果数据中的属性又是一个对象,那么就又通过Observer来监听嘛。
其实,核心思想就是树的先序遍历(关于树,可参考here)。如我们将上述Demo中的data数据,图形化一下,就更加明白了,如下:
好了,理清了大体思路,下面我们就一起来创建一个Observer吧。
二、Observer构造
Observer整体结构如下:
function Observer(data){
//如若this不是Observer对象,即创建一个
if(!(this instanceof Observer)){
return new Observer(data);
}
this.data = data;
this.walk(data);
}
let p = Observer.prototype = Object.create(null);
p.walk = function(data){
/*
TODO:监听data数据中的所有属性,
并查看data中属性值是否为对象,
若为对象,就创建一个Observer实例
*/
}
p.convert = function(key, val){
//TODO:通过Object.defineProperty监听数据
}复制代码
好了,下面,我们一起来完成walk以及convert方法吧。
-walk-
首先,我们在walk方法中实现对data对象中的所有属性监听,如下:
p.walk = function(data){
let keys = Object.keys(data);
keys.forEach( key => {
let val = data[key];
this.convert(key, val);
});
}复制代码
且,由于属性中可能又会是一个对象,那么,我们就有必要监听它们。
怎么办呢?
如果是个对象,再次利用Observer构造函数,处理它不就完了么。
如下:
p.walk = function(data){
let keys = Object.keys(data);
keys.forEach( key => {
let val = data[key];
//如果val为对象,则交给Observer处理
if(typeof val === 'object'){
Observer(val);
}
this.convert(key, val);
});
}复制代码
你可能会有这样的疑问,如果直接利用Observer处理对象,那么不就与父对象失去关联了么?
然而并没有,因为JavaScript对于对象是指向地址关系,所以怎么会失去关联呢。
-convert-
对于convert方法,就比较简单了,一如既往就是利用Object.defineProperty监听数据,如下:
p.convert = function(key, val){
Object.defineProperty(this.data, key, {
get: ()=>{
console.log('访问了'+key+' 值为'+val);
return val;
},
set: (newVal)=>{
console.log('设置了'+key+' 值为'+newVal);
if(newVal !== val){
val = newVal;
}
}
});
}复制代码
好了,到此,一个简单的Observer就构造完成,下面我们就来测试下,是否成功监听了每个属性。
<script src="./observer.js"></script>
<script>
let data = {
user: {
name: 'Monkey',
age: 24
},
lover: {
name: 'Dorie',
age: 23
}
};
Observer(data);
</script>复制代码
效果如下:
Perfect,完整代码见github。
一、前言
在"模拟Vue之数据驱动2"中,我们实现了个Observer构造函数,通过它可以达到监听已有数据data中的所有属性。
但,倘若我们想在某个对象中,新增某个属性呢?
如下:
那么岂不是,新增的infor属性,以及它的对象属性,没有得到监听。
此时,应该怎么处理呢?
通过走读Vue源码,发现他是采用另增属性方法$set实现的。
就是说,如果我们采用常规方法为对象增加属性(如上),我们没法得知并监控它,所以,我们为每个对象扩展一个$set方法,用于另增属性使用,即可,如下:
data.user.$set('infor', {msg: 'happy'});复制代码
好了,下面,我们就一同实现这个$set方法吧。
二、$set方法实现
首先,我们得创建一个恒定extendObj对象,用于将$set方法绑定在其中。
你可能会想,为什么我们需要一个extendObj对象呢?直接将$set函数赋值给每个需要监听的对象不就完了么?
是的,这样也可以,但是随着需求增长,倘若我们又想为每个监听对象扩展其他方法呢?难道又要去Observer里面为对象,一一赋值?
so,创建恒定extendObj对象,如下:
const extendObj = {};复制代码
因为,我们将$set绑定到extendObj中,且让$set为不可枚举型,所以会用到Object.defineProperty,固将其提取出来,作为一个方法如下:
function proxyObject(obj, key, val, enume){
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enume,
writable: true,
configurable: true
});
};复制代码
接下来,就是实现$set方法了,整体结构如下:
proxyObject(extendObj, '$set', function(key, val){
//this指向extendObj
if(this.hasOwnProperty(key)){
return;
}else{
/*
TODO:在extendObj中监听key属性,
且,若key属性值为对象,再次监听key属性值
*/
}
});复制代码
看到上面的TODO注释,是否似曾相识,不就是是在“模拟Vue之数据驱动2”遇见过的嘛,通过Observer.prototype.convert监听key属性,通过new Observer再次监听key属性值不就完啦。
的确,但是一旦这样做了,不就和上面我们提到的“直接将$set赋予监听对象”问题一样嘛,耦合性太大,且随着需求上涨,不易维护。
固而,在此需要一点小技巧:在observer模块中为每个监听对象赋予一个$Observer属性,其值指向Observer自身实例,如下:
//observer.js
p.walk = function(data){
let keys = Object.keys(data);
keys.forEach( key => {
let val = data[key];
if(typeof val === 'object'){
new Observer(val);
}
this.convert(key, val);
});
//$Observer属性指向Observer自身实例
data.$Observer = this;
}
//新增一个observe方法
p.observe = function(data){
if(typeof data === 'object'){
new Observer(data);
}
}复制代码
好了,这样之后,得$set整体实现如下:
proxyObject(extendObj, '$set', function(key, val){
if(this.hasOwnProperty(key)){
return;
}else{
proxyObject(this, key, val, true);
let ob = this.$Observer;
ob.observe(val);
ob.convert(key, val);
}
});复制代码
到此,一个简单的$set方法构建完毕。
在上面我们提到,之所以需要一个恒定extendObj对象,是为了更好的代码管理。且,到目前为止,需要监听的对象上并没有扩展$set方法呢,所以,下面的事情就是为了达到以上效果,如下:
//observer.js
function Observer(data){
if(!(this instanceof Observer)){
return new Observer(data);
}
//将监听对象的隐指针指向我们的extendObj对象
data.__proto__ = extendObj;
this.data = data;
this.walk(data);
}复制代码
好了,一切完毕,接下来就测试下吧:
<script src="./extendObj.js"></script>
<script src="./observer.js"></script>
<script>
let data = {
user: {
name: 'Monkey',
age: 24
},
lover: {
name: 'Dorie',
age: 23
}
};
Observer(data);
</script>复制代码
效果如下:
一、前言
在"模拟Vue之数据驱动3"中,我们实现了为每个对象扩展一个$set方法,用于新增属性使用,这样就可以监听新增的属性了。
当然,数组也是对象,也可以通过$set方法实现新增属性。
但是,对于数组而言,通常我们是通过push之类的方法吧。
PS:Vue中明确指出push、pop、shift、unshift、splice、sort、reverse方法为变异方法,可以通过它们监听属性变化,触发视图更新(详情见here)
下面,我们就一起来实现这些Array的变异方法吧。
注:我们将Array变异方法实现,也写在extendObj.js中的,因为数组也是对象嘛。
二、Array变异方法实现
要实现这些变异方法,毫无疑问,我们会重写它们,那在Array.prototype里面重写吗?
当然不是,这样不就影响了所有数组对象的原型链了么!
为了避免这种情况,且,我们只是想在监听数据对象上继承这些变异数组方法,那么细心的你会发现,其实与我们在"模拟Vue之数据驱动3"中实现$set方法类似了。
首先,我们创建arrKeys对象用于保存需要变异的数组方法以及恒定对象extendArr,如下:
let arrKeys = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const extendArr = [];复制代码
接着,就是在extendArr对象上,一一监听arrKeys中的方法了,与$set方法类似,整体结构如下:
!function(){
arrKeys.forEach(function(key){
proxyObject(extendArr, key, function(){
//TODO
});
});
}();复制代码
注:proxyObject方法其实核心就是Object.defineProperty,详见"模拟Vue之数据驱动3".
接下来,就是实现核心部分代码了,重写这些方法的目的,是为了监听数据变化,所以要在方法原有功能不变的情况下,重写它们,Array.xxx.apply即可实现原有功能。
且,push、unshift、splice这三个方法可以在原数组中,新增属性,故而,我们需要监听新增属性以及它们的属性值,这里就和$set方法完全一样了,通过$Observer,即可利用observe以及convert方法实现了。
实现代码如下:
代码稍长,请自行打开
最后,就是在需要监听的对象上继承这些变异方法咯,如下:
//observer.js
function Observer(data){
if(!(this instanceof Observer)){
return new Observer(data);
}
data.__proto__ = extendObj;
//继承变异方法push、pop等等
if(Array.isArray(data)){
data.__proto__.__proto__ = extendArr;
}
this.data = data;
this.walk(data);
}复制代码
好了,一切完毕,接下来就测试下呗:
<script src="./extendObj.js"></script>
<script src="./observer.js"></script>
<script>
'use strict';
let data = {
msg: [5, 2, 0],
user: {
name: 'Monkey',
age: 24
},
lover: {
name: 'Dorie',
age: 23
}
};
Observer(data);
</script>复制代码
效果如下:
Perfect,此时,你可能会想,数组方法中仅有push、unshift、splice会为数组新增属性,那么我们又何必将其他方法,例如sort、reverse重写呢,也没发现有什么猫腻呢?
不错,在此时,并没有什么卵用,但是,你要知道sort、reverse等这些方法,可是会引起数组变化的,那么就会影响视图展现,这些变化,又怎么通知数组呢?就是下篇随笔会具体说明的。
一、前言
在"模拟Vue之数据驱动4"中,我们实现了push、pop等数组变异方法。
但是,在随笔末尾我们提到,当pop、sort这些方法触发后,该怎么办呢?因为其实,它们并没有往数组中新增属性呢。
而且,当数据改动后,如果我们在变动数据处,就立即更改数据也未免性能不够,此时,走读Vue源码,发现他用了一个很巧妙的方法,就是职责链模式。当某个数据有所变动时,它会向上传递,通俗点就是冒泡至根结点,这样我们也可以在自己代码中使用事件代理咯,哇卡哇卡。
示意图如下所示:
好了,说了这么多,我们下面就一起来实现下吧。
二、正文
注:以下代码皆编写在observer.js文件中。
首先,当数据变动,或者触发某个事件时,我们需要与变动数据关联一个自定义事件(自定义事件详情见here),如果触发某个事件,那么就执行,如下:
绑定事件方法:
//let p = Observer.prototype
p.on = function(eventName, fn){
let listener = this.listener = this.listener || [];
if(typeof eventName === 'string' && typeof fn === 'function'){
if(!listener[eventName]){
listener[eventName] = [fn];
}else{
listener[eventName].push(fn);
}
}
}复制代码
取消事件方法:
//let p = Observer.prototype
p.off = function(eventName, fn){
let listener = this.listener = this.listener || [];
let actionArray = listener[eventName];
if(typeof eventName === 'string' && Array.isArray(actionArray)){
if(typeof fn === 'function'){
actionArray.forEach( (func, i, arr) => {
if(func === fn){
arr.splice(i,1);
}
});
}
}
}复制代码
触发事件方法:
//let p = Observer.prototype
p.emit = function(eventName){
let listener = this.listener = this.listener || [];
let actionArray = listener[eventName];
if(Array.isArray(actionArray)){
actionArray.forEach( func => {
if(typeof func === 'function'){
func();
}
});
}
}复制代码
其次,就是当数据变动,触发自身相关事件后,怎么一路冒泡到根结点的处理了。
怎么冒泡到根结点呢?
那就自身结点关联父结点嘛,这样不就可以追溯到根节点了么。
所以,我们在Observer.walk时,就将自己的父节点记录即可,如下:
//let p = Observer.prototype
p.observe = function(key, data){
if(typeof data === 'object'){
let ob = new Observer(data);
//关联父节点
ob._parent = {
key,
ob: this
};
}
}复制代码
最后,有了子父结点的依赖关系,那么冒泡方法就OK啦,如下:
//let p = Observer.prototype
p.notify = function(eventName){
let ob = this._parent && this._parent.ob;
let key = ob && this._parent.key || 'root';
console.log('parent--'+key+' event--'+eventName);
this.emit(eventName);
//判断节点是否有父节点,若有,就向上传递事件
ob && ob.notify(eventName);
}复制代码
Perfect,具体代码详见github.