Simple realization of two-way binding, life cycle and calculated properties in Vue! ! !

Effect picture

How to achieve a two-way binding is already a common theme, and recently wrote a two-way binding demo, the final presentation as follows (demo ugly point Wuguai):

Insert picture description here

Click demo preview, you can preview online

Preface

Organize Favorites recently found myself manually to achieve a simple two-way data binding mvvm this blog, it is very easy to understand examples to explain the Vue responsive core - two-way binding. After reading it, I also wrote a two-way binding and add a few simple functions:

  • life cycle
  • Methods and events
  • Calculated attributes

Now I will briefly describe the realization of these functions.

What is MVVM

Turning to two-way binding would have to mention the MVVM pattern, it Yes Model-View-ViewModel shorthand.

In the front-end page, the Model is represented by a pure JavaScript object, and the View is responsible for displaying, and the two are separated to the greatest extent.

Associating Model and View is ViewModel. The ViewModel is responsible for synchronizing the data of the Model to the View for display, and it is also responsible for synchronizing the modification of the View back to the Model.

Insert picture description here

In the application, we can Vue <div id="app"></div>template within the considered View , the datareturn value of the function viewed as Model , and the ViewModel is a two-way binding function, it can sense changes in data, used to connect the View and the Model .

What does the ViewModel in Vue do? Let me give you the simplest example:

<div id="app">
    <h1>{
   
   { msg }}</h1>
    <input type="text" v-model="msg" />
</div>
<script>
	new Vue({
        el: '#app',
        data () {
            return { msg: 'Hello world' }
        }
    })
</script>

Will be Hello worldassigned to the input and h1 tags, here is the initialization template, Compile do. And by modifying the value of msg, then the h1 and input values ​​are changed, and entering other values ​​in input to change the values ​​of h1 and msg, the realization of such a function is the realization of a two-way binding. A simple Vue two-way binding can be split into the following functions:

  • Compile parsing template initialization page
  • Dep relies on collection and notification
  • Watcher provides dependencies and updates the view
  • Observer listens to data changes and notifies view updates

Insert picture description here

The above picture is the two-way binding flow chart I drew. It should be much more intuitive than the text. The code is directly below.

options

You can look at the final calling code first. When we try to simulate an existing framework, we can use the existing results to infer how our code is designed.

new Vue({
    
    
    el: '#app',
    data() {
    
    
        return {
    
    
            foo: 'hello',
            bar: 'world'
        }
    },
    computed: {
    
    
        fooBar() {
    
    
            return this.foo + ' ' + this.bar
        },
        barFoo() {
    
    
            return this.bar + ' ' + this.foo
        }
    },
    mounted() {
    
    
        console.log(document.querySelector('h1'));
        console.log(this.foo);
    },
    methods: {
    
    
        clickHandler() {
    
    
            this.foo = 'hello'
            this.bar = 'world'
            console.log('实现一个简单事件!')
            console.log(this)
        }
    }
})

Vue class

The above code 1:1 restores Vue's call writing method. It is obvious that it is natural to write a Vue class and pass an options object as a parameter.

class Vue {
    
    
    constructor(options) {
    
    
        this.$options = options
        this.$el = document.querySelector(options.el)
        // 缓存data中的key,用来数据劫持
        // (ps: Vue 将 options.data 中的属性挂在 Vue实例 上, Object.defineProperty 劫持的其实是 Vue实例 上的属性, options.data 里的数据初始化之后应该用处不大)
        this.$depKeys = Object.keys({
    
    ...options.data(), ...options.computed})
        // 计算属性的依赖数据
        this._computedDep = {
    
    }
        this._addProperty(options)
        this._getComputedDep(options.computed)
        this._init()
    }

    _init() {
    
    
        observer(this)
        new Compile(this)
    }

