记一次MVVM的简单实现

2019-03-18  本文已影响0人  Jason_Shu

在正式实现前,我们先介绍一些铺垫知识。

Object.defineProperty

Object.defineProperty(obj, prop, descriptor)

说说几个重要属性。
(1)configurable
configurable如果在descriptor中不写,就默认为false,定义后不能再次修改该属性,也无法删除该属性。

let obj = {a: 1};

// 我们对obj对象增加一个b属性
obj.b = 2;

console.log(obj); // {a: 1, b: 2}

// 正常情况下我们可以删除obj的属性的
delete obj.b;

console.log(obj); // {a: 1}

// 但是如果我们用Object.defineProperty的configurable属性就不能删除/修改了
Object.defineProperty(obj, 'b', {
    value: 2,
    configurable: false
})

console.log(obj); //此时obj对象里已经有了b属性, {a: 1, b: 2}

// 但是我们不能修改或者删除属性b
obj.b = 3;

console.log(obj); // {a: 1, b: 2}

// 我们也不能删除属性b
delete obj.b; // false

console.log(obj); // {a: 1, b: 2}

(2)** enumerable**
enumerable设置为false后,便不能遍历该属性。

let obj = {a: 1};

Object.defineProperty(obj, 'b', {
    value: 2,
    enumerable: false
})

console.log(obj); // {a: 1, b:2};

// 但是遍历的时候只能遍历出属性a
for(let key in obj) {
    console.log(key, obj[key])
}

(3)** writable**
定义了writable为false后,该属性不能被改写(注:与上述的configurable有些类似,但是上述的configurable是既不能被修改,也不能被删除)

let obj = {a: 1};

Object.defineProperty(obj, 'b', {
    value: 2,
    writable: false
})

obj.b = 3;

console.log(obj); // {a: 1, b:2};

注:value和writable称为「数据描述符」

(4)get和set
get和set称为「存取描述符」。

let obj = {a: 1};
let b;

Object.defineProperty(obj, 'b', {
    get: function() {
        console.log('get b..');
        return b;
    },
    set(val) {
        console.log('set b..');
        b = val;
    }
})

obj.b = 2; // set b..

console.log(obj.b);
// get b..
// 2

get: 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。
set: 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

注:「存取描述符」和「数据描述符」不能同时使用

let obj = {a: 1};
let b;

Object.defineProperty(obj, 'b', {
    value: 3,
    get: function() {
        console.log('get b..');
        return b;
    },
    set(val) {
        console.log('set b..');
        b = val;
    }
})

上述代码会报错。

上述篇幅简述了「Object.defineProperty」的用法,现在我们用这个方法来做「数据劫持和监听」。

let obj = {
    name: 'Jason',
    friends: [1, 2, 3, 4]
};

observe(obj);

console.log(obj.name);
obj.name = 'Jack';
obj.friends[0] = 5;

function observe(obj) {
    if(JSON.stringify(obj) === '{}' || typeof obj !== 'object') return; // 如果是空对象或者不是对象的数据,则返回空。

    // 然后我们遍历对象obj,并把其中的属性都用Object.defineProperty重新定义一遍
    for(let key in obj) {
        let val = obj[key]; // 注意1:这里不能使用var
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function() {
                console.log(`get ${val}`);
                return val;
            },
            set: function(newVal) {
                console.log(`changes happen: ${val} => ${newVal}`);
                val = newVal
            }
        });

        if(typeof val === 'object') {
            observe(val);
        }
    }
}
image.png

上述的observe函数实现了一个数据监听,当监听了某个对象后,我们就可以在用户读取(get)或者设置(set)属性的时候做个拦截。

那为啥「注意1」那里不能用var呢?我们看看用var后输出啥?

let改用var后

我们可以看到get获得的值全部为4了。

如果使用var那就也要配合「立即执行函数」。

function observe(obj) {
    if(JSON.stringify(obj) === '{}' || typeof obj !== 'object') return; // 如果是空对象或者不是对象的数据,则返回空。

    // 然后我们遍历对象obj,并把其中的属性都用Object.defineProperty重新定义一遍
    for(let key in obj) {
        (function() {
            var val = obj[key]; // 注意:这里不能使用var
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get: function() {
                    console.log(`get ${val}`);
                    return val;
                },
                set: function(newVal) {
                    console.log(`changes happen: ${val} => ${newVal}`);
                    val = newVal
                }
            });

            if(typeof val === 'object') {
                observe(val);
            }
        })()
    }
}

