vue--实现MVVM原理

423 阅读3分钟

1. 说明

  1. compile 编译,即模板解析器,能够对模板中的指令和插值表达式进行解析
  2. observer 数据劫持,即 数据监听器,能够对数据对象(data)的所有属性进行监听
  3. watcer 监听者,将compile的解析结果,与observer所观察的对象连接起来,建立关系,在observer观察到数据对象变化时,接收通知,并更新DOM

gitbub

2. 实现MVVM原理

2.1 目录结构

2.2 index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="message.a">
        <div>{{b}}</div>
        <ul>
            <li>{{message.a}}</li>
        </ul>
        {{b}}
    </div>
    <!-- <script src="https://cdn.jsdelivr.net/npm/vue"></script> -->
    <script src="./watcher.js"></script>
    <script src="./observer.js"></script>
    <script src="./compile.js"></script>
    <script src="./mvvm.js"></script>
    <script>
        let vm = new MVVM({
            el: '#app',
            data: {
                message: {
                    a: 'aa'
                },
                b: 'bb'
            }
        })
    </script>
</body>

</html>

3. mvvm

  1. 整合编译和数据劫持
  2. 代理,使vm.$data.message => vm.message

3.1 完整mvvm.js

class MVVM {
    constructor(options) {
            // 实例上的dom元素,<div id="app"></div>
            this.$el = options.el;
            // 实例上的所有数据,data
            this.$data = options.data;
            // 如果有这个dom元素,才开始
            if (this.$el) {
                // 数据劫持,就是对数据的所有属性,改成set和get的方法,以至可以在数据获取前和改变后,触发其它方法(做点事情)
                new Observer(this.$data);
                this.proxyData(this.$data)
                    // 编译元素,例如<input type="text" v-model="message.a">,根据message.a,找到data中对应的message.a的数据,赋值给input的value
                new Compile(this.$el, this)
            }
        }
        // proxy代理: vm.$data.message => vm.message
    proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                get() {
                    return data[key]
                },
                set(newValue) {
                    data[key] = newValue;
                }
            })
        })
    }
}

4. compile

4.1 nodeType

nodeType 属性返回节点类型。

  1. 如果节点是一个元素节点,nodeType 属性返回 1。

  2. 如果节点是属性节点, nodeType 属性返回 2。

  3. 如果节点是一个文本节点,nodeType 属性返回 3。

4.2 createDocumentFragment()

  1. DocumentFragments 是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有的子元素所代替。
  2. 因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此,使用文档片段通常会带来更好的性能。

4.3 reduce

4.3.1 说明

接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值

4.3.2 有一个字符串message.a.b,有一个对象{"message":{"a":{"b":"我是bb"}}},想要找到字条串中的b,在对象中key为b,对应的value

        //对象
        var dataObj = {
            message: {
                a: {
                    b: '我是bb'
                }
            },
        };
        //字符串
        var dataStr = 'message.a.b';
        // 字符串转成数组
        var dataArray = dataStr.split('.')
        console.log(dataArray)
            //找到字条串中的b,在对象中key为b,对应的value
        var result = dataArray.reduce((prev, next) => {
            return prev[next]
        }, dataObj)
        console.log(result)

4.4 /\{\{([^}]+)\}\}/g;

将{{a}} => a

        let expr = "{{message.a.b}}"; // 取文本中的内容
        let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}} 
        var result = expr.replace(reg, 'a');
        console.log(result)

4.5 ...运算符

        function sub(...arg) {
            let sum = 0;
            arg.forEach(item => {
                sum += item;
            })
            return sum;
        }
        var a = sub(1, 2, 3)
        var b = sub(1, 2, 3, 4)
        console.log(a)   //6
        console.log(b)   //10

4.6 setVal()

监听input输入框的值,根据<input type="text" v-model="message.a">(message.a),然后把值赋给vm.data里对应的键(vm.data.message.a = 值),再更新视图上的显示modelUpdater

    model(node, vm, expr) { 
        let updateFn = this.updater['modelUpdater'];
        node.addEventListener('input', (e) => {
            let newValue = e.target.value;
            this.setVal(vm, expr, newValue)
        })
        updateFn && updateFn(node, this.getVal(vm, expr));
    },
    setVal(vm, expr, value) { // [message,a]
        expr = expr.split('.');
        return expr.reduce((prev, next, currentIndex) => {
            if (currentIndex === expr.length - 1) {
                return prev[next] = value;
            }
            return prev[next];
        }, vm.$data);
    },
    updater: {
        modelUpdater(node, value) {
            node.value = value;
        }
    }

4.6 完整compile.js代码

