手写一个简单的MVVM模式

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
    <input type="text" v-model='value.a'>
    {{value.a}}
    <button @click='button'>加1</button>
    </div>
</body>
<script src="./mvvm.js"></script>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./dep.js"></script>
<script src="./compile.js"></script>

<script>
    var app = new mvvm({
        el:'#app',
        data:{
            value:{
                a:'123'
            },
        },
        methods:{
            button(){
                this.value.a++
            },
        }
    })
</script>
</html>

mvvm.js

class mvvm {
    constructor(config) {
        //把模板元素节点绑定到实例的$el属性
        this.$el = document.querySelector(config.el)
        if (!this.$el) {
            throw new Error('组件的DOM根元素不能为空')
        }
        this.$data = config.data
        this.methods = config.methods
        //数据代理
        this.proxyData(config.data)
        //数据监测
        new Observer(config.data)
        //编译模板
        new Compile(this.$el, this)
    }
    //代理的方法
    proxyData(data) {
        Object.keys(data).forEach(key => {
            Object.defineProperty(this, key, {
                configurable: true,
                get() {
                    return this.$data[key]
                },
                set(newValue) {
                    this.$data[key] = newValue
                }
            })
        })
    }
}

observer.js

class Observer {
    constructor(data) {
        this.data = data
        this.observer(data)
    }
    observer(data) {
        if (!data || typeof data !== 'object') { return }
        //Object.keys会返回对象的key组成的数组
        //循环通过Object.defineProperty来监听属性的变化
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key]) //数据监测
            //如果data[key]是对象里面含有对象,需要继续监听深层次的数据
            this.observer(data[key])
        })
    }
    defineReactive(data, key, value) {
        let dep = new Dep()
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get: () => {
                // 将订阅器Dep添加订阅者的操作设计在getter里面,这是为了让Watcher初始化时进行触发,因此需要判断是否要添加订阅者。
                Dep.target && dep.addSub(Dep.target)
                console.log('获取了数据' + value, key);
                return value
            },
            //set的参数是该属性赋的新值
            set: (newValue) => {
                //替换原来旧的值
                if (newValue != value) {
                    console.log('更新了数据' + newValue);
                    //新的值也需要重新监听
                    this.observer(newValue)
                    value = newValue
                    //触发watcher的update()
                    dep.notify()
                }
            }
        })
    }
}

watcher.js

class Watcher {
    constructor(vm, exp, cb) {
        this.vm = vm
        this.exp = exp//v-model等指令的属性值 如 v-model="value.a",exp 就是value.a,或者或者插值符号中的属性,如{{value.a}} ,exp 就是 value.a.
        this.cb = cb
        //存储上一个值
        this.oldValue = this.get()
    }
    get() {
        Dep.target = this //把watcher实例缓存到 Dep.target
        //获取vm上的数据,就会出发数据对象的getter,这样就会从  Dep.target中读取 watcher实例并添加到Dep中
        // let value = this.vm.$data[this.exp] //这种写法只能获取对象最外面一级,如value.a就获取不到了
        let value = this.getValue(this.vm, this.exp)
        Dep.target = null
        return value
    }
    update() {
        // let newValue = this.vm.$data[this.exp]
        let newValue = this.getValue(this.vm, this.exp)
        if (this.oldValue !== newValue) {
            this.oldValue = newValue
            this.cb(newValue) //执行回调函数
        }
    }
    getValue(vm, exp) {
        exp = exp?.split('.') // [message.a]
        return exp?.reduce((prev, next) => {
            return prev[next]
        }, vm.$data)
    }

}

dep.js