观察者模式
通俗来说,一个典型的观察者模式应用场景,用户在一个网站订阅主题

  1. 多个用户(观察者, Observe)都可以订阅该网站的某个主题(Subject)。
  2. 当该主题内容更新的时候,订阅该主题的用户就都能收到消息。
function Subject() {
    this.observes = [];
}

Subject.prototype.addObserve = function(observe) {
    let index = this.observes.indexOf(observe);
    if(index === -1) {
        this.observes.push(observe);
    }
}

Subject.prototype.removeObserve = function(observe) {
    let index = this.observes.indexOf(observe);
    if(index > -1) {
        this.observes.slice(index, 1);
    }
}

Subject.prototype.notify = function() {
    this.observes.forEach((observe) => {
        observe.update();
    })
};

function Observe(name) {
    this.name = name;
    this.update = function() {
        console.log(`${this.name} update...`);
    }
}

// 创建主题
var subject = new Subject();

// 创建观察者1
var observe1 = new Observe('Jason');
// 创建观察者2
var observe2 = new Observe('Jack');

// 主题中添加观察者1
subject.addObserve(observe1);
// 主题中添加观察者2
subject.addObserve(observe2);

// 主题通知所有观察者更新
subject.notify();
image.png

Subject是构造函数,new Subject() 创建一个主题实例,该对象的内部维护订阅当前主题的观察者数组。主题对象上有一些方法,如添加观察者(addObserve),删除观察者(removeObserve),通知观察者更新)(notify),当notify后,订阅了该主题的所有观察者都会调用自身的update方法。
Observer 是构造函数,new Observer() 创建一个观察者对象,该对象有一个 update 方法。

我们换用ES6重写一遍。

class Subject {
    constructor() {
        this.observes = [];
    }

    addObserve(observe) {
        let index = this.observes.indexOf(observe);
        if(index === -1) {
            this.observes.push(observe);
        }
    }

    removeObserve(observe) {
        let index = this.observes.indexOf(observe);
        if(index > -1) {
            this.observes.slice(index, 1);
        }
    }

    notify() {
        this.observes.forEach((observe) => {
            observe.update();
        })
    }
}

class Observe {
    constructor(name) {
        this.name = name;
    }
    update() {
        console.log(`${this.name} update...`);
    }
}


// 创建主题
var subject = new Subject();

// 创建观察者1
var observe1 = new Observe('Jason');
// 创建观察者2
var observe2 = new Observe('Jack');

// 主题中添加观察者1
subject.addObserve(observe1);
// 主题中添加观察者2
subject.addObserve(observe2);

// 主题通知所有观察者更新
subject.notify();

上面代码中,主题被观察者订阅的写法是「subject.addObserve(observe)」,不是很直观,我们给观察者添加订阅方法。

class Observe {
    constructor(name) {
        this.name = name;
    }
    update() {
        console.log(`${this.name} update...`);
    }
    subscribeTo(subject) {
        subject.addObserve(this);
    }
}


// 创建主题
var subject = new Subject();

// 创建观察者1
var observe1 = new Observe('Jason');
// 创建观察者2
var observe2 = new Observe('Jack');

// 观察者1订阅主题subject
observe1.subscribeTo(subject);
// 观察者2订阅主题subject
observe2.subscribeTo(subject);

MVVM实现单向绑定

首先说书MVVM(Model-View-ViewModel),是一种用于把数据和UI分离的模式。

MVVM中的Model表示应用程序的数据,比如说一个账户的信息(姓名,年龄,电子邮件等等)。Model保存信息,但通常不处理信息,不会对信息进行再次加工,数据的格式化是由View处理的。行为一般是业务逻辑,封装到ViewModel中。

View是与用户交流的桥梁。

ViewModel充当数据转换器,将Model信息转换为View的信息,将命令从View传到Model层。

假如我们又如下代码,data里面的「name」会和视图中的「{{name}}」一一映射,修改data里面的「name」可以引起视图里面的值的变化。

<body>
  <div id="app" >{{name}}</div>

  <script>
    function mvvm(){
        //todo...
    }
    var vm = new mvvm({
      el: '#app',
      data: { 
          name: 'jirengu' 
      }
    })
  </script>
<body>

如何实现上述MVVM呢?我们回顾下上文中讲述的观察者模式。
(1)主题(subject)是什么?
(2)观察者(observe)是什么?
(3)观察者何时订阅?
(4)主题何时通知更新?

