Use ES6 class to imitate Vue to write a two-way binding

Original address: Use ES6 class to imitate Vue to write a two-way binding

Click to try it online

The final effect is as follows:
image

Constructor

Construct a TinyVue object, including basic el, data, methods

class TinyVue{
    constructor({el, data, methods}){
        this.$data = data
        this.$el = document.querySelector(el)
        this.$methods = methods
        // 初始化
        this._compile()
        this._updater()
        this._watcher()
    }
}

compiler (compile)

@click for resolving click events of v-models and elements bound to input boxes and dropdown boxes .
First create a function to load the event:

// el为元素tagName,attr为元素属性(v-model,@click)
_initEvents(el, attr, callBack) {
    this.$el.querySelectorAll(el).forEach(i => {
        if(i.hasAttribute(attr)) {
            let key = i.getAttribute(attr)
            callBack(i, key)
        }
    })
}

Load input box event

this._initEvents('input, textarea', 'v-model', (i, key) => {
    i.addEventListener('input', () => {
        Object.assign(this.$data, {[key]: i.value})
    })
})

Load selection box event

this._initEvents('select', 'v-model', (i, key) => {
    i.addEventListener('change', () => Object.assign(this.$data, {[key]: i.options[i.options.selectedIndex].value}))
})

Load click event

The click event corresponds to the event in methods

this._initEvents('*', '@click', (i, key) => {
    i.addEventListener('click', () => this.$methods[key].bind(this.$data)())
})

View updater (updater)

Similarly, first create public functions to process views in different elements, including the value of input, textarea, the selection value of select, and the innerHTML of div

_initView(el, attr, callBack) {
    this.$el.querySelectorAll(el, attr, callBack).forEach(i => {
        if(i.hasAttribute(attr)) {
            let key = i.getAttribute(attr),
                data = this.$data[key]
            callBack(i, key, data)
        }
    })
}

Update input box view

this._initView('input, textarea', 'v-model', (i, key, data) => {
    i.value = data
})

Update the select box view

this._initView('select', 'v-model', (i, key, data) => {
    i.querySelectorAll('option').forEach(v => {
        if(v.value == data) v.setAttribute('selected', true)
        else v.removeAttribute('selected')
    })
})

update innerHTML

The implementation method here is a bit low, just think of regular replacement {{text}}

let regExpInner = /\{{ *([\w_\-]+) *\}}/g
this.$el.querySelectorAll("*").forEach(i => {
    let replaceList = i.innerHTML.match(regExpInner) || (i.hasAttribute('vueID') && i.getAttribute('vueID').match(regExpInner))
    if(replaceList) {
        if(!i.hasAttribute('vueID')) {
            i.setAttribute('vueID', i.innerHTML)
        }
        i.innerHTML = i.getAttribute('vueID')
        replaceList.forEach(v => {
            let key = v.slice(2, v.length - 2)
            i.innerHTML = i.innerHTML.replace(v, this.$data[key])
        })
    }
})

watcher

Update views after data changes

_watcher(data = this.$data) {
    let that = this
    Object.keys(data).forEach(i => {
        let value = data[i]
        Object.defineProperty(data, i, {
            enumerable: true,
            configurable: true,
            get: function () {
                return value;
            },
            set: function (newVal) {
                if (value !== newVal) {
                    value = newVal;
                    that._updater()
                }
            }
        })
    })
}

use

<div id="app">
    <input type="text" v-model="text1"><br>
    <input type="text" v-model="text2"><br>
    <textarea type="text" v-model="text3"></textarea><br>
    <button @click="add">加一</button>
    <h1>您输入的是:{{text1}}+{{text2}}+{{text3}}</h1>
    <select v-model="select">
        <option value="volvo">Volvo</option>
        <option value="saab">Saab</option>
    </select>
    <select v-model="select">
        <option value="volvo">Volvo</option>
        <option value="saab">Saab</option>
    </select>
    <h1>您选择了:{{select}}</h1>
</div>
<script src="./TinyVue.js"></script>
<script>
    let app = new TinyVue({
        el: '#app',
        data: {
            text1: 123,
            text2: 456,
            text3: '文本框',
            select: 'saab'
        },
        methods: {
            add() {
                this.text1 ++
                this.text2 ++
            }
        }
    })
</script>

All TinyVue code

class TinyVue{
    constructor({el, data, methods}){
        this.$data = data
        this.$el = document.querySelector(el)
        this.$methods = methods
        this._compile()
        this._updater()
        this._watcher()
    }
    _watcher(data = this.$data) {
        let that = this
        Object.keys(data).forEach(i => {
            let value = data[i]
            Object.defineProperty(data, i, {
                enumerable: true,
                configurable: true,
                get: function () {
                    return value;
                },
                set: function (newVal) {
                    if (value !== newVal) {
                        value = newVal;
                        that._updater()
                    }
                }
            })
        })
    }
    _initEvents(el, attr, callBack) {
        this.$el.querySelectorAll(el).forEach(i => {
            if(i.hasAttribute(attr)) {
                let key = i.getAttribute(attr)
                callBack(i, key)
            }
        })
    }
    _initView(el, attr, callBack) {
        this.$el.querySelectorAll(el, attr, callBack).forEach(i => {
            if(i.hasAttribute(attr)) {
                let key = i.getAttribute(attr),
                    data = this.$data[key]
                callBack(i, key, data)
            }
        })
    }
    _updater() {
        this._initView('input, textarea', 'v-model', (i, key, data) => {
            i.value = data
        })
        this._initView('select', 'v-model', (i, key, data) => {
            i.querySelectorAll('option').forEach(v => {
                if(v.value == data) v.setAttribute('selected', true)
                else v.removeAttribute('selected')
            })
        })
        let regExpInner = /\{{ *([\w_\-]+) *\}}/g
        this.$el.querySelectorAll("*").forEach(i => {
            let replaceList = i.innerHTML.match(regExpInner) || (i.hasAttribute('vueID') && i.getAttribute('vueID').match(regExpInner))
            if(replaceList) {
                if(!i.hasAttribute('vueID')) {
                    i.setAttribute('vueID', i.innerHTML)
                }
                i.innerHTML = i.getAttribute('vueID')
                replaceList.forEach(v => {
                    let key = v.slice(2, v.length - 2)
                    i.innerHTML = i.innerHTML.replace(v, this.$data[key])
                })
            }
        })
    }
    _compile() {
        this._initEvents('*', '@click', (i, key) => {
            i.addEventListener('click', () => this.$methods[key].bind(this.$data)())
        })
        this._initEvents('input, textarea', 'v-model', (i, key) => {
            i.addEventListener('input', () => {
                Object.assign(this.$data, {[key]: i.value})
            })
        })
        this._initEvents('select', 'v-model', (i, key) => {
            i.addEventListener('change', () => Object.assign(this.$data, {[key]: i.options[i.options.selectedIndex].value}))
        })
    }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325473468&siteId=291194637