一個簡單的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(捂臉),編碼能力真心還有待提升,繼續加油吧!

相關文章
相關標籤/搜索