上面的例子中,主题(subject)是data中的「name」属性,观察者是视图中的「{{name}}」,当一开始执行MVVM初始化(根据el发现「{{name}}」的时候订阅主题),当data中的「name」改变的时候通知观察者更新。

function observe(data) {
    if(JSON.stringify(data) === '{}' || typeof data !== 'object') return;
    for(let key in data) {
        let val = data[key];
        let subject = new Subject(); // 每一个key都是一个主题
        Object.defineProperty(data, key, {
            get: function() {
                console.log(`get ${key}`)
                if(currentObserver) {
                    console.log('hasCurrentObserver');
                    currentObserver.subscribeTo(subject);
                }
                return val;
            },

            set: function(newVal) {
                val = newVal;
                console.log('start notify....');
                subject.notify();
            }
        })
        if(typeof val === 'object') {
            this.observe(val);
        }
    }
}

let id = 0;
let currentObserver = null;

class Subject {
    constructor() {
        this.id = id++;
        this.observers = [];
    }

    addObserver(observer) {
        let index = this.observers.indexOf(observer);
        if(index === -1) {
            this.observers.push(observer);
        }
    }

    removeObserver(observer) {
        let index = this.observers.indexOf(observer);
        if(index > -1) {
            this.observers.slice(index, 1);
        }
    }

    notify() {
        this.observers.forEach((observer) => {
            observer.update();
        })
    }

}

class Observer {
    constructor(vm, key, cb) {
        this.subjects = {}; // 存放该observer订阅的主题
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        this.value = this.getValue();
    }

    update() {
        let oldVal = this.value;
        let newVal = this.getValue();
        if(oldVal !== newVal) {
            // 值有更改,要更新
            this.value = newVal;
            this.cb.bind(this.vm)(newVal, oldVal);
        }
    }

    subscribeTo(subject) {
        if(! this.subjects[subject.id]) {
            // 如果我们还未订阅该主题
            console.log('SubscribeTo ...', subject);
            subject.addObserver(this);
            this.subjects[subject.id] = subject;
        }
    }

    getValue() {
        currentObserver = this;
        let value = this.vm.$data[this.key];
        currentObserver = null;
        return value;
    }
}


class MVVM {
    constructor(opts) {
        this.init(opts);
        observe(this.$data);
        // 编译
        this.compile();
    }

    init(opts) {
        this.$el = document.querySelector(opts.el);
        this.$data = opts.data;
        this.observes = [];
    }

    compile() {
        // 转换视图层的节点
        this.traverse(this.$el);
    }

    traverse(node) {
        if(node.nodeType === 1) { // 如果为节点,就递归转换
            node.childNodes.forEach((childNode) => {
                this.traverse(childNode);
            })
        } else if(node.nodeType === 3){ //如果是文本
            this.renderText(node); // 就渲染文字
        }
    }

    renderText(node) {
        let reg = /{{(.+?)}}/g;
        let match;
        while((match = reg.exec(node.nodeValue))) {
            let raw = match[0];
            let key = match[1].trim();
            node.nodeValue = node.nodeValue.replace(raw, this.$data[key]);
            // 针对每一个key,创建一个观察者
            new Observer(this, key, function(val, oldVal) {
                node.nodeValue = node.nodeValue.replace(oldVal, val);
            })
        }
    }

}

let vm = new MVVM({
    el: '#app',
    data: {
        name: 'Jason',
        age: 23
    }
})
image.png

当我们在控制台修改「age」为20。视图页面也跟着改变。


image.png

MVVM实现双向绑定

function observe(data) {
    if(JSON.stringify(data) === '{}' || typeof data !== 'object') return;
    for(let key in data) {
        let val = data[key];
        let subject = new Subject(); // 每一个key都是一个主题
        Object.defineProperty(data, key, {
            get: function() {
                console.log(`get ${key}`)
                if(currentObserver) {
                    console.log('hasCurrentObserver');
                    currentObserver.subscribeTo(subject);
                }
                return val;
            },

            set: function(newVal) {
                val = newVal;
                console.log('start notify....');
                subject.notify();
            }
        })
        if(typeof val === 'object') {
            this.observe(val);
        }
    }
}

let id = 0;
let currentObserver = null;

class Subject {
    constructor() {
        this.id = id++;
        this.observers = [];
    }

    addObserver(observer) {
        let index = this.observers.indexOf(observer);
        if(index === -1) {
            this.observers.push(observer);
        }
    }

