认识和实现MVVM

325 阅读2分钟

一、什么是MVVM

MVVM就是model-view-viewModel的简写。它本质上就是MVC的改进版,MVVM就是将其中的view的状态和行为抽象化,视图UI和业务逻辑分开;让我们只要关注逻辑的实现。

  • M -> Model 数据保存和数据处理
  • V -> View 视图 HTML
  • VM -> viweModel 驱动 视图改变驱动数据改变 数据改变驱动视图改变

image.png

实现方法

在实现MVVM库前,我们需要先理清下整体思路,先看下以下流程图:

image.png

  1. 实现compile,对模板进行编译,编译元素、方法、事件绑定等
  2. 实现observe,对数据进行监听,并且数据变化时执行update函数

1、新建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"></div>
    <script type="module" src="./app.js"></script>
</body>
</html>

新建入口文件app.js

/**
 * MVVM
 * VM 驱动VM ViewModel
 * V  View视图
 * M  Model 数据保存和数据处理
 * 视图改变通过VM驱动数据改变
 * 数据改成通过VM驱动视图改变
 */

import { createApp, useReactive } from './MVVM'

function App() {
    const state = useReactive({
        count: 0,
        name: '张三'
    })

    const add = (num) => {
        console.log(num, 'num')
        state.count += num
        console.log(state.count)
    }

    const minus = (num) => {
        state.count -= num
        console.log(state)
    }

    const changeName = (num) => {
        state.name = num
        console.log(state)
    }

    return {
        template: `
            <h1>{{ count }}</h1>
            <h2>{{ name }}</h2>
            <button @click="add(1)">+</button>
            <button @click="minus(1)">-</button>
            <button @click="changeName('李四')">changeName</button>
        `,
        state,
        methods: {
            add,
            minus,
            changeName
        }
    }
}

createApp(
    App(),
    document.querySelector('#app')
)

2、实现render函数以及Observe(reactive)

MVVM -> index.js

export { createApp } from './render'
export { useReactive } from './reactive'
export { eventFormat } from './compile/event'
export { stateFormat } from './compile/state'

MVVM -> render.js

import { bindEvent } from './compile/event'
import { eventFormat, stateFormat } from './index'

export function createApp(
    {
        template,
        state,
        methods 
    },
    rootDom
) {
    rootDom.innerHTML = render(template, state)
    // 绑定事件
    bindEvent(methods)
}

function render(template, state) {
    template = eventFormat(template)
    template = stateFormat(template, state)
    return template
}

export function update(statePool, key, value) {
    const allElements = document.querySelectorAll('*')
    let oItem = null,
        _mark = null;

    statePool.forEach(item => {
        // 当state的最后一个key 和 更新的key相等时
        if(item.state[item.state.length - 1] === key) {
            for(let i = 0; i < allElements.length; i++) {
                oItem = allElements[i]
                _mark = parseInt(oItem.dataset.mark)
                
                // 当mark相等时
                if(item.mark === _mark) {
                    // 直接更改元素内容
                    oItem.innerHTML = value
                }
            }
        }
    })
}

reactive -> index.js

import { isObject } from "../shared/utils";
import { mutableHandler } from "./mutableHandler";

export function useReactive(target) {
    return createReactObject(target, mutableHandler)
}

function createReactObject(target, baseHandler) {
    if(!isObject(target)) {
        return target
    }

    const observe = new Proxy(target, baseHandler)

    return observe
}

reactive -> mutableHandler.js

import { useReactive } from ".";
import { statePool } from "../compile/state";
import { update } from "../render";
import { hasOwnProperty, isObject } from "../shared/utils";

const get = createGetter(),
      set = createSetter();

function createGetter() {
    return function(target, key, receiver) {
        const res = target[key]
        console.log('获取:', key, res)

        if(isObject(res)) {
            return useReactive(res)
        }

        return res
    }
}

function createSetter() {
    return function(target, key, value, receiver) {
        const isKeyExist = hasOwnProperty(target, key),
                oldValue = target[key];

        if(!isKeyExist) { // 不存在的属性 新增
            target[key] = value
        } else if(oldValue !== value) { // 存在 并且新value不等于旧value
            target[key] = value
            update(statePool, key, value)
            // update
        }

        return target
    }
}

const mutableHandler = { 
    get,
    set
}

export {
    mutableHandler
}

3、实现compile

compile -> event.js

