一个简单的MVVM

前言

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

开始

先来一个基本的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类了。我感受写这个的话一种由上而下的思路会比较好,就是先把最顶层的思路想好,而后再慢慢往下写细节。bash

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节点确实存在的话,就能够开始咱们的初始化编译模板操做。app

Compiler

function Compiler(el, vm) {}复制代码

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

遍历子节点

首先咱们先来思考,编译模板的时候但愿的是将相似{{key}} 的部分用咱们的data对象中的对应的value来取代。因此咱们应该先遍历全部的dom节点,找到形如{{key}}所在的位置,再进行下一步操做。先来两个函数mvvm

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作了啥。ui

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值,为啥要记录呢,请看下面的方法。this

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)复制代码


总结与反思

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

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

相关文章
相关标签/搜索