class Compile {
    constructor(el, vm) {
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        if (this.el) {
            let fragment = this.node2fragment(this.el);
            this.compile(fragment);
            this.el.appendChild(fragment)
        }
    }
    isElementNode(node) {
        return node.nodeType === 1;
    }
    isDirective(name) {
        return name.includes('v-')
    }
    compileElement(node) {
        let attrs = node.attributes;
        Array.from(attrs).forEach(attr => {
            let attrName = attr.name;
            if (this.isDirective(attrName)) {
                let expr = attr.value;
                let [, type] = attrName.split('-');
                CompileUtil[type](node, this.vm, expr)
            }
        })
    }
    compileText(node) {
        let expr = node.textContent;
        let reg = /\{\{([^}]+)\}\}/g;
        if (reg.test(expr)) {
            CompileUtil['text'](node, this.vm, expr)
        }
    }
    compile(fragment) {
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(node => {
            if (this.isElementNode(node)) {
                this.compileElement(node)
                this.compile(node)
            } else {
                this.compileText(node)
            }
        })
    }
    node2fragment(el) {
        let fragment = document.createDocumentFragment();
        let firstChild;
        while (firstChild = el.firstChild) {
            fragment.appendChild(firstChild);
        }
        return fragment;
    }
}

CompileUtil = {
    getVal(vm, expr) {
        expr = expr.split('.');
        return expr.reduce((prev, next) => {
            return prev[next]
        }, vm.$data)
    },
    getTextVal(vm, expr) {
        return expr.replace(/\{\{([^}]+)\}\}/g, (...argument) => {
            return this.getVal(vm, argument[1])
        })
    },
    setVal(vm, expr, value) {
        expr = expr.split('.');
        return expr.reduce((prev, next, currentIndex) => {
            if (currentIndex === expr.length - 1) {
                return prev[next] = value
            }
            return prev[next]
        }, vm.$data)
    },
    model(node, vm, expr) {
        let updateFn = this.updater['modelUpdater'];
        let value = this.getVal(vm, expr);
        new Watcher(vm, expr, (newValue) => {
            updateFn && updateFn(node, this.getVal(vm, expr))
        })
        node.addEventListener('input', (e) => {
            let newValue = e.target.value;
            this.setVal(vm, expr, newValue)
        })
        updateFn && updateFn(node, value)
    },
    text(node, vm, expr) {
        let updateFn = this.updater['textUpdater'];
        let value = this.getTextVal(vm, expr);
        expr.replace(/\{\{([^}]+)\}\}/g, (...argument) => {
            new Watcher(vm, argument[1], (newValue) => {
                updateFn && updateFn(node, this.getTextVal(vm, expr))
            })
        })
        updateFn && updateFn(node, value)
    },
    updater: {
        modelUpdater(node, value) {
            node.value = value;
        },
        textUpdater(node, value) {
            node.textContent = value;
        }
    }
}

5. observer

5.1 this.subs=[]

<div id="app"></div>下面的节点为准,{{b}}算1个,message.a算2个

3个watcher

    <div id="app">
        <input type="text" v-model="message.a">
        <div>{{b}}</div>
    </div>
    addSub(watcher) {
        this.subs.push(watcher)
        console.log(this.subs)
    }

5个watcher

    <div id="app">
        <input type="text" v-model="message.a">
        <div>{{b}}</div>
        {{message.a}}
    </div>

5.2完整observer.js代码

class Observer {
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
        if (!data || typeof data !== 'object') {
            return;
        }
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key]);
            this.observer(data[key])
        })
    }
    defineReactive(obj, key, value) {
        let that = this;
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newValue) {
                if (newValue != value) {
                    that.observer(newValue)
                    value = newValue;
                    dep.notify();
                }
            }
        })
    }
}


class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(watcher) {
        this.subs.push(watcher);
    }
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}

6. watcer

6.1 new Watcher()

<div id="app"></div>下面的节点(有expr),一个expr对应一个watcher,一个watcher后续变化都保存到一个dep.subs[]里

6.2 完整watcer.js代码

class Watcher {
    constructor(vm, expr, cb) {
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        this.value = this.get();
    }
    getVal(vm, expr) {
        expr = expr.split('.'); // [message,a]
        return expr.reduce((prev, next) => { // vm.$data.a
            return prev[next];
        }, vm.$data);
    }
    get() {
        Dep.target = this;
        let value = this.getVal(this.vm, this.expr);
        Dep.target = null;
        return value;
    }
    update() {
        let newValue = this.getVal(this.vm, this.expr);
        let oldValue = this.value;
        if (newValue != oldValue) {
            this.cb(newValue)
        }
    }
}

7. 效果

GifCam录制gif

7.1 修改数据,视图变化

  1. 操作前,根据表达式v-model="message.a",得到vm.data里的数据,compile,渲染到页面
  2. vm.data里的数据变化,触发observer.set(),
  3. 因为新旧数据不一样,触发dep.notify()
  4. 触发watcher里的this.cb(newValue)
  5. 触发compile里的CompileUtil.updater()

7.2 修改视图,数据变化

  1. node.addEventListener('input'),监听输入框,得到新值newValue
  2. setVal(),使用vm.data里的数据等于新值
  3. 重复上面操作