VUE雙向綁定原理實踐

前言

近幾年,前端框架層出不窮,在技術瞬息萬變的時代裏,關注JS語言自己,探究一些框架底層實現原理也許會讓咱們走得更深更遠。下面是本身看vue源碼的一些理解和實踐,主要是對vue雙向綁定原理和觀察者模式作了一些實踐,以v-model爲例。javascript

雙向綁定原理解析

整個過程分爲如下幾步:html

  • compile:vue對template模板中進行編譯,編譯成真正的html,在編譯的過程當中對vue的指令解析
  • observe:在對template編譯的過程當中,對一些v-model,{{}}之類的指令使用在data中定義的變量來初始化,同時開啓一個對該變量的watcher,至關於開啓觀察者模式,發佈者是data中咱們定義的變量,訂閱者是咱們須要更新的dom視圖。
  • 發佈者data中的變量改變,通知訂閱者作相關操做,更新dom視圖。

主要涉及到幾個對象:前端

  • 模擬vue的原型
  • 觀察者原型watcher
  • Dep對象,封裝了對訂閱者的操做,一個data中的變量對應不一樣的watcher,這些watcher都存在在一個dep對象的數組中。

對象原型解析

1. 實現Dep

對訂閱者的操做能夠抽象成一個Dep的原型。這裏的對象屬性subscriber是訂閱者的數組,target是每次要加入subscriber中的目標訂閱者,每次都只能加一個target到subscriber中。vue

function Dep(cb) {
        this.subscriber = []
        this.target = null
    }
    Dep.prototype = {
        addSub(sub) {
            // 加入到訂閱者數組中
            this.subscriber.push(sub)
        },
        notify() {
            // 
            this.subscriber.forEach((item) => {
                item.update()
            })
        }
    }
複製代碼

2. 實現watcher

而後,咱們再來抽象一個watcher對象。exp是咱們在data中定義的變量,cb是訂閱者要執行的回調函數。get獲取的是變量的值,update對應的當變量值發生更新時,執行訂閱者的cb函數。初始化一個watcher的時候,調用get可把該watcher加入到訂閱者的數組裏。java

function Watcher(vm, exp, cb) {
        this.vm = vm
        this.exp = exp
        this.cb = cb
        // 添加到訂閱者列表中
        this.value = this.get()
    }
    Watcher.prototype = {
        get() {
            Dep.target = this
            let value = this.vm.$data[this.exp]
            Dep.target = null
            return value
        },
        update() {
            let oldVal = this.value
            let newVal = this.vm.$data[this.exp]
            if (newVal !== oldVal ) {
                console.log('更新', this.exp)
                this.value = newVal
                this.cb.call(this.vm, newVal)
            }
        }
    }
複製代碼

3. 實現vue原型

最後,咱們先模擬一個vue的原型。每個data中的變量對應一個Dep對象,用於收集這個變量所對應的全部訂閱者。這裏,使用了原生JS中的Object.defineProperty方法,當get的時候把相關訂閱者加入到dep對象中,當set的時候通知訂閱者執行相關回調。node

function Vue(options) {
        let vm = this
        vm._init(options)

        //開啓觀察者模式觀察數據變化
        vm._observe(options.data)

        // 編譯dom
        vm._compile()
    }
    Vue.prototype._init = function(options) {
        var vm = this
        
        vm.$el = options.el
        vm.$data = options.data 
    }
    Vue.prototype._observe = function(data) {
        Object.entries(data).forEach(([key, value]) => {
            var dep = new Dep()
            let property = Object.getOwnPropertyDescriptor(data, key)

            if (property && !property.configurable) {
                return
            }
            let getter = property && property.get
            let setter = property && property.set

            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get() {
                    let val = getter ? getter.call(data) : value
                    // get時候添加訂閱者
                    if (Dep.target) {
                        dep.addSub(Dep.target)
                    }
                    
                    return val
                },
                set(newValue) {
                    let val = getter ? getter.call(data) : value
                    // 髒檢查,排除NaN !== NaN
                    if (newValue === val || (newValue !== newValue)) {
                        return
                    }
                    if (setter) {
                        setter.call(data, newValue)
                    } else {
                        value = newValue
                    }
                    // 通知訂閱者
                    dep.notify()
                }
            })
        })
    }
    Vue.prototype._compile = function() {
        let rootNode = document.querySelector(this.$el)
        let compile = (rootNode) => {
            if (rootNode.childNodes) {
                Array.prototype.forEach.call(rootNode.childNodes, (node) => {
                    if (node.attributes && node.attributes.hasOwnProperty('v-model')) {
                        compileVmodel(this, node)
                    }
                    if (/{{.*}}/.test(node.innerHTML)) {
                        compileBrace(this, node)
                    }
                    if (node.childNodes) {
                        compile(node)
                    }
                }) 
            }
        }
        compile(rootNode)
    }
    function compileVmodel(vm, node) {
        // 檢測到有v-model屬性,則添加對應watcher
        let exp = node.getAttribute('v-model')
        new Watcher(vm, exp, (val) => {
            node.setAttribute('v-model', val)
        })
        // 監聽input事件
        node.addEventListener('input', () => {
            if (node.value !== vm.$data[exp]) {
                vm.$data[exp] = node.value
            }
        }, false)
    }
複製代碼

這裏用到了兩個函數compileVmodel和compileBrace,只是做爲模擬,實際vue解析的過程當中用到了AST。數組

function compileVmodel(vm, node) {
        // 檢測到有v-model屬性,則添加對應watcher
        let exp = node.getAttribute('v-model')
        new Watcher(vm, exp, (val) => {
            node.setAttribute('v-model', val)
        })
        // 監聽input事件
        node.addEventListener('input', () => {
            if (node.value !== vm.$data[exp]) {
                vm.$data[exp] = node.value
            }
        }, false)
    }
    function compileBrace(vm, node) {
        // 解析{{}}中值
        let exp = node.innerHTML.match(/{{(.*)}}/)[1]
        console.log('compileBrace', exp)
        new Watcher(vm, exp, (val) => {
            console.log('新的值:', node)
            // let innerHTML = node.innerHTML.replace(/{{.*}}/g, val)
            node.textContent = val
            // console.log(innerHTML)
        })
    }
複製代碼

驗證和實踐

以上就完成了對vue雙向綁定原理的簡單建模,如今寫段代碼來實踐驗證下bash

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>vue雙向綁定原理實踐</title>
</head>
<body>
    <div id="app">
        <input type="testVmodel" name="testVmodel" v-model="inputValue">
        <p>{{inputValue}}</p>
    </div>

    <script type="text/javascript"> 
        // 這裏是引入上面的原型

        window.onload = (function(window) {
            let app = new Vue({
                el: '#app',
                data: {
                    inputValue: '初始化inputValue值'
                }
            })
            // 開啓觀察者模式監測 inputValue變化
            new Watcher(app, 'inputValue', function(value) {
                let inputEl = document.querySelector('input')
                inputEl.value = value
            })
            app.$data.inputValue = '測試更新'
            setTimeout(() => {
                app.$data.inputValue = '測試更新2'
            }, 1000)
        })(window)
    </script>
</body>
</html>
複製代碼

總結

以上只是本身簡單模擬了下vue的雙向綁定原理渲染過程,便於你們去理解。實際的vue源碼中比這個要詳細和複雜得多啦,尤爲是template模板解析和渲染這塊,用了AST的思想,有空能夠再研究下這個。前端框架

相關文章
相關標籤/搜索