仿vue写一个极简版MVVM框架(一)(实现双向绑定,watch,computed)

197 阅读1分钟

为了方便实现模板编译这里用<data> </data>来代替{{}}语法,没有虚拟dom和diff算法

着重于理解vue的非侵入式双向绑定原理

文件目录

image.png

index.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>
    <script type="module">

        import MVVM from './MVVM.js'
        const vm = new MVVM({
            el: '#app',
            data: {
                name: 'wenhao',
                age: 21,
                intro: ''
            },
            methods: {
                inputAge(e) {
                    this.age = e.target.value
                },
                inputName(e) {
                    this.name = e.target.value
                },

            },
            watch: {
                name(newValue, oldValue) {
                console.log('im watch')
                }
            },
            computed: {
                intro() {
                    return this.name + this.age
                }
            }
        })


    </script>
</head>

<body>
    <div id="app">
        <p>
            我叫1<data>name</data>
        </p>
        <p>
            我的年龄2 <data>age</data>
        </p>
        <p>
            我的年龄 3<data>age</data>
        </p>
        <p>
            我的年龄 3<data>age</data>
        </p>
        <p>
            intro:<data>intro</data>
        </p>

        <input type="text" :value="age" @input="inputAge">
        <input type="text" :value="name" @input="inputName">
    </div>
</body>

</html>

MVVM.js


import Watcher from "./Watcher.js"
import observe from "./observe.js"
export default class MVVM {
    constructor({ el, data, methods, watch, computed }) {
        this.el = document.querySelector(el)
        this._data = data
        this.methods = methods
        this._watch = watch
        this._computed = computed
        this.$initData(this._data)
        this.$compile(this.el)
        this.$handleWatch()
        this.$handleComputed()

        this.__proto__ = this._data
        Object.assign(this, this.methods)
    }
    $initData(data) {
        observe(data) // 数据劫持
    }
    $compile(el) { // 模板编译
        if (el) {
            if (el.tagName == 'DATA') {
                new Watcher(this._data, el.textContent.trim(), (newVal, oldVal) => {
                    el.textContent = newVal
                }).update()
                return
            }
            const attrs = el.getAttributeNames && el.getAttributeNames()

            if (attrs && attrs.includes(':value')) {
                new Watcher(this._data, el.getAttribute(':value'), (newVal, oldVal) => { // 在数据劫持的基础上,加入回调

                    el.value = newVal
                }).update()

            }
            if (attrs && attrs.includes('@input')) {
                console.log(el);

                const prop = el.getAttribute('@input')

                el.addEventListener('input', this.methods[prop].bind(this))
            }
            if (el.nodeType === 1 && el.tagName !== 'DATA') {

                el.childNodes.forEach(element => {
                    this.$compile(element)
                });
            }
        }

    }
    $handleWatch() {
        if (this._watch)
            Object.keys(this._watch).forEach(n => {
                new Watcher(this._data, n, this._watch[n].bind(this._data))
            })
    }
    $handleComputed() {
        if (this._computed)
            Object.keys(this._computed).forEach(n => {
                new Watcher(this._data, n, this._computed[n].bind(this._data), {
                    computed: true
                })
            })
    }
}

Dep.js (依赖管理,收集watcher)

export default class Dep { // 观察者模式
    constructor() {
        this.subs = []

    }
    depend() {
        if (Dep.target && !this.subs.includes(Dep.target)) {
            this.subs.push(Dep.target)
        }
    }
    notify() {
        this.subs.forEach(n => {
            n.update()
        })
    }
}

observe.js (数据劫持)


import Dep from "./Dep.js";
function defReactive(obj, key, val) {
    const dep = new Dep()
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend();
            return val
        },
        set: function (newVal) {
            if (val != newVal) {
                val = newVal
                dep.notify()
            }
        }
    })
}

export default function observe(ob) {
    // 只是浅层的监听,实际会递归整个对象
    Object.keys(ob).forEach(n => {
        defReactive(ob, n, ob[n])
    })
}


watcher.js (依赖或中介)

import Dep from "./Dep.js";
export default class Watcher {
    constructor(target, prop, cb, options) {
        this.target = target;
        this.prop = prop;
        this.cb = cb;
        this.op = options
        if (!this.op || !this.op.computed)
            this.value = this.target[this.prop] // 不是计算属性的时候,才需要value
        this.invoke()

    }
    update() {
        if (this.op && this.op.computed) {

            this.target[this.prop] = this.cb() // 计算属性直接更新
        }
        else {
            const newVal = this.target[this.prop] // 获取新值
            this.cb(newVal, this.value)
            this.value = newVal //  然后让旧值更新
        }
    }

    invoke() {
        Dep.target = this;
        let temp = ''

        if (this.op && this.op.computed) {
            const res = this.cb()
            temp = res
        } else {
            this.target[this.prop]
        }
        Dep.target = null

        if (this.op && this.op.computed) { // 如果是计算属性就在他依赖收集后赋值一下,不能在上面直接赋值,因为 this.target[this.prop]  这个会被依赖收集
            this.target[this.prop] = temp
        }
    }
}


总结

image.png

此次着重实现是右侧部分,左侧的模板编译分用data></data>简化了,并没有实现虚拟dom和diff算法