    // 获取计算属性依赖
    _getComputedDep(computed) {
    
    
        Object.keys(computed).forEach(key => {
    
    
            const computedFn = computed[key]
            const computedDeps = this._getDep(computedFn.toString())

            computedDeps.forEach(dep => {
    
    
                if (!this._computedDep[dep]) {
    
    
                    this._computedDep[dep] = {
    
    
                        [key]: computed[key]
                    }
                } else {
    
    
                    Object.assign(this._computedDep[dep], {
    
    
                        [key]: computed[key]
                    })
                }
            })
        })
    }

    _getDep(fnStr) {
    
    
        const NOT_REQUIRED = ['(', ')', '{', '}', '+', '*', '/', '\'']
        return fnStr.replace(/[\r\n ]/g, '')
            .split('')
            .filter(item => !NOT_REQUIRED.includes(item))
            .join('')
            .split('return')[1]
            .split('this.')
            .filter(Boolean)
    }

    // 将 data 和 methods 中的值注入Vue实例中(实现在方法或生命周期等能直接用 this[key] 来取值)
    _addProperty(options) {
    
    
        const {
    
    computed, data, methods} = options
        const _computed = {
    
    }
        Object.keys(computed).forEach(key => {
    
    
            _computed[key] = computed[key].call(data())
        })

        const allData = {
    
    ...data(), ...methods, ..._computed}
        Object.keys(allData).forEach(key => {
    
    
            this[key] = allData[key]
        })
    }
}

Vue class calls observerand Compileto perform an initialization operation, the necessary parameters collected Vue instance hanging in the subsequent operation easy. Here I have added data, computed, and methods to the Vue instance. Why I did this will be mentioned later.

Compile

// 编译模板
class Compile {
    
    
    constructor(vm) {
    
    
        this.vm = vm
        this._init()
    }

    _init() {
    
    
        // 搬来 Vue 中匹配 {
    
    { 插值 }} 的正则
        const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/;
        // 获取主容器
        const container = this.vm.$el
        // 创建虚拟节点
        const fragment = document.createDocumentFragment()
        // 只取元素节点
        let firstChild = container.firstElementChild
        while (firstChild) {
    
    
            // 这里 append 一个,container 就会少一个 子元素,若没有子元素则会返回 null
            fragment.appendChild(firstChild)
            firstChild = container.firstElementChild
        }
        fragment.childNodes.forEach(node => {
    
    
            // 将属性节点(ArrayLike)转换为数组
            [...node.attributes].forEach(attr => {
    
    
                // 匹配 v-model 指令
                if (attr.name === 'v-model') {
    
    
                    const key = attr.value
                    node.value = this.vm[key]

                    new Watcher(this.vm, key, val => {
    
    
                        node.value = val
                    })

                    node.addEventListener('input', e => {
    
    
                        // input 事件触发 set 方法,并通知 Watcher 实例操作变更dom
                        this.vm[key] = e.target.value
                    })
                }
                // 匹配 @click 绑定点击事件
                if (attr.name === '@click') {
    
    
                    // 使用 bind 将此函数内部 this 改为 Vue实例
                    node.addEventListener('click', this.vm[attr.value].bind(this.vm))
                }
            })

            // 匹配双花括号插值(textContent取赋值 比 innerText 好一点)
            if (node.textContent && defaultTagRE.test(node.textContent)) {
    
    
                console.log(node.textContent);
                const key = RegExp.$1.trim()
                // 替换 {
    
    {}} 后的文本,用于初始化页面
                const replaceTextContent = node.textContent.replace(defaultTagRE, this.vm[key])
                // 移除 {
    
    {}} 后的文本,用于响应性更新
                const removeMustache = node.textContent.replace(defaultTagRE, '')
                node.textContent = replaceTextContent

                new Watcher(this.vm, key, val => {
    
    
                    node.textContent = removeMustache + val
                })
            }
        })

        // 将 虚拟节点 添加到主容器中(这里可以将虚拟节点理解为 Vue 中的 template 标签,只起到一个包裹作用不会存在真实标签)
        this.vm.$el.appendChild(fragment)
        // 此处定义 mounted 生命周期
        typeof this.vm.$options.mounted === 'function' && this.vm.$options.mounted.call(this.vm)
    }
}

