Vue2.x響應式原理

RisingSunBlogs發表於2021-08-15

一、回顧Vue響應式用法

​ vue響應式,我們都很熟悉了。當我們修改vue中data物件中的屬性時,頁面中引用該屬性的地方就會發生相應的改變。避免了我們再去操作dom,進行資料繫結。

二、Vue響應式實現分析

對於vue的響應式原理,官網上給了出文字描述 https://cn.vuejs.org/v2/guide/reactivity.html

vue內部主要是通過資料劫持和觀察者模式實現的

資料劫持:

vue2.x內部使用Object.defineProperty https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

vue3.x內部使用的Proxy https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

觀察者模式:https://juejin.cn/post/6995865134132363295

內部成員示意圖

各個成員的功能

Vue:

  • 把data中的成員注入到Vue例項中,並把data中的成員轉換為getter和setter

Observer:

  • 對data物件中的簡單型別資料及物件進行監聽,當資料發生變化時通知Dep

Compiler:

  • 解析每個元素中的指令/差值表示式,並替換成相應的資料

Dep:

  • 觀察者模式中的通知者,新增觀察者,當資料變化時通知觀察者

Watcher:

  • 每個引用data中的屬性的地方都有一個watcher物件,負責更新檢視

附:data物件中的屬性充當被觀察者,引用data物件中屬性的地方充當觀察者

三、Vue響應式原始碼實現

Vue物件實現

功能

  • 負責接受初始化的引數
  • 把data中的屬性注入到data例項,轉換成getter和setter
  • 呼叫Observer監聽data中所有屬性的變化
  • 呼叫compiler解析指令、差值表示式.
class Vue{
    constructor(options){
        // 1、通過屬性儲存穿進來的屬性
        this.$options= options||{};
        this.$data= options.data||{};
        this.$el = typeof options.el ==='string' ? document.querySelector(options.el) : options.el;
        // 2、把data引數中的資料轉換為getter和setter 掛載到Vue例項上
        this._proxyData(this.$data)
        // 3、呼叫observe物件監視data資料的變化
        new Observer(this.$data)
        // 4、呼叫compiler物件渲染頁面
        new Compiler(this)
    }
    _proxyData(data){
        if (data&&Object.keys(data).length>0){
             for (const key in data) {
                Object.defineProperty(this,key,{
                    configurable:true,
                    enumerable:true,
                    get(){
                        return data[key]
                    },
                    set(value){
                        if (data[key]===value) {
                            return;
                        }
                        data[key]=value;
                    }
                })
             }
        }
    }
}

Observer物件實現

功能

  • 把data選項中的屬性進行資料劫持
  • data中的某個屬性也是物件的話,進行遞迴轉換成響應式物件
  • 資料變化傳送通知
//資料劫持
class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) { 
        //1、判斷data是否是物件    
        if (!data || typeof data !== 'object') {     
            return
        }
        //2、迴圈呼叫defineReactive進行資料劫持
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive(obj, key, val) {
        //建立通知者
        const dep = new Dep()
        //使用walk把引用物件中的屬性變成響應式的
        this.walk(val)
        const that=this;
        Object.defineProperty(obj, key, {
            configurable: true,
            enumerable: true,
            get() {
                //通知者收集觀察者
                Dep.target && dep.addSub(Dep.target)
                return val;
            },
            set(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                that.walk(newVal)
                //被觀察者發生變化的時候,通知者物件給每個觀察者傳送通知
                dep.notify()
            }
        })
    }
}

Compile物件實現

功能

  • 負責編譯模板,解析指令、差值表示式
  • 負責頁面首次渲染
  • 當資料發生改變後,負責重新渲染檢視
