从0到1实现一个简单的MVVM框架 | 青训营笔记

147 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的第1天

  • 简介:实现一个简单的Mvvm框架,利用js es6模块化,less进行简单样式美化,webpack进行打包

  • 实现的功能:1.数据劫持;2.发布订阅模式;3.数据的单向绑定;4.双向绑定

  • 绑定指令语法:{{ }};v-text;v-model;

  • gitee pages: xieyusai.gitee.io/class-assig…

  • 项目地址:gitee.com/xieyusai/cl…

    <!-- 简化后的html代码,源码稍加修饰 -->
    <div id="app">
    <h2>{{title}}</h2>
    <span>{{msg}}</span>
    <input v-model="msg" type="text" />
    <div v-text="hello"></div>
    <select v-model="hello" name="hello">
        <option value="hello world">hello world</option>
        <option value="hello html">hello html</option>
        <option value="hello javascript">hello javascript</option>
        <option value="hello vue">hello vue</option>
        <option value="hello nodejs">hello nodejs</option>
        <option value="hello typescript">hello typescript</option>
    </select>
    </div>
    <script>
        const vm = new Mvvm({
            el: '#app',
            data: {
                msg: '请输入文字:',
                title: 'Mvvm框架',
                hello: '请下拉选择:',
            },
        })
    </script>

效果:

1.gif

前言

通过Object.defineProperty()中的get与set来实现对数据的劫持,数据变动时通知订阅者,触发相应的回调函数,以监听数据变动。 1、创建数据监听器Observer,对数据对象的属性进行监听。 2、创建指令解析器Compiler,对每个元素节点的指令进行编译 3、创建观察者Watcher,监听模型数据的变动以更新视图 4、创建订阅器Dep,收集依赖属性的watcher 5.各个模块如下:

1、mvvm入口函数

Mvvm构造器如下,在其中做了属性代理操作,把data的属性都映射到Mvvm中,这样直接使用vm就可以操作data属性。 在构造器中,先获取操作的dom对象,调用observer模块进行数据监听,然后调用compiler模块进行编译,以识别{{}}语法(也添加了v-model与v-text语法)。

class Mvvm {
    constructor(options) {
        // 获取元素dom对象
        this.$el = document.querySelector(options.el);
        // 转存数据,保存传递过来的data数据
        this.$data = options.data || {};
        // 进行数据的代理
        this._proxyData(this.$data);
        // 数据劫持
        new Observer(this.$data);
        // 模板编译
        new Compiler(this)
    }
    // 数据的代理
    _proxyData(data) {
        Object.keys(data).forEach((key) => {
            // 利用Object.keys方法,取出data中每一项数据的属性名key
            Object.defineProperty(this, key, {
                // 设置可以枚举
                enumerable: true,
                // 设置可以配置
                configurable: true,
                // 获取数据;读数据
                get() {
                    return data[key]
                },
                // 设置数据;写数据
                set(newValue) {
                    // 判断新值和旧值是否相等,若相等则return退出函数
                    if (newValue === data[key]) {
                        return
                    }
                    // 若不相等,则设置新值
                    data[key] = newValue
                },
            })
        })
    }
}
// 挂载到window
window.Mvvm = Mvvm;

2、Observer,对Mvvm中data数据进行劫持

数据可能存在子数据,为进行深度数据劫持创建walk遍历函数进行递归调用,递归的终止条件:data为空或者非对象。

import Dep from "./dep";//导入Dep模块
export default class Observer {
    constructor(data) {
        this.data = data;
        // 遍历对象,完成所有对象(对象的子数据)的劫持
        this.walk(data)
    }
    // 遍历对象函数
    walk(data) {
        // 递归的终止条件,data为空或者非对象
        if (!data || typeof data !== 'object') {
            return
        }
        // 利用Object.keys方法,取出data中每一项数据的属性名key
        Object.keys(data).forEach((key) => {
            // 进行数据绑定,完成数据劫持
            this.dataHijack(data, key, data[key])
        })
    }
    // 设置响应式数据(get与set),完成数据劫持
    dataHijack(obj, key, value) {
        // 对数据值进行遍历
        this.walk(value)
        // 暂存当前this指向
        const that = this
        // 新建Dep对象,收集该依赖属性的Watcher对象
        let dep = new Dep()
        Object.defineProperty(obj, key, {
            // 可遍历
            enumerable: true,
            // 可再配置
            configurable: false,
            // 获取数据;读数据
            get() {
                // 添加观察者对象
                if (Dep.target) {
                    dep.addSub(Dep.target)
                }
                return value
            },
            // 设置数据;写数据
            set(newValue) {
                // 判断新值和旧值是否相等,若相等则return退出函数
                if (newValue === value) {
                    return
                }
                // 若不相等,则设置新值
                value = newValue
                // newValue对象可能存在属性,对其业进行遍历
                that.walk(newValue)
                // 通知订阅者
                dep.notify()
            },
        })
    }
}

Observer中调用了Dep,在get时添加Dep.target(watcher),在set时触发notify(通知每个watcher)。 实现Observer后,程序已经能够监听数据以及通知订阅者,接下来在Compiler模块中编译模板。

3、Compiler,对HTML进行模板编译