import { reg_fnArg, reg_fnName, reg_onClick, reg_type } from "../shared/regExp"
import { randomNum, checkType } from "../shared/utils"

let eventPool = []

/**
 * 
 * eventPool
 * {
 *  mark: 唯一标识,
 *  handler: 方法,
 *  type: 'click'
 * }
 */
export function eventFormat(template) {
    return template.replace(reg_onClick, function(node, key) {
        const _mark = randomNum() // 唯一标识
        const _type = template.match(reg_type)[1] // 事件类型

        eventPool.push({
            mark: _mark,
            handler: key.trim(),
            type: _type
        })

        // 删除dom上的事件
        return `data-mark="${_mark}"`
    })
}

export function bindEvent(methods) {
    const allElements= document.querySelectorAll('*')
    let _mark = 0,
        oItem = null;

    eventPool.forEach(item => {
        for(let i = 0; i < allElements.length; i++) {
            oItem = allElements[i]
            _mark = parseInt(oItem.dataset.mark) // 找到绑定在dom上的data-mark
            
            // 相等的情况下就给其绑定事件函数
            if(item.mark === _mark) {
                oItem.addEventListener(item.type, function() {
                    const fnName = item.handler.match(reg_fnName)[1] // 找到方法名
                    const fnArg = checkType(item.handler.match(reg_fnArg)[1]) // 找到参数 并且处理参数类型
                    methods[fnName](fnArg) // 执行方法
                }, false)
            }
        }
    })
}

compile -> state.js

import { reg_html, reg_tag, reg_var } from "../shared/regExp"
import { randomNum } from "../shared/utils"


export let statePool = []

let o = 0

/**
 * statePool
 * {
 *  mark: _mark 唯一标识
 *  state: value
 * }
 */
export function stateFormat(template, state) {
    let _state = {}

    template = template.replace(reg_html, function(node, key) {
        const _mark = randomNum(),
              _tag = node.match(reg_tag)[1];

        _state.mark = _mark

        statePool.push(_state)

        _state = {}
        
        return `<${_tag} data-mark="${_mark}">{{${key}}}</${_tag}>`
    })

    template = template.replace(reg_var, function(node, key) {
       let _var = key.trim(),
            i = 0;
       // 可能会是state.obj.name 组成数组
       const _varArr = _var.split('.')

       // 循环 找到最后一个的值
       while(i < _varArr.length) {
           _var = state[_varArr[i]]
           i++
       }

       _state.state = _varArr

       statePool[o].state = _varArr

       o++

       return _var
    })

    return template
}

4、一些工具函数和正则

shared -> utils.js

import { reg_check_str, reg_str } from "./regExp"

function isObject(target) {
    return typeof target === 'object' && target !== null
}

function hasOwnProperty(target, key) {
    return Object.prototype.hasOwnProperty.call(target, key)
}

function randomNum() {
    return new Date().getTime() + parseInt(Math.random() * 10000)
}

function checkType(str) {
    // 参数带有'' "" 统一处理成 ''
    if(reg_check_str.test(str)) {
       return str.replace(reg_str, '')
    }

    // 参数是字符串的true 直接返回true或者false
    switch(str) {
        case 'true': 
            return true
        case 'false':
            return false
        default:
            break
    }

    // 其余直接处理成数字类型
    return Number(str)
}
 
export {
    isObject,
    hasOwnProperty,
    randomNum,
    checkType
}

shared -> regExp.js

const reg_onClick = /@click\=\"(.+?)\"/g // 匹配dom元素上绑定的事件
const reg_type = /@(.*?)\=/ // 匹配绑定的事件类型 click等
const reg_fnName = /^(.*?)\(/ // 匹配绑定的事件方法名
const reg_fnArg = /\((.*?)\)/ // 匹配绑定的事件的参数
const reg_check_str = /^[\'|\"].*?[\'|\"]$/ // 匹配绑定的事件的参数的参数
const reg_str = /[\'|\"]/g // 匹配参数字符串
const reg_html= /\<.*?\>\{\{(.*?)\}\}\<\/.*?>/g // 匹配html元素 包含标签和内容 完全匹配出来
const reg_tag = /\<(.*?)\>/ // 匹配标签
const reg_var = /\{\{(.*?)\}\}/g // 匹配{{}}里面的值

export {
    reg_onClick,
    reg_type,
    reg_fnName,
    reg_fnArg,
    reg_check_str,
    reg_str,
    reg_html,
    reg_tag,
    reg_var
}