欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一篇文章帶你徹底搞懂VUE響應(yīng)式原理

 更新時(shí)間:2022年06月28日 09:03:19   作者:Dddusty  
這篇文章主要介紹了一篇文章帶你徹底搞懂VUE響應(yīng)式原理,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可任意參考一下,需要的朋友可以參考下

首先上圖,下面這張圖,即為MVVM響應(yīng)式原理的整個(gè)過(guò)程圖,我們本篇都是圍繞著這張圖進(jìn)行分析,所以這張圖是重中之重。

響應(yīng)式原理圖

 一臉懵逼?沒(méi)關(guān)系,接下來(lái)我們將通過(guò)創(chuàng)建一個(gè)簡(jiǎn)單的MVVM響應(yīng)系統(tǒng)來(lái)一步步了解這個(gè)上圖中的全過(guò)程。全文分為兩大塊,首先介紹實(shí)例模板的編譯過(guò)程,然后詳細(xì)介紹響應(yīng)式,這里先介紹編譯是為了給介紹響應(yīng)式奠定基礎(chǔ)。

編譯

我們把我們創(chuàng)建的這個(gè)微型響應(yīng)系統(tǒng)命名為miniVue,我們按照平常使用Vue的模式,首先創(chuàng)建一個(gè)miniVue的實(shí)例。

<scirpt>
    const vm = new miniVue({
 ? ? ? ?el: '#app',
 ? ? ? ?data: {
 ? ? ? ? ? ?obj: {
 ? ? ? ? ? ? ? ?name: "miniVue",
 ? ? ? ? ? ? ? ?auth: 'xxx'
 ? ? ? ? ?  },
 ? ? ? ? ? ?msg: "this is miniVue",
 ? ? ? ? ? ?htmlStr: "<h3>this is htmlStr</h3>"
 ? ? ?  },
 ? ? ? ?methods: {
 ? ? ? ? ? ?handleClick() {
 ? ? ? ? ? ? ? ?console.log(this);
 ? ? ? ? ?  }
 ? ? ?  }
 ?  });
</scirpt>

我們根據(jù)這個(gè)實(shí)例,我們可以創(chuàng)建出miniVue的類,這個(gè)類中我們肯定要保存該實(shí)例所綁定的DOM以及數(shù)據(jù)對(duì)象data。然后我們要開(kāi)始解析模板,即解析我們所綁定的DOM

class miniVue {
    constructor(options) {
        this.$el = options.el
 ? ? ? ?this.$data = options.data
 ? ? ? ?this.$options = options
 ?  }
 ? ?if(this.$el) {
        // 解析模板 to Compile
 ?  }
}

這里我們來(lái)創(chuàng)建一個(gè)compile類來(lái)進(jìn)行解析模板的操作

創(chuàng)建compile類

Compile類是用來(lái)解析模板的,所以肯定要傳入要解析的DOM。拿到DOM后直接操作這個(gè)DOM會(huì)導(dǎo)致頁(yè)面頻繁的回流和重繪,所以我們把這個(gè)DOM放到一個(gè)文檔碎片中,然后操作這個(gè)文檔碎片。操作這個(gè)文檔碎片的過(guò)程中我們需要獲取到數(shù)據(jù)對(duì)象data中的屬性來(lái)填充一些節(jié)點(diǎn)的內(nèi)容,所以我們還需要傳入實(shí)例對(duì)象。最后將操作好的文檔碎片追加到原本的DOM上。

class Compile {
 ? ?constructor(el, vm) {
 ? ? ? ?// 判斷的原因是因?yàn)閭魅氲膃l有可能是DOM,也有可能是選擇器例如‘#app'
        this.el = this.isElementNode(el) ? el : document.querySelector(el)
 ? ? ? ?this.vm = vm
 ? ? ? ?// 新建文檔碎片存儲(chǔ)DOM
 ? ? ?  const fragment = this.toFragment(this.el)

 ? ? ? ?// 操作文檔碎片 to handle fragment

 ? ? ? ?// 將操作好的文檔碎片追加到原本的DOM上面
 ? ? ? ?this.el.appendChild(fragment)
 ?  }