compiler模块用来解析模板指令,同时将模板中的变量替换成数据,然后对文本内容进行替换赋值给节点。同时添加订阅者watcher以监听数据,数据变动时会收到通知。在指令的处理上,文本节点利用正则表达式识别{{}}指令,元素节点利用order函数通过字符去匹配对应的指令,能够识别节点属性中的v-text与v-model指令。

import Watcher from "./watcher";//导入Watcher模块
export default class Compiler {
    constructor(vm) {
        // 拿到vm与el
        this.vm = vm
        this.el = vm.$el
        // 编译模板
        this.compile(this.el)
    }
    // 编译模板函数
    compile(el) {
        // 拿到元素子节点
        let childNodes = [...el.childNodes]
        // 对子节点进行遍历,判断每个节点node类型,以匹配不同的编译方法
        childNodes.forEach((node) => {
            // 文本节点类型===3
            if (node.nodeType === 3) {
                // 编译文本节点
                this.compileTextNode(node)
            } else if (node.nodeType === 1) {
                //元素节点类型===1,编译元素节点
                this.compileElementNode(node)
            }
            // 如果子节点还存在子节点,则进行递归遍历
            if (node.childNodes && node.childNodes.length > 0) {
                // 继续递归编译模板
                this.compile(node)
            }
        })
    }
    // 编译文本节点函数
    compileTextNode(node) {
        // 匹配 {{}}内容的正则表达式
        let reg = /{{(.+?)}}/
        // 获取文本节点的内容
        let val = node.textContent
        // 判断文本是否存在{{}}
        if (reg.test(val)) {
            // 获取{{  }}中内容同时去除前后空格
            let key = RegExp.$1.trim()
            // 对文本内容进行替换,同时赋值给节点
            node.textContent = val.replace(reg, this.vm[key])
            // 创建一个观察者
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = newValue
            })
        }
    }
    // 编译元素节点函数,编译指令
    compileElementNode(node) {
        // 遍历元素节点的属性
        ![...node.attributes].forEach((attr) => {
            // 得到属性名
            let attrName = attr.name
            // 判断属性名是否以v-开头
            if (attrName.startsWith('v-')) {
                // 若是v-开头,则除去v-
                attrName = attrName.substr(2);
                // 设置属性值
                let key = attr.value;
                // 执行对应的指令方法
                // 利用order指令方法去执行对应的指令
                this.order(node, key, attrName)
            }
        })
    }
    // 添加指令方法 并且执行
    order(node, key, attrName) {
        // 利用字符去匹配对应的指令
        // textOrder,modelOrder
        let orderFunction = this[`${attrName}Order`]
        // 调用方法
        if (orderFunction) {
            orderFunction.call(this, node, key, this.vm[key])
        }
    }

    // 设置了两个指令方法
    // v-text
    textOrder(node, key, value) {
        node.textContent = value
        // 创建观察者
        new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue
        })
    }
    // v-model
    modelOrder(node, key, value) {
        node.value = value
        // 创建观察者
        new Watcher(this.vm, key, (newValue) => {
            node.value = newValue
        })
        // input值时进行数据同步,实现双向绑定
        node.addEventListener('input', () => {
            this.vm[key] = node.value
        })
    }
}

4、Dep,收集所有Watcher订阅者

创建一个Dep类,数据劫持操作时实例化dep对象,在监听到数据属性变化时收集依赖属性的watcher,添加到数组中存储。类似一个消息订阅器,创建一个subs数组收集订阅者,当数据变动触发notify通知sub数组中每个watcher,再触发每个观察者的update更新方法。

export default class Dep {
    constructor() {
        // 创建数组以存储watcher
        this.subs = []
    }
    // sub数组中添加watcher
    addSub(watcher) {
        // 判断观察者是否存在 和 是否拥有update方法
        if (watcher && watcher.update) {
            this.subs.push(watcher)
        }
    }
    // 通知sub数组中每个watcher
    notify() {
        // 触发每个观察者的更新方法
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

5、Watcher,对Compiler提取的数据进行订阅

当编译模板的时候,实例化watcher观察者对象,以监听模型数据的变动,并在数据变动时调用callback函数更新视图

import Dep from "./dep";//导入Dep模块
export default class Watcher {
    constructor(vm, key, cb) {
        // 拿到vm与属性key
        this.vm = vm
        this.key = key
        // callback回调函数用来执行更新视图的方法
        this.cb = cb
        // 负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中
        Dep.target = this
        // 存储旧数据,同时触发get方法,通过observer.js中的dep.addSub(Dep.target)把watcher添加到了sub数组中
        this.oldValue = vm[key]
    }
    // updata函数,让发布者通知watcher进行更新
    update() {
        // 获取新值
        let newValue = this.vm[this.key]
        // 比较旧值和新值,若值不改变则return
        if (newValue === this.oldValue) {
            return
        }
        // 若值改变则调用callback函数更新视图
        this.cb(newValue)
    }
}

总结

至此,一个基本的Mvvm框架就完成了,实现了1.数据劫持;2.发布订阅模式;3.数据的单向绑定;4.双向绑定。 在效果的展示中添加了css样式以让视图更美观,在作业的完成过程中参考了一些对vue2.0的解析文章,自身的代码水平还有待进一步的提高。