//編譯器
class Compiler {
    constructor(vm) {
        this.el = vm.$el;
        this.vm = vm;
        this.compile(this.el)
    }
    //編譯模板 判斷節點是文字節點還是元素節點
    compile(el) {
        let childNodes = el.childNodes;
        //處理第一層子節點
        Array.from(childNodes).forEach(node => {
            if (this.isTextNode(node)) {
                this.compileText(node)
            } else if (this.isElementNode(node)) {
                this.compileElement(node)
            }
            //如果當前節點還有子節點  遞迴呼叫編譯指令
            if (node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }

    //編譯元素節點,處理指令
    compileElement(node) {  
        //遍歷所有的指令
        Array.from(node.attributes).forEach(attr => {
            //判斷是不是指令節點
            if (this.isDirective(attr.name)) {
                const nodeName = attr.name;
                const key = attr.nodeValue;
                const directive = nodeName.substr(2)
                this.updater(directive,node,key)
            }
        }) 
    }
    updater(directive,node,key){
        const updaterFn = this[directive+"Updater"]
        updaterFn && updaterFn.call(this,node,this.vm[key],key)
    }
    //v-text
    textUpdater(node,value,key){
        node.textContent=value
        //使用v-text表示式的地方就是一個觀察者
        new Watcher(this.vm,key,newValue => {
            node.textContent = newValue
        })
    }
    //v-model
    modelUpdater(node,value,key){
        node.value =value
        //使用v-model表示式的地方就是一個觀察者
        new Watcher(this.vm,key,newValue => {
            node.value = newValue
        })
      //實現雙向繫結
        node.addEventListener('input',()=>{
            this.vm[key] = node.value
        })
    }
    //v-html
    htmlUpdater(node,value,key){
        node.innerHTML = value
        //使用v-html表示式的地方就是一個觀察者
        new Watcher(this.vm,key,newValue => {
            node.innerHTML = newValue
        })
    }

    //處理差值表示式
    compileText(node) {
        //匹配差值表示式的正則
        let reg = /\{\{(.+?)\}\}/
        //用正則匹配node的textContent,如果匹配到了 就替換
        if (reg.test(node.textContent)) {
            //獲取插值表示式的key
            let key = RegExp.$1;
            let value = node.textContent;
            node.textContent = value.replace(reg, this.vm[key])

            //使用差值表示式的地方就是一個觀察者
            new Watcher(this.vm,key,newValue => {
                node.textContent = newValue
            })
        }
    }

    //是否是指令
    isDirective(attrName) {
        return attrName.startsWith('v-')
    }

    //是否是文字節點
    isTextNode(node) {
        return node.nodeType === 3
    }

    //是否是元素
    isElementNode(node) {
        return node.nodeType === 1
    }
}

Dep物件實現

功能

  • 收集依賴,新增觀察者
  • 通知所有觀察者
//通知者類
class Dep {
    constructor() {
        //儲存觀察者
        this.subs = []
    }

    /**
     * 收集觀察者
     */
    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }

    /**
     * 通知觀察者改變狀態
     */
    notify() {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}

Watcher物件實現

功能

  • 當資料變化時,Dep通知所有Watcher例項更新檢視
  • 自身例項化的時候往Dep物件中新增自己
//觀察者類
class Watcher {
    constructor (vm,key,cb) {
        //Vue例項
        this.vm =vm;
        // data中的key物件
        this.key =key;
        // 更新檢視的回撥函式
        this.cb = cb
        //把當前觀察者例項存放在Dep的target靜態屬性中
        Dep.target =this
        //觸發Observe的getter方法,把當前例項存放在Dep.subs中
        //data中key對應的舊值
        this.oldValue = this.vm[this.key]
        Dep.target = null
    }
    //每個觀察者都有一個update方法來改變狀態
    update(){
        const newValue = this.vm[this.key]
        if ( this.newValue === this.oldValue ) {
            return
        }
        this.cb(newValue)
    }
}

測試

<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>index</title>
    <script src="./js/dep.js"></script>
    <script src="./js/watcher.js"></script>
    <script src="./js/compiler.js"></script>
    <script src="./js/observer.js"></script>
    <script src="./js/vue.js"></script>
</head>
<body>
    <div id="app">
        <h1>差值表示式</h1>
        <h3>{{msg}}</h3>
        <h3>{{count}}</h3>
        <h1>v-text</h1>
        <div v-text='msg'></div>
        <h1>v-model</h1>
        <input type="text" v-model="msg" attr="msg">
        <input type="text" v-model="count">
        <h1>v-html</h1>
        <div v-html="htmlText"></div>
    </div>
    <script>
        let vm = new Vue({
            el:"#app",
            data:{
                msg:'資訊',
                count:'數量',
                person:{name:'張三'},
                htmlText:"<div style='color:red'>你好</div>"
            }
        })
    </script>
</body>

相關文章