一个简单的MVVM

简介: 一个简单的MVVM

前言

看了Vue的一些思想之后,开始有想法去模仿Vue写一个小的MVVM,奈何当自己真正开始写的时候才知道有多难,不过也让自己明白,自身的编码水平和设计代码的思维还有很大的提升空间,哈哈哈。


开始

先来一个基本的index.html文件,然后我们模仿Vue的写法,实例化一个MVVM类和定义data对象(Vue里为了拥有自己的命名空间data应该为函数)

<!DOCTYPE html><html lang="en">
<head>  ```</head>
<body>
    <div id="app">
        <div>
            <div>
                <span>{{hello}}</span>
            </div>
            <div>{{msg}}</div>
        </div>
    </div>
    <script src="./src/index.js"></script>
    <script>
        const app = new MVVM({
            $el: '#app',
            data: {
                msg: 'mvvm',
                hello: 'david'
            },
        })
    </script>
</body>
</html>

我们设想是这样来操作滴,然后就可以编写我们的MVVM类了。我感觉写这个的话一种由上而下的思路会比较好,就是先把最顶层的思路想好,然后再慢慢往下写细节。

MVVM

class MVVM {
    constructor(options) {
        this.$el = options.$el
        this.data = options.data
        if (this.$el) {
            const wathcers = new Compiler(this.$el, this)
            new Observer(this.data, wathcers)
        }
    }
}

这里我们定义了一个MVVM类,在options里面可以拿到$eldata参数,因为我们上面的模板里面就是这么传的。如果传入的$el节点确实存在的话,就可以开始我们的初始化编译模板操作。

Compiler

function Compiler(el, vm) {}


看上面我们知道,Compiler的参数有两个,一个是$el字符串,还有一个就是我们的MVVM实例,上面我传了this

遍历子节点

首先我们先来思考,编译模板的时候希望的是将类似{{key}} 的部分用我们的data对象中的对应的value来取代。所以我们应该先遍历所有的dom节点,找到形如{{key}}所在的位置,再进行下一步操作。先来两个函数

this.forDom = function (root) {
        const childrens = root.children
        this.forChildren(childrens)
}

这是一个获取dom节点的子节点的函数,然后将子节点传入下一个函数

this.forChildren = function (children) {
        for (let i = 0; i < children.length; i++) {
            //每个子节点
            let child = children[i];
            //判断child下面有没有子节点,如果还有子节点,那么就继续的遍历
            if (child.children.length !== 0) {
                this.forDom(child);
            } else {
                //将vm与child传入一个新的Watcher中
                let key = child.innerText.replace(/^\{\{/g, "").replace(/\}\}$/g, "")
                let watcher = new Watcher(child, vm, key)
                //初始转换模板
                compilerTextNode(child, vm)
                watchers.push(watcher)
            }
        }
}

如果子节点还有子节点,就继续调用forDOM函数。否则就将标签中{{key}}里面的key拿出来(这里我只考虑了形如<div>{{key}}</div>的情况,大佬轻喷),拿到key之后就实例化一个watcher,让我们来看看watcher做了啥。

Watcher

function Watcher(child, vm, initKey) {
    this.initKey = initKey
    this.update = function (key) {
        if (key === initKey) {
            compilerTextNode(child, vm, initKey)
        }
    }
}

首先把所对应的子节点child传入,然后vm实例也要传入,因为下面有一个函数需要用到vm实例,然后这个initKey是我自己的一些骚操作(流下了没有技术的泪水),它的作用主要是记录一开始的那个key值,为啥要记录呢,请看下面的方法。

compilerTextNode

compilerTextNode = function (child, vm, initKey) {
    if (!initKey) {
        //第一次初始化
        const keyPrev = child.innerText.replace(/^\{\{/g, "").replace(/\}\}$/g, "") //获取key的内容
        if (vm.data[keyPrev]) {
            child.innerText = vm.data[keyPrev]
        } else {
            throw new Error(
                `${key} is not defined`
            )
        }
    } else {
        child.innerText = vm.data[initKey]
  }

首先这个函数会有两个逻辑,一个是初始化的时候,还有一个是数据更新的时候。可以看到初始化的时候我们是这样做的compilerTextNode(child, vm),也就是会进入这个if逻辑。这里就是拿到了模板中的key值,然后节点的值替换成我们data对象里面的值。为啥要记录这个initKey呢,就是在这里如果模板的innerText直接被整个替换掉了,例如说原本模板中是{{msg}},它经过这个函数处理之后,会变成mvvm,那我们的data中是没有mvvm这个key的,这里记录是为了更新的时候用。最后,所有的watcher都会被pushwatchers数组里,并且返回。

Observer

function Observer(data, watchers) {}

然后就到了我们熟悉的响应式数据啦,这个函数接受两个参数,一个就是我们一开始定义的data对象,还有一个就是刚才我们拿到的watchers数组。

observe

    this.observe = function (data) {
        if (!data || typeof data !== 'object') {
            return
        }
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
            this.observe(data[key]) //递归深度劫持
        })
    }

首先我们先来对data做一下判断,然后调用defineReactive方法对data做响应式处理,最后来个递归深度劫持data

defineReactive

    this.defineReactive = function (obj, key, value) {
        let that = this
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                return value
            },
            set(newValue) {
                if (newValue !== value) {
                    that.observe(newValue)
                    value = newValue
                    //重新赋值之后 应该通知编译器
                    watchers.forEach(watcher => {
                        watcher.update(key)
                    })
                }
            }
        })
    }

get方法调用时直接返回valueset方法调用时如果value有重新赋值,那么应该重新监听value的新值,然后用watcher通知编译器重新渲染模板。

然后调用observe方法,this.observe(data)

这里我们再看回watcher.update方法,在defineReactive方法中调用时传入的key是我们data中定义的,而这个initKey也就是我们之前在初始化模板的时候保存的,当这两个相等的时候才重新渲染对应的模板块

this.update = function (key) {
        if (key === initKey) {
            compilerTextNode(child, vm, initKey)
        }
}

最后让我们来看一眼效果,加上一小段改变数据的代码。

setTimeout(() => {
            app.data.msg = 'change'
        }, 2000)

2.png

总结与反思

我们来思考一下ObserverWatcherCompiler三者之间的关系。Observer最重要的职责是把数据变成响应式的,换句话说就是我们可以在数据被取值或者赋值的时候加入一些自己的操作。Compiler就是把HTML模板中的{{key}}变成我们data中的值。Watcher就是它们二者之间的桥梁了,在一开始的时候观察所有存在插值的节点,当data中的数据更新时,可以通知模板,让其重新渲染同步data中的数据。

最后,其实我也不知道写的这个算不算MVVM(捂脸),编码能力真心还有待提高,继续加油吧!


相关文章
|
JavaScript 前端开发 设计模式
什么是MVVM
什么是MVVM
|
7月前
|
前端开发
什么是MVVM架构?
MVVM是Model-View-ViewModel的简写。它本质上就是MVC的改进版。MVVM模式有助于将应用程序的业务和表示逻辑与用户界面 (UI) 清晰分离。 保持应用程序逻辑和UI之间的清晰分离有助于解决许多开发问题,并使应用程序更易于测试、维护和演变。 它还可以显著提高代码重用机会,并允许开发人员和UI设计人员在开发应用各自的部分时更轻松地进行协作。
119 2
|
3月前
|
开发框架 前端开发 JavaScript
|
7月前
|
前端开发 JavaScript 架构师
什么是 MVVM?
什么是 MVVM?
73 0
|
前端开发 JavaScript
什么是MVVM?
MVVM,是Mode1-View-ViewModel的简写,是M/-V-VM三部分组成。它本质上就是MVC的改进版。MVVM就是将其中的View的状态和行为抽象化,其中ViewModel将视图U和业务逻辑分开,它可以取出Model的数据间时帮忙处理View中由于需要展示内容而涉及的业务逻辑
124 0
|
存储 前端开发 测试技术
mvvm讲解
mvvm讲解
62 0
|
JavaScript 前端开发
实现一个自己的MVVM(二)
实现一个自己的MVVM
58 0
|
JavaScript 前端开发 容器
实现一个自己的MVVM(一)
实现一个自己的MVVM
73 0
|
前端开发 JavaScript 数据可视化
深入理解MVVM架构模式
深入理解MVVM架构模式
1088 0
|
敏捷开发 前端开发 测试技术