 ? ?// 判斷是否為元素節(jié)點(diǎn)
 ? ?isElementNode(node) {
 ? ? ? ?return node.nodeType === 1
 ?  }
 ? ?// dom碎片化
 ? ?toFragment(el) {
 ? ? ? ?const f = document.createDocumentFragment()
        f.appendChild(el.clone(true))
 ?  }
}
// 上面的miniVue實(shí)例相應(yīng)的改為
class miniVue {
    constructor(options) {
        this.$el = options.el
 ? ? ? ?this.$data = options.data
 ? ? ? ?this.$options = options
 ?  }
 ? ?if(this.$el) {
        // 解析模板 to Compile
 ? ? ? ?new Compile(this.$el, this) // 這里的this就是miniVue實(shí)例
 ?  }
}

操作fragment

操作保存好的文檔碎片,我們可以專門定義一個(gè)函數(shù),然后把文檔碎片通過(guò)參數(shù)傳入進(jìn)來(lái)。

操作文檔碎片我們又可以分為兩步。因?yàn)獒槍?duì)文本節(jié)點(diǎn)元素節(jié)點(diǎn),我們需要進(jìn)行不同的操作,所以我們?cè)诒闅v所有節(jié)點(diǎn)后的第一步應(yīng)該先判斷它是元素節(jié)點(diǎn)還是文本節(jié)點(diǎn)。

handleFragment(fragment) {
 ? ?// 獲取文檔碎片的子節(jié)點(diǎn)
    const childNodes =  fragment.childNodes
 ? ?// 遍歷所有子節(jié)點(diǎn)
 ?  [...childNodes].forEach((child) => {
        if(this.isElementNode(child)) {
 ? ? ? ? ? ?// 元素節(jié)點(diǎn)
 ? ? ? ? ? ?this.compileElement(child)
 ? ? ?  } else {
 ? ? ? ? ? ?// 文本節(jié)點(diǎn)
 ? ? ? ? ? ?this.compileText(child)
 ? ? ?  }

 ?      // 遞歸遍歷
 ?      if(child.childNodes && child.childNodes.length) {
 ? ? ? ? ? ?handleFragment(child)
 ? ? ?  }
 ?  })
}

// 同樣的我們需要完善一下compile的構(gòu)造函數(shù)
constructor(el, vm) {
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
 ? ?this.vm = vm
 ? ?// 新建文檔碎片存儲(chǔ)DOM
 ? ?const fragment = this.toFragment(this.el)

 ? ?// 操作文檔碎片 to handle fragment
 ? ?this.handleFragment(fragment)

 ? ?// 將操作好的文檔碎片追加到原本的DOM上面
 ? ?this.el.appendChild(fragment)
}

獲取元素節(jié)點(diǎn)上的信息

元素節(jié)點(diǎn)上的信息主要就是這個(gè)元素節(jié)點(diǎn)上面的屬性,然后拿到綁定在節(jié)點(diǎn)上面的vue指令,分離出來(lái)vue指令的名稱和值(注意:@開(kāi)的頭的指令需要額外處理)。然后還有很重要的一步,那就是去掉這些指令(這些指令updater是不認(rèn)的)

compileElement(node) {
    const attrs = node.attributes
 ? ?// 遍歷節(jié)點(diǎn)上的全部屬性
 ?  [...attrs].forEach(({name, value}) => {
 ? ? ? ?// 分類看指令以什么開(kāi)頭
 ?      if(this.headWithV(name)) {
            // 以v開(kāi)頭
 ? ? ? ? ? ?const [,directive] = name.split("-") //分離出具體指令
 ? ? ? ? ? ?const [dir,event] = directive.split(":") // 考慮v-on的情況 例如v-on:click

 ? ? ? ? ? ?// 將指令的名稱、值、node節(jié)點(diǎn)、整個(gè)vm實(shí)例、事件名(如果有的話)一起傳給最后真正操作的node的函數(shù)
 ? ? ? ? ? ?handleNode[dir](node, value, this.vm, event)

 ? ? ?  }else if(this.headWithoutV(name)) {
            // 以@開(kāi)投
 ? ? ? ? ? ?const [, event] = name.split("@")
 ? ? ? ? ? ?// 和上面一樣,但是指令名字是確定的,為“on” 因?yàn)锧是v-on的語(yǔ)法糖
 ? ? ? ? ? ?handleNode["on"](node, value, this.vm, event)
 ? ? ?  }
 ?  })
}
?
headWithV(name) {
    return name.startsWith("v-");
}
headWithoutV(name){
 ? ?return name.startsWith("@");
}