    removeObserver(observer) {
        let index = this.observers.indexOf(observer);
        if(index > -1) {
            this.observers.slice(index, 1);
        }
    }

    notify() {
        this.observers.forEach((observer) => {
            observer.update();
        })
    }

}

class Observer {
    constructor(vm, key, cb) {
        this.subjects = {}; // 存放该observer订阅的主题
        this.vm = vm;
        this.key = key;
        this.cb = cb;
        this.value = this.getValue();
    }

    update() {
        let oldVal = this.value;
        let newVal = this.getValue();
        if(oldVal !== newVal) {
            // 值有更改,要更新
            this.value = newVal;
            this.cb.bind(this.vm)(newVal, oldVal);
        }
    }

    subscribeTo(subject) {
        if(! this.subjects[subject.id]) {
            // 如果我们还未订阅该主题
            console.log('SubscribeTo ...', subject);
            subject.addObserver(this);
            this.subjects[subject.id] = subject;
        }
    }

    getValue() {
        currentObserver = this;
        let value = this.vm.$data[this.key];
        currentObserver = null;
        return value;
    }
}


class MVVM {
    constructor(opts) {
        this.init(opts);
        observe(this.$data);
        // 编译
        new Compile(this)
    }

    init(opts) {
        this.$el = document.querySelector(opts.el);
        this.$data = opts.data;
        this.$methods = opts.methods;

        // 把$data中的数据直接代理到当前vm对象
        for(let key in this.$data) {
            Object.defineProperty(this, key, {
                enumerable: true,
                configurable: true,
                get: () => {
                    return this.$data[key]
                },

                set: (newVal) => {
                    this.$data[key] = newVal;
                }

            })
        }

        // 把「this.$methods」里面的函数中的this,都指向this,也就是vm
        for(let key in this.$methods) {
            this.$methods[key] = this.$methods[key].bind(this);
        }
    }


}

class Compile {
    constructor(vm) {
        this.vm = vm;
        this.node = vm.$el;
        this.compile();
    }
    compile(){
        this.traverse(this.node)
    }
    traverse(node){
        if(node.nodeType === 1){
            this.compileNode(node)   //解析节点上的v-bind 属性
            node.childNodes.forEach(childNode=>{
                this.traverse(childNode)
            })
        }else if(node.nodeType === 3){ //处理文本
            this.compileText(node)
        }
    }
    compileText(node){
        let reg = /{{(.+?)}}/g
        let match
        console.log(node)
        while(match = reg.exec(node.nodeValue)){
            let raw = match[0]
            let key = match[1].trim()
            node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key])
            new Observer(this.vm, key, function(val, oldVal){
                node.nodeValue = node.nodeValue.replace(oldVal, val)
            })
        }
    }

    //处理指令
    compileNode(node){
        let attrs = [...node.attributes] //类数组对象转换成数组,也可用其他方法
        attrs.forEach(attr=>{
            //attr 是个对象,attr.name 是属性的名字如 v-model, attr.value 是对应的值,如 name
            if(this.isModelDirective(attr.name)){
                this.bindModelHander(node, attr);
            } else if(this.isEventDirective(attr.name)) {
                this.bindEventHander(node, attr);
            }
        })
    }

    bindModelHander(node, attr) {
        let key = attr.value       //attr.value === 'name'
        node.value = this.vm.$data[key]
        new Observer(this.vm, key, function(newVal){
            node.value = newVal
        })
        node.oninput = (e)=>{
            this.vm.$data[key] = e.target.value  //因为是箭头函数,所以这里的 this 是 compile 对象
        }
    }

    bindEventHander(node, attr) {
        console.log(attr)
        let eventType = attr.name.substr(5);
        let methodName = attr.value;
        node.addEventListener(eventType, this.vm.$methods[methodName]);
    }

    //判断属性名是否是指令
    isModelDirective(attrName){
        return attrName === 'v-model'
    }

    isEventDirective(attrName){
        return attrName.indexOf('v-on') === 0
    }
}


let vm = new MVVM({
    el: '#app',
    data: {
        name: 'Jason',
        age: 23
    },
    methods: {
        sayHi(){
            alert(`hi ${this.name}` )
        }
    }
})

注意「编译」步骤,同时在MVVM中我们MVVM类中转移了「this.data」和「this.methods」的this都指向vm。

上一篇下一篇

猜你喜欢

热点阅读