MVVM框架实现vue的双向绑定

174 阅读6分钟

1.MVVM

MVVM => Model(数据)-View(视图)-ViewModel(视图模型)

  • vue中的对应关系:Model => data,View => Template,ViewModel => new Vue()...

  • MVVM 将数据双向绑定作为核心思想,View 和 Model 之间没有关联,它们通过 ViewModel 这个桥梁进行交互。

  • Model 和 ViewModel 之间的交互是双向的, View 的变化会同步到 Model,而 Model 的变化也会立即同步到 View 上。

  • 当用户操作 View,ViewModel 感知到变化,然后通知 Model 发生相应改变;反之当 Model 发生改变,ViewModel 也能感知到变化,使 View 作出相应更新。

2.手写vue.js

以下代码简单实现了vue的双向数据绑定,以及computed,methods功能。 注:CompierUtil为抽离出来的公共方法。

代码注释很详细(可以留言提问)

  • 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="school.name">
    <div>{{school.name}}{{school.age}}</div>
    <div>{{school.age}}</div>
    <div>{{desc}}</div>
    <div v-html="message"></div>
    <ul>
      <li>1</li>
      <li>2</li>
    </ul>
    <button v-on:click="change"></button>
  </div>
  <!-- <script src="./vue/dist/vue.js"></script> -->
  <script src="./MVVM.js"></script>
  <script>
    // console.log(Vue);
    var vm = new Vue({
      el: '#app',
      data: {
        school:{
          name: 'alex',
          age:'18'
        },
        message: '<h1>哈哈</h1>'
      },
      computed:{
        desc() {
          return this.school.name + '厉害'
        }
      },
      methods:{
        change() {
          this.school.age = 100
        }
      }
    });
  </script>
</body>
</html>
  • MVVM.js

新建一个mvvm.js,将参数通过options传入mvvm中,并取出el和data绑定到mvvm的私有变量和data中。

// 创建自己的vue类
class Vue {
    constructor(options) {
        // options:实例化时传进来的参数
        this.$el = options.el;
        this.$data = options.data;
        let computed = options.computed;
        let methods = options.methods;
        // 判断根元素是否存在
        if (this.$el) {
            // 数据劫持,给每一个属性添加一个dep
            new Observer(this.$data)
            // 代理 computed 数据到this.$data上,以便可以直接通过this.xxx访问数据
            for (let key in computed) {
                Object.defineProperty(this.$data, key, {
                    get:() => {
                        return computed[key].call(this)
                    }
                })
            }
            // 代理 methods 数据到实例上,以便可以直接通过this.xxx访问数据
            for (let key in methods) {
                Object.defineProperty(this, key, {
                    get:() => {
                        return methods[key]
                    }
                })
            }
            // 将this.$data 上的数据代理到this上
            this.proxyVm(this.$data)
            // 编译模版数据
            new Compiper(this.$el, this)
        }
    }
    proxyVm(data) {
        // 访问this.xxx 即 this.$data.xxx
        for (let key in data) {
            Object.defineProperty(this, key, {
                // 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
                configurable: false,
                // 当且仅当该属性的 enumerable 为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
                enumerable: false,
                get(){
                    return data[key]
                },
                set(newValue){
                    data[key] = newValue
                }
            })
        }
    }
}

实现Observer,监听所有的数据,并对变化数据发布通知;

class Observer {
    constructor(data) {
        this.observer(data)
    }
    observer(data) {
        // 判断属性值是否为object,只要对象才能做数据劫持
        if (data && typeof data == 'object') {
            for (let key in data) {
                this.defineReactive(data, key, data[key])
            }
        }
    }
    // 对当前属性重新定义
    defineReactive(obj, key, value) {
        // 属性的值如果是对象的话,进行递归定义,以达到所有属性都被监测
        this.observer(value)
        // 实例化一个订阅器到当前属性作用域内,此dep只能被当前属性调用
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get() {
                // 判断
                console.log(Dep.target)
                // 将订阅者存储(为了不重复存储,当target存在时才执行,执行一次后在 watcher 中设为 null)
                Dep.target && dep.addSub(Dep.target)
                return value
            },
            set: (newValue) => {
                // 当新值发生变化时执行
                if (newValue != value) {
                    // 对新值做监测
                    this.observer(newValue)
                    // 将新值覆盖老值
                    value = newValue
                    // 通知此属性的订阅者进行数据更新
                    dep.notify();
                }
            }
        })
    }
}

实现Compiper,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且添加订阅者