獲取文本節(jié)點(diǎn)信息

文本節(jié)點(diǎn)和元素節(jié)點(diǎn)類似,只不過(guò)文本節(jié)點(diǎn)的信息存儲(chǔ)在節(jié)點(diǎn)的textContent里面,主要用來(lái)替換mustache語(yǔ)法,(雙大括號(hào)插值)需要通過(guò)正則識(shí)別額外處理。如果是正常的文本節(jié)點(diǎn),則不進(jìn)行處理(原模原樣展示即可)。

compileText(node) {
 ? ?const content = node.textContent
 ? ?if(!/{{(.+?)}}/.test(content)) return
 ? ?// 識(shí)別到是mustache語(yǔ)法 處理方法其實(shí)和v-text一樣
 ? ?handleNode["text"](node, content,this.vm)
}

操作fragment

前面鋪墊了這么多,終于到了操作文檔碎片這一步了。按照上面的思路,handleNode應(yīng)該是一個(gè)對(duì)象,里面有多個(gè)屬性對(duì)應(yīng)不同的指令的處理方法。

// node--操作的node節(jié)點(diǎn)  exp--指令的值(或者是mustache語(yǔ)法內(nèi)部插入的內(nèi)容)  vm--vm實(shí)例  event--事件名稱
const handleNode = {
 ? ?// v-html
 ? ?html(node, exp, vm) {
 ?      // 去vm實(shí)例中找到這個(gè)表達(dá)式所對(duì)應(yīng)的值
 ? ? ? ?const value = this._get(vm, exp)
 ? ? ? ?// 更新node
 ? ? ? ?updater.htmlUpdater(node, value)
    },
 ? ?// v-model
 ? ?model(node, exp, vm) {
 ? ? ? ?// 同html
 ?      const value = this._get(vm, exp)
 ? ? ? ?updater.modelUpdater(node, value)
    },
 ? ?// v-on
 ? ?on(node, exp, vm, event) {
        // v-on特殊一點(diǎn),我們需要為該node綁定事件監(jiān)聽(tīng)器
 ? ? ? ?const listener = vm.$options.methods && vm.$options.methods[exp] // 獲取監(jiān)聽(tīng)器的回調(diào)
 ? ? ? ?// 綁定監(jiān)聽(tīng)器,注意回調(diào)綁定使用bind把this指向vm實(shí)例,false代表事件冒泡時(shí)觸發(fā)監(jiān)聽(tīng)器
 ? ? ? ?node.addEventListener(event, listener.bind(this), false) 
    },
 ? ?// v-text
 ? ?text(node, exp, vm) {
 ? ? ? ?// v-text是最復(fù)雜的,需要考慮兩種情況,一種是通過(guò)v-text指令操作node,另一種則是通過(guò)mustache語(yǔ)法操作node,需分類
 ? ? ? ?let value
 ? ? ? ?if(exp.indexOf("{{") !== -1) {
 ? ? ? ? ? ?// mustache語(yǔ)法操作node
 ? ? ? ? ? ?// 捕捉到所有的mustache語(yǔ)法,將其整個(gè)替換為vm實(shí)例中屬性對(duì)應(yīng)的值
 ? ? ? ? ? ?// 拿我們最初初始化實(shí)例的一個(gè)數(shù)據(jù)舉例:{{obj.auth}} -- 'xxx'
 ? ? ? ? ? ?value = exp.replace(/{{(.+?)}}/g, this._get(vm, exp)) 
 ? ? ?  }else {
 ? ? ? ? ? ?// v-text操作node
 ? ? ? ? ? ?value = this._get(vm, exp)
 ? ? ?  }
 ? ? ? ?// 更新node
 ? ? ? ?updater.textUpdater(node, value);
 ?  },
}
?
// 根據(jù)表達(dá)式去數(shù)據(jù)對(duì)象里面獲取值
_get(vm, exp) {
 ? ?const segments = exp.split('.')
 ? ?// 這里使用reduce是為了獲取嵌套對(duì)象內(nèi)部屬性的值,不熟悉的話去補(bǔ)一補(bǔ)reduce
 ? ?// 比如data.a.b.c,那么每次遍歷的值為data[a],data[a][b],最終結(jié)果是data[a][b][c]
    segments.reduce((pre, key) => {
        return pre[key]
 ?  }, vm.$data)
}