class Dep {
    constructor() {
        //使用一个数组来存watcher
        this.subs = []
    }
    addSub(watcher) {
        //把watcher存起来
        this.subs.push(watcher)
    }
    notify() {
        //更新值的时候找到对应的watcher调用update()
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}
//这是一个全局的Watcher,同一时间只能有一个全局的Wathcer被计算
Dep.target = null

compile.js

class Compile {
    constructor(el, vm) {
        this.el = el
        this.vm = vm
        // 生成虚拟dom(用对象或者数组的方式来描述节点)
        this.complierNodes();
        //根据虚拟dom重新生成节点
        this.createElement()
    }
    //生成虚拟dom
    complierNodes() {
        //根节点的dom元素下的所有子节点
        var nodeList = this.el.childNodes;
        //生成vmNodes虚拟dom
        this.vm.vmNodes = this.complierNodesChild(nodeList)
    }
    //通过递归生成虚拟dom函数
    complierNodesChild(nodeList) {
        //初始化vmNodes数组,最后用来返回
        var vmNodes = []
        //使用一个对象来拼接我们的数据
        var data = {}
        //nodeList为伪数组,不能直接遍历
        Array.from(nodeList).forEach(node => {
            // 匹配字符串中插值表达式的内容
            var reg = /\{\{[^\{\}]*\}\}/g;
            data = {
                node: node,//存放节点元素
                nodeName: node.nodeName,//节点的名字
                nodeValue: node.nodeValue,//元素的值
                nodeType: node.nodeType,//元素的节点类型,1为普通元素,3位文本节点,8为注释
                data: [],//存放节点中的插值表达式的内容
                attrs: node.attributes,//元素节点的属性
                props: {},//除了v-开头剩下的属性
                directives: {},//专门存放指令的数组
                children: [],//用来存放子节点
                events: {},//存放节点的事件
            }
            //如果当前节点是一个文本节点
            if (node.nodeType === 3) {
                // nodeValue的值为空的话直接返回
                if (node.nodeValue.trim() === '') {
                    return false
                } else {
                    //字符串的match方法会根据正则返回一个数组
                    var arr = node.nodeValue.match(reg) || [];
                    //循环去掉大括号
                    arr.forEach(v => {
                        v = v.replace(/[/{/}]/g, "");
                        data.data.push(v)
                    })
                }
            }
            //如果当前是一个普通节点
            if (node.nodeType === 1) {
                //获取元素节点的属性
                var attrObj = { ...node.attributes }
                Object.keys(attrObj).forEach(index => {
                    var prop = attrObj[index]
                    //判断属性是否是v-开头的
                    if (/^(v-)+/.test(prop.name)) {
                        //把指令保存到data的directives中
                        data.directives[prop.name] = prop.value
                    } else if (/^(@)+/.test(prop.name)) {
                        //如果是以@开头的属性,说明他是一个事件
                        data.events[prop.name.replace('@', '')] = prop.value
                    } else {
                        //把非v-开头的属性添加到data.props
                        data.props[prop.name] = prop.value
                    }
                })
            }
            //如果节点还有子节点,就要循环递归执行当前的这个函数
            if (node.childNodes.length > 0) {
                data.children = this.complierNodesChild(node.childNodes)
            }
            //把对象放到虚拟节点上
            vmNodes.push(data)
        })
        return vmNodes
    }
    /**
     * reduce 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,
     * 接受四个参数:初始值(或者上一次回调函数的返回值),
     * 当前元素值,当前索引,调用 reduce 的数组。
     */
    //使用reduce函数来获取最底层的key
    getValue(exp) {
        exp = exp?.split('.')
        return exp?.reduce((prev, next) => {
            return prev[next]
        }, this.vm.$data)
    }
    getInputSetValue(vm, exp, value) {
        exp = exp?.split('.')
        return exp?.reduce((prev, next, currentIndex) => {
            //找出最后一个索引,也就是vm.$data里面的value[a]赋值value
            if (currentIndex == exp.length - 1) {
                prev[next] = value
            }
            return prev[next]
        }, vm.$data)
    }
    //递归生成子节点
    createElementChild(parentNode, nodeList) {
        //循环虚拟node节点重新生成dom元素
        nodeList.forEach(node => {
            var newNode;
            //如果是元素节点(非文本节点)使用createElement
            if (node.nodeType === 1) {
                newNode = document.createElement(node.nodeName)
                //元素节点会可能会有时间,使用addEventListener绑定事件
                Object.keys(node.events).forEach(eventName => {
                    newNode.addEventListener(eventName, (event) => {
                        //一下这种写法的this指向不对,需要让methods里面的this指向vm实例
                        // this.vm.methods[node.events[eventName]](event);
                        this.vm.methods[node.events[eventName]].call(this.vm, event);
                    })
                })
                //如果当前的节点是input元素,input元素中也含有v-model
                if (node.nodeName == 'INPUT' && node.attrs['v-model']) {
                    //获取vm.$data所对应的属性值来赋值当前节点的value
                    let value = this.getValue(node.attrs['v-model'].value)
                    newNode.value = value
                    //监听v-model绑定的 data属性的值的变化
                    new Watcher(this.vm, node.attrs['v-model'].value, () => {
                        newNode.value = this.getValue(node.attrs['v-model'].value)
                    })
                    //处理input标签的input事件
                    newNode.addEventListener('input', (event) => {
                        //获取输入框的值
                        var inputValue = event.target.value;
                        //修改vm.$data下面的某个属性,会触发set方法更新页面
                        this.getInputSetValue(this.vm, node.attrs['v-model'].value, inputValue)
                    })
                }
            }
           //文本节点
            if (node.nodeType === 3) {
                //把节点的插值表达式内容替换为vm.$data的属性值
                var text = this.replaceElementText(node.nodeValue);
                newNode = document.createTextNode(text)
                //监听vm.$data当前节点属性值的变化
                new Watcher(this.vm, node.data[0], () => {
                    node.node.nodeValue = this.replaceElementText(node.nodeValue)
                })
            }
            //注释节点
            if (node.nodeType === 8) {
                return
            }
            //把新的dom节点覆盖原来的dom节点
            node.node = newNode
            parentNode.appendChild(newNode)
            if (node.children.length > 0) {
                //判断如果有子元素,中心的执行函数,函数也会相对的变化
                this.createElementChild(newNode, node.children)
            }
        })
    }
    //把节点的插值表达式内容替换为vm.$data的属性值
    replaceElementText(value) {
        // 全局的正则表达式
        var reg = /\{\{[^\{\}]*\}\}/g;
        // 把带有两个大括号的数据返回一个数组
        var regArr = value.match(reg);
        // 如果是一个数组代表值里面有两个大括号
        if (Array.isArray(regArr)) {
            // 循环的替换value的值
            regArr.forEach(v => {
                // 相当于把{{value.a}}两侧的大括号给删掉
                var prop = v.replace(/[/{/}]/g, "");
                value = this.getValue(prop)
            })
        }
        return value;
    }
    // 根据虚拟dom重新生成节点
    createElement() {
        //创建一个虚拟的模板节点(文档片段)
        var fragment = document.createDocumentFragment();
        //返回最后所有虚拟节点生成的dom元素
        this.createElementChild(fragment, this.vm.vmNodes);
        //清空根节点所有内容
        this.el.innerHTML = '';
        //重新生成dom元素
        this.el.appendChild(fragment)
    }
}

效果演示:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值