// 编译模版(核心代码)
class Compiper {
    constructor(el, vm) {
        // document.getElementById获取到的是动态的 document.querySelector获取的是静态的
        // 获取根元素
        this.el = this.isElementNode(el) ? el : document.querySelector(el);
        this.vm = vm;
        // 将根元素获取到内存中
        let fragment = this.nodeToFragment(this.el)
        // 编译模版,将数据替换模版中的表达式({{school.name}}\v-model="school.name")
        this.compier(fragment)
        // 将替换后的内容塞到页面
        this.el.appendChild(fragment)
    }
    compier(node) {
        let childNode = node.childNodes; // 一层子元素,不包括儿子的儿子 类数组
        [...childNode].forEach(child => {
            if (this.isElementNode(child)) {
                // 元素节点处理
                this.compierElement(child)
                // 递归将所有元素编译
                this.compier(child)
            } else {
                // 文本节点处理
                this.compierText(child)
            }
        })
    }
    // 判断属性是否为指令
    isDirective(attrName) {
        // startsWith es6的方法
        return attrName.startsWith('v-')
    }
    // 编译元素
    compierElement(node) {
        // 获取元素属性
        let attributes = node.attributes; // 类数组
        [...attributes].forEach(attr => {
            // console.log(attr); type=text v-model=school.name
            let {
                name,
                value: expr
            } = attr
            // 判断属性是否为指令
            if (this.isDirective(name)) { // v-model v-html v-bind v-on:click
                let [, directive] = name.split('-') // directive:model\html\bind\on:click
                // 如果是on:click进行分割
                let [directiveName, eventName] = directive.split(':')
                // 解析指令
                CompierUtil[directiveName](node, expr, this.vm, eventName)
            }
        })
    }
    // 编译文本
    compierText(node) {
        // 获取文本节点的内容
        let content = node.textContent;
        if (/\{\{(.+?)\}\}/.test(content)) {
            // 解析指令
            CompierUtil['text'](node, content, this.vm)
        }
    }
    nodeToFragment(node) {
        // 创建文档碎片
        let fragment = document.createDocumentFragment()
        let firstChild
        // 将模版添加到文档碎片这种
        while (firstChild = node.firstChild) {
            // appendChild可以将模版元素移到文档碎片中
            fragment.appendChild(firstChild)
        }
        return fragment
    }
    isElementNode(node) {
        // 判断是不是元素 
        //1.元素节点 2.属性节点 3.文本节点
        return node.nodeType === 1
    }
}

实现Watcher,作为一个中枢,接收observe发来的通知,并执行Dep中的更新方法。

//定义一个订阅者
class Watcher {
    constructor(vm, expr, cb) {
        // 缓冲当前值
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        // 对老值进行存储
        this.oldValue = this.getValue()
    }
    getValue() {
        // 在获取老值的时候,首先将自己添加到全局
        Dep.target = this; // watcher实例
        // 获取已经被劫持的值,会调用 object.defineProperty 的 get 方法,从而将 watcher 添加到订阅器上
        let newValue = CompierUtil.getValue(this.vm,this.expr)
        // 清楚实例,以免重复添加
        Dep.target = null;
        return newValue
    }
    update() {
        // 获取新值
        let newValue = CompierUtil.getValue(this.vm,this.expr)
        if (newValue != this.oldValue) {
            // 调用新值的回掉函数
            this.cb(newValue)
        }
    }
}

实现Dep:管理订阅者,通知更新

class Dep{
    constructor() {
        // 存储订阅者
        this.subs = []
    }
    // 订阅
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 发布
    notify() {
        // 数据变化时通知订阅者更新
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

解析指令方法

// 解析指令的方法
CompierUtil = {
    getValue(vm, expr) {
        // 根据表达式获取值(school.name => alex,message => <h1>哈哈</h1>)
        return expr.split('.').reduce((data, current) => {
            return data[current]
        }, vm.$data)
    },
    setValue(vm,expr,value) {
        // 对表达式对应的属性重新赋值
        expr.split('.').reduce((data, current, index, arr) => {
            if (index === arr.length -1 ) {
                return data[current] = value
            }
            return data[current]
        }, vm.$data)
    },
    model(node, expr, vm) {
        // 定义更新元素内容的方法
        let fn = this.updater['modelUpdater']
        // 根据表达式获取值
        let value = this.getValue(vm, expr)
        // 初始化视图渲染
        fn(node, value)
        // 对输入框(v-model)订阅
        new Watcher(vm, expr, (newValue) => {
            // 数据变化执行,将视图更新
            fn(node, newValue)
        })
        // 监测视图的更新
        node.addEventListener('input', (e) => {
            let newValue = e.target.value;
            // 将新值更新到数据中(vm.$data)
            this.setValue(vm, expr, newValue)
        })
    },
    html(node,expr,vm) {
        // 定义更新元素内容的方法
        let fn = this.updater['htmlUpdater']
        // 根据表达式获取值
        let value = this.getValue(vm, expr)
        // 初始化视图渲染
        fn(node, value)
        // 对v-html订阅
        new Watcher(vm, expr, (newValue) => {
            // 数据变化执行,将视图更新
            fn(node, newValue)
        })
    },
    on(node,expr,vm,eventName) {
        // 对on指令对应的元素进行事件监听 v-on:click="change"
        node.addEventListener(eventName, (e) => {
            // expr => change
            // change 方法内的 this 指向 vm
            vm[expr].call(vm,e)
        })
    },
    // 获取文本框表达式对应的数据
    getContentValue(vm,expr) {
        let value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            // console.log(args) ["{{school.name}}", "school.name", 0, "{{school.name}}{{school.age}}"]
            return this.getValue(vm, args[1])
        })
        return value
    },
    text(node, expr, vm) {
        // 定义更新文本内容的方法
        let fn = this.updater['textUpdater']
        // 对每一文本进行数据替换
        // expr => {{school.name}}{{school.age}}
        let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
            // 给每一个{{}}加入观察者
            new Watcher(vm, args[1], () => {
                // 对每一个{{}}所在的元素节点更新
                fn(node, this.getContentValue(vm,expr) )
            })
            let value = this.getValue(vm, args[1])
            return value
        })
        // 初始化视图渲染
        fn(node, content)
    },
    updater: {
        // 输入框更新方法
        modelUpdater(node, value) {
            node.value = value
        },
        // 文本更新方法
        textUpdater(node, value) {
            node.textContent = value
        },
        // 富文本更新方法
        htmlUpdater(node,value) {
            node.innerHTML = value
        }
    }
}