// 更新node (終于到了更新node這一步)
const updater = {
 ? ?textUpdater(node, value) {
        node.textContent = value;
 ?  },
 ? ?htmlUpdater(node, value) {
        node.innerHTML = value;
 ?  },
 ? ?modelUpdater(node, value){
        node.value = value;
 ?  }
}

至此我們已經(jīng)實(shí)現(xiàn)了vue實(shí)例模板編譯,并更新了node,其實(shí)到現(xiàn)在我們還沒(méi)有涉及到響應(yīng)式這三個(gè)字。下面我們開(kāi)始介紹本篇的核心,即vue是如何實(shí)現(xiàn)響應(yīng)式的。

響應(yīng)式

數(shù)據(jù)劫持

關(guān)鍵點(diǎn):Object.defineProperty(具體用法參考MDN)

主要目的:為data中每個(gè)屬性添加gettersetter,然后在gettersetter中進(jìn)行數(shù)據(jù)劫持

思路很簡(jiǎn)單,其實(shí)就是從最外層的data層開(kāi)始遍歷屬性,通過(guò)Object.defineProperty給這些屬性都添加上gettersetter,需要注意對(duì)象的嵌套,所以需要使用遞歸來(lái)為嵌套的屬性添加gettersetter

function observe(data) {
    if(typeof data !== 'object') return
 ? ?Object.keys(data).forEach((key) => {
 ? ? ? ?defineReactive(data, key, data[key])
 ?  })
}

function defineReactive(data, key, value) {
 ? ?// 遞歸子屬性
    observe(value)

 ? ?Object.defineProperty(data, key, {
        get() {
 ? ? ? ? ? ?// 數(shù)據(jù)劫持 在這個(gè)地方進(jìn)行相關(guān)操作
            return value
 ? ? ?  }
 ? ? ? ?set(newVal) {
            if(newVal == value) return
 ? ? ?      value = newVal
 ? ? ?      // 為新數(shù)據(jù)添加getter和setter
 ? ? ?      observe(newVal)
 ? ? ?      // 數(shù)據(jù)劫持 在這個(gè)地方進(jìn)行相關(guān)操作
 ?      }
 ?  })
}

收集依賴

依賴其實(shí)說(shuō)白了,就是數(shù)據(jù)的依賴,data中的某個(gè)屬性,可能在DOM中好幾個(gè)地方進(jìn)行了使用,那DOM中使用到該屬性的地方就都會(huì)產(chǎn)生一個(gè)對(duì)于該屬性的依賴,也就是watcher。當(dāng)該屬性的值發(fā)生了變化,那么就可以通知watcher來(lái)使得頁(yè)面中使用到這個(gè)屬性的地方進(jìn)行視圖更新。為每個(gè)屬性綁定watcher的過(guò)程其實(shí)就是訂閱,反過(guò)來(lái),當(dāng)屬性的值發(fā)生了變化,通知所有watcher的過(guò)程就是發(fā)布。