If you want to immediately see the results, then, to write Compileto be sure. I’ll post the final complete code here, so there will be more code. If you split it into detail, it will have the following functions:

  • Analytical #appcontent, the elements { {}}or v-modelinto the actual value assigned, and appended to a virtual node. About Why createDocumentFragmentcreate a virtual node method, the first advantage is the convenience, and the second benefit is to reduce the performance overhead. In the browser, every time you add and delete elements will cause the page to reflow, it needs to recalculate the position of other elements. If there are hundreds of elements sequentially added to the dom will cause redraw hundreds of times, but add all elements to the virtual node in the redraw would lead to a reflux.
  • Generating Watcherinstance, it caches the dependent key, and a method of updating dom added data, is to rely on the value at the time was changed to update dom.

Observer

function observer(vm) {
    
    
    const dep = new Dep()
    const {
    
    _computedDep} = vm
    vm.$depKeys.forEach(key => {
    
    
        let value = vm[key]
        Object.defineProperty(vm, key, {
    
    
            enumerable: true,
            configurable: true,
            get() {
    
    
                if (Dep.target) {
    
    
                    // 添加订阅者-Watcher实例
                    dep.add(Dep.target)
                }
                return value
            },
            set(newVal) {
    
    
                value = newVal
                // (ps:可以在此处根据 key 值通知对应的 Watcher 进行更新)
                dep.notify()

                Object.keys(_computedDep).forEach(computedDep => {
    
    
                    // 在 set 中匹配对应的依赖项更新对应的计算属性
                    if (key === computedDep) {
    
    
                        for (let getter in _computedDep[key]) {
    
    
                            vm[getter] = _computedDep[key][getter].call(vm)
                        }
                    }
                })
            }
        })
    })
}

observerIn fact, a listener, here used to monitor changes and computed data and update notification dom, so here Object.definePropertyis to achieve two-way binding Vue is a cornerstone. It is really difficult to automatically collect dependencies on calculated calculated attributes. The weird and weird conversion in the Vue source code is really hard to stand, so I had to write a simple and rude way to achieve it. The calculated attribute is essentially the return value of a method. All my implementation principles here are: a dependent key corresponds to multiple calculated methods, detects the update of the dependent key, and triggers the calculated method at the same time.

DepAnd the Watchercode is relatively simple nothing to talk about, is not posted, later in the article will be labeled source code.

How to realize the life cycle?

If you asked me how the life cycle in Vue is realized before? I really don’t know, I rarely read interview questions. When I first came into contact with Vue, I guessed that its life cycle might be realized by some js api that I didn't know. In fact, before or after some js code is executed, a life cycle function is called to form a life cycle. In Compile, after the completion of parsing the template to fill dom, call the mountedfunction will be achieved mounted lifecycle.

How to easily retrieve data values ​​and calculated attributes?

In Mothods, life cycles, and are computed in computing and data acquisition required properties directly in Vue arbitrary use thiscan be retrieved all values. I wrote the above sentence by the existing results to infer how our code design , the most simple and crude way to hang on to these data Vue example is solved, Vue in fact, do so.

to sum up

Indeed my code in comparison to online examples may be relatively long, because the addition of a few extra features, it's just me exploring other functions implemented for the Vue.

At the end of 2020 and the beginning of 2021, I felt just one word—"busy". I don't have time to blog or do other things when I am busy, so I don't have time to write this blog from the shallower to the deeper. It is easy to understand. The small functions are divided and written little by little, and the length is more than 10 times. It is also considered to be written as soon as possible. It is the only article produced in January 2021.

Source code

Click to see index.html

reference

Guess you like

Origin blog.csdn.net/dizuncainiao/article/details/113482795