美文网首页
记一次MVVM的简单实现

记一次MVVM的简单实现

作者: Jason_Shu | 来源:发表于2019-03-18 00:34 被阅读0次

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

    Object.defineProperty

    Object.defineProperty(obj, prop, descriptor)

    • 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。

    相关文章

      网友评论

          本文标题:记一次MVVM的简单实现

          本文链接:https://www.haomeiwen.com/subject/aovumqtx.html