MVVM框架实现vue的双向绑定

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
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容

  • 2017年1月31日 一~所有的心理学治疗依据都是形成假设!第一个步骤就是形成假设,人与人的互动过程中,需要访谈!...
    阿甘阿干阅读 448评论 0 0
  • 周末,一周最关键的时刻,也是决定胜负的时刻,两对Pk的热火朝天。中午交接班的时候,这时客流也比较多,每个人...
    刘清红阅读 98评论 2 0
  • 今晚和朋友去看港囧了,一部有青春,有现实,有幻想的片子…看完,很感人,不知道其他的观众是怎样呢,但怎样也与我无关…...
    桃树的眼泪阅读 266评论 2 1
  • 她有气质、又漂亮,把这组图集分享,收藏好吧不谢 真诚感谢你在百忙之中移驾鄙人主父眼寒舍,虽然未能如愿观赏到飞花似雪...
    主父眼阅读 79评论 0 0