【重學前端框架】Vue中的視圖更新原理(二)

Vue更新視圖的思想

Vue的響應式的核心是defineProperty,經過defineProperty來設置響應式的變量,當變量的值改變時就觸發對應的setter方法,從而調用視圖更新的方法,更新視圖。那麼Vue中究竟如何渲染視圖的呢?
html

Vue的思想:vue

  • 實例化vue時初始化數據,將data中的變量設置成響應式;
  • 渲染模版初始化頁面時,進行依賴收集,也就是會將有讀取data中的變量的dom對象存在Watcher對象中;
  • 修改數據,觸發setter,觸發對應的watch對象,調用渲染函數,從新渲染被收集的dom,更新視圖。

模擬vue實現視圖更新

<!DOCTYPE html>
<html lang="en">

<head>
    <title></title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>

    <div id="app">
        <div>
            {{message}}
        </div>
    </div>
</body>

<script>
    document.addEventListener('DOMContentLoaded', function () {
        let app = {
            el: '#app',
            data: {
                message: '頁面加載於 ' + new Date().toLocaleString()
            }
        }
        let vm = new miniVue(app)
        setTimeout(() => {
            app.data.message = '加載完成!!'
        }, 2000);
    })

    class miniVue {
        constructor(opt) {
            this.opt = opt
            this.observe(opt.data)
            let root = document.querySelector(opt.el)
            this.compile(root)
            console.log(this)
        }
        // 爲響應式對象 data 裏的每個 key 綁定一個觀察者對象
        observe(data) {
            Object.keys(data).forEach(key => {
                let obv = new Observer()
                data["_" + key] = data[key]
                // 經過 getter setter 暴露 for 循環中做用域下的 obv,閉包產生
                Object.defineProperty(data, key, {
                    get() {
                        Observer.target && obv.addSubNode(Observer.target);
                        return data['_' + key]
                    },
                    set(newVal) {
                        obv.update(newVal)
                        data['_' + key] = newVal
                    }
                })
            })
        }
        // 初始化頁面,遍歷 DOM,收集每個key變化時,隨之調整的位置,以觀察者方法存放起來    
        compile(node) {
            [].forEach.call(node.childNodes, child => {
                if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
                    let key = RegExp.$1.trim()
                    child.innerHTML = child.innerHTML.replace(new RegExp('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm'), this.opt
                        .data[key])
                    Observer.target = child
                    this.opt.data[key]
                    Observer.target = null
                } else if (child.firstElementChild)
                    this.compile(child)
            })
        }
    }
    // 常規觀察者類
    class Observer {
        constructor() {
            this.subNode = []
        }
        addSubNode(node) {
            this.subNode.push(node)
        }
        update(newVal) {
            this.subNode.forEach(node => {
                node.innerHTML = newVal
            })
        }
    }
</script>

</html>

注意以上demo:node

巧妙的使用了閉包閉包

在obsever方法中的Object.keys(data).forEach(key => {})中使用到了閉包,data中的每一個變量對應的都有一個new Observer()觀察者對象,這個對象一直被setter方法引用着。(能夠試着把遍歷data屬性的方法改爲for循環遍歷,這時候就沒有了閉包,new Observer()不會被保存在setter方法中,數據改變時觸發setter方法讀取不到new Observer()對象,最終沒法更新視圖)app

閉包的判斷依據:函數內部的函數一直對該函數的做用域保持着引用,這個引用就是閉包。閉包的好處就是當函數執行完,做用域不會被釋放,還能夠讀到其內部的數據。dom

觀察者模式解耦代碼技巧函數

編譯渲染的時候,在Observe類上定義了屬性target,做爲所有變量用來標記依賴,同時在渲染方法中又讀取了一次變量,用來觸發obsever方法中的變量對應的getter方法來存放依賴。this

image.png

image.png

相關文章
相關標籤/搜索