下面我們來(lái)將依賴抽象化,即實(shí)現(xiàn)watcher

class Watcher {
    // data--最外層數(shù)據(jù)對(duì)象  exp--表達(dá)式  cb--數(shù)據(jù)更新后需要執(zhí)行的回調(diào)
    // 通過(guò)data和exp可以獲取watcher所依賴屬性的具體值
    constructor(data, exp, cb) {
	this.data = data
        this.exp = exp
        this.cb = cb
        // 每次初始化watcher實(shí)例時(shí),對(duì)依賴屬性進(jìn)行訂閱
        this.value = this.subscribe()
    }
    // 訂閱
    subscribe() {
        // 獲取依賴屬性的值
	const value = _get(this.data, this.exp)
        return value
    }
    // 更新
    update() {
        // 獲取新值
	this.value = _get(this.data, this.exp)
        cb()
    }
}

// 根據(jù)表達(dá)式去數(shù)據(jù)對(duì)象里面獲取值 其實(shí)上面已經(jīng)定義過(guò)一個(gè)了,功能是一樣的,這里重復(fù)定義加深一下印象,也方便閱讀
function _get(obj, exp) {
    const segments = exp.split('.')
    // 這里使用reduce是為了獲取嵌套對(duì)象內(nèi)部屬性的值,不熟悉的話去補(bǔ)一補(bǔ)reduce
    // 比如data.a.b.c,那么每次遍歷的值為data[a],data[a][b],最終結(jié)果是data[a][b][c]
    segments.reduce((pre, key) => {
	return pre[key]
    }, obj)
}

依賴我們大概清楚了,但是我們上面講,需要把一個(gè)屬性全部的依賴(watcher)收集起來(lái),所以我們?cè)撊绾?strong>收集依賴呢?

首先我們先想第一個(gè)問(wèn)題,一個(gè)屬性會(huì)有一個(gè)或者好多個(gè)watcher,我們應(yīng)該如何保存這些watcher呢,這個(gè)我們很容易想到,我們可以專門拿一個(gè)數(shù)組保存一個(gè)屬性的全部watcher,我們把這個(gè)數(shù)組命名為dep(dependency)。

第二個(gè)問(wèn)題,我們應(yīng)該什么時(shí)候進(jìn)行收集watcher的操作呢。還記得我們上面提到的訂閱嗎,我們每次初始化watcher時(shí),會(huì)為該watcher訂閱屬性,訂閱的過(guò)程中我們會(huì)首先獲取這個(gè)屬性的值,這時(shí)就可以發(fā)揮數(shù)據(jù)劫持的作用了,獲取這個(gè)屬性值的時(shí)候,我們就會(huì)進(jìn)到這個(gè)屬性的getter方法中,所以我們可以在這個(gè)時(shí)候完成收集watcher的操作。

第三個(gè)問(wèn)題,我們說(shuō)watcher的作用其實(shí)就是監(jiān)聽(tīng)到訂閱屬性的變化(即監(jiān)聽(tīng)發(fā)布),監(jiān)聽(tīng)到變化后執(zhí)行其update方法,即執(zhí)行更新回調(diào),來(lái)更新視圖。那么我們?cè)鯓硬拍茏?code>watcher監(jiān)聽(tīng)到“發(fā)布”呢,這時(shí)我們又需要用到數(shù)據(jù)劫持,即在setter中通知這個(gè)屬性所有的watcher

function defineReactive(data, key, value) {
    // 新建用于存儲(chǔ)watcher的數(shù)據(jù)
    const dep = []

    // 遞歸子屬性
    observe(value)

    Object.defineProperty(data, key, {
        get() {
            // 數(shù)據(jù)劫持 在這個(gè)地方進(jìn)行相關(guān)操作
            dep.push(watcher) // 收集依賴
                        return value
        }
        set(newVal) {
            if(newVal == value) return
            value = newVal
            // 為新數(shù)據(jù)添加getter和setter
            observe(newVal)

            // 數(shù)據(jù)劫持 在這個(gè)地方進(jìn)行相關(guān)操作
            dep.notify() // 通知依賴
    	}
    })
}

