模拟Vue之数据驱动

一、前言

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.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值