效果
簡述原理
配置對象傳入vue實例
模板解析,遍歷出所有文本節點,利用正則替換插值表達式為真實數據
data數據代理給vue實例,以后通過this.xxx訪問
給每個dom節點增加觀察者實例,由觀察者群組管理,內部每一個鍵值含有多個對不同dom的觀察者
data數據劫持,給data的每個屬性增加get和set函數,當值改變時觸發觀察者的update方法,更新所有與當前屬性值相關的dom元素
劫持數據,說的挺好聽的,就是加工數據嘛,多了set變化觸發了模板重新渲染,該渲染方式使用觀察者模式,獲取觀察者收集的各個dom的所有屬性 div,觀察的屬性,div的屬性textContent,同時根據最新值渲染模板
div.textContent=vm[key]
html
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><!-- <script src="./vue.js"></script> -->
</head><body><div id="app">{{ name }} {{age}}<h1>{{age}}</h1><button @click="cli">按鈕</button><input type="text" v-model="name"></div>
</body>
<script src="./vue.js">
</script>
<script>new Vue({el: '#app',data: {name: 'Zwwwww',age: 18,},methods: {cli() {console.log(this);console.log(this.age);}},})</script></html>
js代碼
class Vue {constructor(options) {// 獲取配置對象的節點,存放在vm$el身上this.$el = document.querySelector(options.el)// console.log(this.$el)// 將配置對象的data對象代理到$datathis.$data = options.data// 獲取配置對象的method值,// vue實例監聽,當觸發了方法執行對應函數this.$methods = options.methods// 代理數據,后續通過this調用data對象的值this.$allWatcher = {}this.proxyData()// 劫持數據,為其增加觀察者監視數據變化引起視圖渲染this.observe()// 收集所有觀察者,用對象的屬性存放this.compile(this.$el)}// 數據代理到vue實例身上,后續this調用方法和data值proxyData() {// 遍歷$data身上所有keyfor (let key in this.$data) {// 數據代理給vue實例,thisObject.defineProperty(this, key, {// 使用get和set后續觸發獲取值和設置值做額外操作get() {// 返回當前data對應的key屬性值return this.$data[key]},set(value) {// 設置新值給當前屬性this.$data[key] = value},})}}// js數據替換{{name}},模板解析compile(node) {// 遍歷根節點下的所有節點node.childNodes.forEach((item, index) => {//遞歸元素節點,//如果還沒到文本節點,也就是說元素節點內還有元素節點//則繼續遞歸,直到元素節點沒有子節點//第二種可能,如果為元素元素節點,判斷是否有@click屬性,并獲取值//該值為綁定的methods方法if (item.nodeType === 1) {if (item.childNodes.length > 0) {this.compile(item)}if (item.hasAttribute('@click')) {let domKey = item.getAttribute('@click')// console.log('我是dom標簽的key', domKey)// 設置監聽器,如果被點擊了,觸發配置對象中的method函數item.addEventListener('click', () => {// 通過模板獲取的屬性值方法命,調用函數// 由于$methods只是引用地址,this指向還是原來的methods// 我們這里使用call來綁定他的上下文this,也就是綁定他的調用者// 在html部分我們就可以使用this.$data.age來獲取vue實例上的數據// 如果我們想直接this.age 就需要將data代理到vue實例身上this.$methods[domKey.trim()].call(this)})}if (item.hasAttribute('v-model')) {let vmodelKey = item.getAttribute('v-model').trim()// console.log('我是v-model的key', vmodelKey)// 設置監聽器,如果被點擊了,觸發配置對象中的method函數// 先單向給input框設置值item.value = this.$data[vmodelKey]item.addEventListener('input', () => {console.log('用戶正在輸入')// 每次輸入時將輸入框的值重新賦給data對象屬性值,完成雙向綁定this.$data[vmodelKey] = item.valueconsole.log(this.$data[vmodelKey])// 數據更新的同時重新解析模板// 這里使用觀察者類觀察數據變化所作出的響應})}}// 判斷是否為文本節點,nodeType == 3// console.log(item.nodeType)// 如果是文本節點,進行數據替換// 如果不是文本節點,為元素節點則往里遞歸遍歷文本節點if (item.nodeType === 3) {// 定義正則,替換{{xxx}}形式的字串為data下的屬性值let reg = /\{\{(.*?)\}\}/g// 獲取原本標簽里的值,后續進行替換let text = item.textContent// console.log(text)item.textContent = text.replace(reg, (match, dataKey) => {// 先將dataKey去空格處理dataKey = dataKey.trim()// match為匹配到的整體,datakey為捕獲到的子內容(.*?)//我們這里只需獲取dataKey對應的值并塞入即可// console.log(match, dataKey)// 返回值作為替換內容 去除dataKey的前后空格// 增加觀察者,傳vue實例對象,data屬性,item標簽,標簽屬性// 相當于給每個文本節點都添加了一個觀察者// 將所有觀察者收集到vue實例上,在數據發生變化時調用觀察者的update方法let watcher = new Watcher(this, dataKey, item, 'textContent')// 先進行判斷觀察者群組里是否有該節點的觀察者// 如果有,就push添加,因為一個dataKey可能有多個模板使用// 舉個例子,name屬性可能在div1里使用也在div2里使用// 也就是將多個文本節點與同個datakey綁定if (this.$allWatcher[dataKey]) {this.$allWatcher[dataKey].push(watcher)}// 如果沒有該屬性的觀察者存在,則新建空數組,push該觀察者進入else {this.$allWatcher[dataKey] = []this.$allWatcher[dataKey].push(watcher)}return this.$data[dataKey]})}})}observe() {console.log('開始劫持')// 遍歷所有的key,對其data數據劫持,值增加響應式功能for (let key in this.$data) {// 先獲取value,否則數據重新定義后值會丟失// 此處的value變量不會隨著observe方法的結束而銷毀// 與內部匿名函數get和set作為閉包永遠綁定在一起// 同時value值是對$data的一個引用,修改value值會引起$data變化let value = this.$data[key]// 保存一份vue的引用_this=this,// 防止后續在組件外部,也就是input輸入框// 此時觸發的set為一個閉包環境,上下文變成由defineproper定義的this.$data數據對象// 此時找不到vue實例作為上下文,對key和其他數據的引用也會失效let _this = thisObject.defineProperty(this.$data, key, {get() {console.log('有人要獲取劫持數據值', value)// 返回上面存儲的value值// 由于是響應式的,只有當觀察到數據變化時所以才接觸數據// 其value值作用域也作用在劫持過程中return value},set(newValue) {console.log('劫持到數據,修改值為', newValue)console.log('劫持前的數據為', value)value = newValue// 更新值的同時進行模板更新// 由于觀察者隊列含有觀察者來觀察不同屬性管理的若干個模板// 調用該屬性值下所有模板觀察者即可,// 只要屬性值變化,該屬性值下的所有觀察者重新渲染模板console.log(_this.$allWatcher)console.log(_this.$allWatcher[key])_this.$allWatcher[key].forEach((watcher, index) => {watcher.update()})},})}console.log('劫持成功')}
}class Watcher {constructor(vm, key, node, attr) {this.vm = vmthis.key = keythis.node = nodethis.attr = attr}// item.textContent = this.$data[dataKey.trim()]update() {console.log('開始渲染')// 將原始dom標簽內容值替換為 data里的屬性值this.node[this.attr] = this.vm[this.key]}
}
代碼參考
VUE雙向綁定原理分析~實現視圖和數據的雙向綁定~_嗶哩嗶哩_bilibili