現(xiàn)在我覺(jué)得我有必要理一下這個(gè)依賴收集的全過(guò)程。首先頁(yè)面初次渲染的時(shí)候,會(huì)遇到我們?cè)赿ata中定義的屬性(注意:此時(shí)屬性上面已經(jīng)定義好getter和setter了),遇到屬性后會(huì)初始化一個(gè)watcher實(shí)例,在此過(guò)程中watcher實(shí)例會(huì)獲取這個(gè)屬性的值,于是會(huì)進(jìn)入到這個(gè)屬性的getter中,于是我們通過(guò)數(shù)據(jù)劫持來(lái)收集這個(gè)watcher。那么又出現(xiàn)了一個(gè)問(wèn)題,我們此時(shí)在getter中,如何獲取到初始化的watcher實(shí)例呢,也就是dep.push的時(shí)候,其實(shí)我們是沒(méi)有辦法直接拿到這個(gè)watcher的。因此,我們需要在初始化watcher的時(shí)候,把這個(gè)watcher放到全局,比如window.target

subscribe() {
    // 獲取依賴屬性的值
    window.target = this // 這里的this即為此時(shí)初始化的watcher實(shí)例
	const value = _get(this.data, this.exp)
    return value
}
function defineReactive(data, key, value) {
    // 新建用于存儲(chǔ)watcher的數(shù)據(jù)
    const dep = []
    observe(value)
    Object.defineProperty(data, key, {
        get() {
            dep.push(window.target) // 改為window.target

            return value
        }
        set(newVal) {
            if(newVal == value) return
            value = newVal
            observe(newVal)

            dep.notify()
    	}
    })
}

響應(yīng)式代碼完善

Dep類

我們可以講dep數(shù)組抽象為一個(gè)類

class Dep {
    constructor() {
        this.subs = []
    }
    // 收集依賴
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 通知依賴
    notify() {
        [...this.subs].forEach((watcher) => {
            watcher.update()
        })
    }
}

defineReactive也要做出相應(yīng)的調(diào)整

function defineReactive(data, key, value) {
    // 新建用于存儲(chǔ)watcher的數(shù)據(jù)
    const dep = new Dep()
    observe(value)
    Object.defineProperty(data, key, {
        get() {
            // 收集依賴
            dep.addSub(window.target) 
            return value
        }
        set(newVal) {
            if(newVal == value) return
            value = newVal
            observe(newVal)
            // 通知依賴
            dep.notify()
    	}
    })
}

全局watcher用完清空

下面有一個(gè)場(chǎng)景,我們?cè)谠L問(wèn)到data中的一個(gè)屬性a后,實(shí)例化了一個(gè)watcher1,此時(shí)在實(shí)例化這個(gè)watcher1的過(guò)程中,會(huì)把window.target設(shè)置為watcher1,之后我們?cè)跊](méi)有實(shí)例化其他watcher的情況下直接去訪問(wèn)其他的屬性,例如屬性b,那么屬性b中的getter會(huì)直接把watcher1推入到它的依賴數(shù)組中。這樣是不合理的,所以我們每次將watcher推入到依賴數(shù)組中后,要將這個(gè)watcher從全局中收回。(window.target這里改成Dep.target了,其實(shí)都是一樣的)

subscribe() {
    Dep.target = this // 這里的this即為此時(shí)初始化的watcher實(shí)例
    const value = _get(this.data, this.exp)
    Dep.target = null // 清空暴露在全局中的watcher
    return value
}

// 同時(shí)在收集依賴時(shí)添加一層過(guò)濾
addSub(watcher) {
    if(watcher) {
        this.subs.push(watcher)
    }
}

依賴的update方法

上面我們?cè)?code>watcher的update方法中更新了值并且執(zhí)行了數(shù)據(jù)更新后的回調(diào),為了讓豐富回調(diào)中的操作,我們可以將回調(diào)的this指向我們的最外層數(shù)據(jù)對(duì)象,這樣在回調(diào)中就可以通過(guò)this任意獲取數(shù)據(jù)對(duì)象中的其他屬性,并且將更新之前的舊值和新值一起傳入到update里面

