一个简单的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(捂脸),编码能力真心还有待提高,继续加油吧!


相关文章
|
编解码 对象存储 UED
[Halcon&标定] 单相机标定
[Halcon&标定] 单相机标定
1619 2
|
前端开发 C#
WPF技术之ContentControl 控件
ContentControl 是 WPF 中的一个常见控件,用于显示单个内容元素。它可以包含任意类型的内容,包括文本、图像、控件等。
2384 0
|
Java Nacos Spring
使用Spring Boot的Profile功能来实现不同环境使用不同的Nacos Namespace的配置
使用Spring Boot的Profile功能来实现不同环境使用不同的Nacos Namespace的配置
907 1
|
Kubernetes 调度 容器
二进制 k8s 集群下线 worker 组件流程分析和实践
二进制 k8s 集群下线 worker 组件流程分析和实践
256 0
|
C语言 C++
【c++】C语言之输入行数,输出实心菱形和空心菱形
C语言之输入行数,输出实心菱形和空心菱形
1666 1
【c++】C语言之输入行数,输出实心菱形和空心菱形
|
开发框架 前端开发 JavaScript
ASP.NET程序设计课程设计——新闻发布系统
ASP.NET程序设计课程设计——新闻发布系统
192 0
ASP.NET程序设计课程设计——新闻发布系统
|
网络协议 Python
python实现TCP客户端从服务器下载文件
python实现TCP客户端从服务器下载文件TCP模拟服务器 import socket def send_file_2_client(new_client_socket, client_addr): # 1.
1528 0
|
存储 监控 关系型数据库
MySQL配置信息解读(my.cnf)
从年后换了工作到现在差不多两个月了,比较忙,所以写博客的时间越来越少了。     以前学生时代用MySQL,从安装开始就是“下一步”,设置向导弄中设置用户、端口、编码什么的就好了。后来工作了公司用的Oracle,但是普通程序员也接触不到。
813 0
ruby文件打开和关闭
class File   def File.Open(*args)     result=f=File.new(*args)     if block_given?     begin       result=yield f     ensure       f.
604 0