update() {
  const oldValue = this.value // 獲取舊值
  this.value = parsePath(this.data, this.expression) // 獲取新值
  this.cb.call(this.data, this.value, oldValue)
}

需要注意的一個(gè)地方

下面是watcher中獲取所依賴屬性值的方法,這里需要說(shuō)明一下,對(duì)于存在對(duì)象嵌套的情況,每一層屬性的依賴數(shù)組中都會(huì)添加這個(gè)watcher,想不明白的話可以看一下下面的注解。

// 根據(jù)表達(dá)式去數(shù)據(jù)對(duì)象里面獲取值
function _get(obj, exp) {
    const segments = exp.split('.')
    /*
    比如data.a.b.c,那么每次遍歷的值為data[a],data[a][b],最終結(jié)果是data[a][b][c]
    遍歷到data[a]、data[a][b]時(shí),肯定會(huì)去訪問(wèn)這兩個(gè)屬性的值,于是會(huì)進(jìn)入到這兩個(gè)屬性的getter里面
    所以這個(gè)watcher不僅僅會(huì)被添加到最內(nèi)層屬性的getter中,中間每一層屬性的getter中都會(huì)有這個(gè)watcher
    即如果data[a]的值發(fā)生了變化,也會(huì)通知這個(gè)watcher去更新視圖
    */ 
    segments.reduce((pre, key) => {
        return pre[key]
    }, obj)
}

雙劍合璧

怎樣將上面的編譯響應(yīng)式整合到一起形成一個(gè)完整的具有響應(yīng)式的miniVue類呢。其實(shí)很簡(jiǎn)單,從我們最上面那張圖就可以看出來(lái)??偨Y(jié)一下就兩點(diǎn),在我們通過(guò)各種指令操作node節(jié)點(diǎn)的時(shí)候,同時(shí)初始化watcher,另一點(diǎn)即為初始化watcher時(shí)指定的回調(diào)內(nèi)部需要執(zhí)行updater里面對(duì)應(yīng)的方法來(lái)更新視圖

兩點(diǎn)分別對(duì)應(yīng)下圖的這兩根線:

這樣是不是就清晰多了。至此”雙劍合璧“完成,下面貼一下合璧后的代碼(只放需要合成的部分,這樣更清晰一點(diǎn))

// node--操作的node節(jié)點(diǎn)  exp--指令的值(或者是mustache語(yǔ)法內(nèi)部插入的內(nèi)容)  vm--vm實(shí)例  event--事件名稱
const handleNode = {
 ? ?// v-html
 ? ?html(node, exp, vm) {
 ? ? ? ?const value = this._get(vm, exp)
 ? ? ? ?// 新建watcher實(shí)例,并綁定更新回調(diào)
 ? ? ? ?new Watcher(vm, exp, (newVal, oldVal) => {
 ? ? ? ? ? ?// 這里是所依賴數(shù)據(jù)更新以后更新視圖
 ? ? ?      this.updater.htmlUpdater(node, newVal);
 ?      })
 ? ? ? ?// 這里是編譯的時(shí)候更新視圖
 ? ? ? ?updater.htmlUpdater(node, value)
    },
 ? ?// v-model
 ? ?model(node, exp, vm) {
 ?      const value = this._get(vm, exp)
 ? ? ? ?// 新建watcher實(shí)例,并綁定更新回調(diào)
 ? ? ? ?new Watcher(vm, exp, (newVal, oldVal) => {
 ? ?        this.updater.modelUpdater(node, newVal);
 ?      });
 ? ? ? ?updater.modelUpdater(node, value)
    },
 ? ?// v-on
 ? ?on(node, exp, vm, event) {
 ? ? ? ?// watcher只針對(duì)屬性 v-on這里不會(huì)生成watcher(方法名也沒(méi)什么好監(jiān)聽(tīng)的,一般也不會(huì)操作方法名讓方法名發(fā)生變化)
 ? ? ? ?const listener = vm.$options.methods && vm.$options.methods[exp] 
 ? ? ? ?node.addEventListener(event, listener.bind(this), false) 
    },
 ? ?// v-text
 ? ?text(node, exp, vm) {
 ? ? ? ?let value
 ? ? ? ?if(exp.indexOf("{{") !== -1) {
 ? ? ? ? ? ?// mustache語(yǔ)法操作node
 ? ? ? ? ? ?value = exp.replace(/{{(.+?)}}/g, this._get(vm, exp)) 
 ? ? ? ? ? ?
 ? ? ?  }else {
 ? ? ? ? ? ?// v-text操作node
 ? ? ? ? ? ?value = this._get(vm, exp)
 ? ? ?  }
 ? ? ? ?// 新建watcher實(shí)例,并綁定更新回調(diào)
 ? ? ? ?new Watcher(vm, exp, (newVal, oldVal) => {
 ? ?        this.updater.textUpdater(node, newVal);
 ?      });
 ? ? ? ?updater.textUpdater(node, value);
 ?  },
}
_get(vm, exp) {
 ? ?const segments = exp.split('.')
    segments.reduce((pre, key) => {
        return pre[key]
 ?  }, vm.$data)
}
?
const updater = {
 ? ?textUpdater(node, value) {
        node.textContent = value;
 ?  },
 ? ?htmlUpdater(node, value) {
        node.innerHTML = value;
 ?  },
 ? ?modelUpdater(node, value){
        node.value = value;
 ?  }
}

最后的最后,修改一下我們最開(kāi)始定義miniVue類的構(gòu)造函數(shù)

class miniVue {
    constructor(options) {
        this.$el = options.el
 ? ? ? ?this.$data = options.data
 ? ? ? ?this.$options = options
 ?  }
 ? ?if(this.$el) {
 ? ? ? ?// 添加數(shù)據(jù)劫持
        this.observe()
 ? ? ? ?// 編譯
 ? ? ? ?new Compile(this.$el, this);
 ?  }
}

大功告成。

總結(jié)

如果你是第一次閱讀本文,看到最后應(yīng)該還是會(huì)感覺(jué)到些許混亂。下面允許我為大家概括一下整體的流程。建議結(jié)合我們最上方的中心圖。

  • 1.初始化minivue實(shí)例 執(zhí)行其構(gòu)造函數(shù),首先對(duì)實(shí)例的數(shù)據(jù)對(duì)象data中全部屬性添加數(shù)據(jù)劫持功能(getterand setter
  • 2.開(kāi)始編譯實(shí)例綁定的模板。
  • 3.首先編譯做準(zhǔn)備,創(chuàng)建compile類,拿到模板的整個(gè)DOM對(duì)象,遍歷其子節(jié)點(diǎn),獲取到每個(gè)子節(jié)點(diǎn)上的信息,這些信息中凡是引用過(guò)vm實(shí)例data中的屬性的,一律都新增一個(gè)watcher實(shí)例
  • 4.初始化watcher實(shí)例的時(shí)候,會(huì)訪問(wèn)這個(gè)屬性,然后進(jìn)入這個(gè)屬性的getter中,在getter中,將這個(gè)watcher添加到這個(gè)屬性的Dep類中
  • 5.最后更新node,至此初始化編譯完成
  • 6.當(dāng)data中某一個(gè)屬性的值發(fā)生變化,會(huì)進(jìn)入這個(gè)屬性的setter中,setter會(huì)通知該屬性的Dep
  • 7.Dep類會(huì)通知存儲(chǔ)的所有相關(guān)watcher進(jìn)行更新,于是這些watcher分別執(zhí)行自己update中的回調(diào)?;卣{(diào)即會(huì)更新node。

到此這篇關(guān)于一篇文章帶你徹底搞懂VUE響應(yīng)式原理的文章就介紹到這了,更多相關(guān) VUE響應(yīng)式原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論