在模擬最小的vue之前,先複習一下,發佈訂閱模式和觀察者模式
對兩種模式有了瞭解之後,對Vue2.0和Vue3.0的數據響應式核心原理
1.Vue2.0和Vue3.0的數據響應式核心原理
(1). Vue2.0是採用Object.defineProperty的方式,對數據進行get,set方法設置的, 具體可以詳見Object.defineProperty的介紹
<script> // 模擬 Vue 中的 data 選項 let data = { msg: 'hello' } // 模擬 Vue 的實例 let vm = {} // 數據劫持:當訪問或者設置 vm 中的成員的時候,做一些干預操作 Object.defineProperty(vm, 'msg', { // 可枚舉(可遍歷) enumerable: true, // 可配置(可以使用 delete 刪除,可以通過 defineProperty 重新定義) configurable: true, // 當獲取值的時候執行 get () { console.log('get: ', data.msg) return data.msg }, // 當設置值的時候執行 set (newValue) { console.log('set: ', newValue) if (newValue === data.msg) { return } data.msg = newValue // 數據更改,更新 DOM 的值 document.querySelector('#app').textContent = data.msg } }) // 測試 vm.msg = 'Hello World' console.log(vm.msg) </script>
如果,vm裏的屬性是對象如何處理,可以,對其遍歷,在進行Object.defineProperty
<script> // 模擬 Vue 中的 data 選項 let data = { msg: 'hello', count: 10, person: {name: 'zhangsan'} } // 模擬 Vue 的實例 let vm = {} proxyData(data) function proxyData(data) { // 遍歷 data 對象的所有屬性 Object.keys(data).forEach(key => { // 把 data 中的屬性,轉換成 vm 的 setter/setter Object.defineProperty(vm, key, { enumerable: true, configurable: true, get () { console.log('get: ', key, data[key]) return data[key] }, set (newValue) { console.log('set: ', key, newValue) if (newValue === data[key]) { return } data[key] = newValue // 數據更改,更新 DOM 的值 document.querySelector('#app').textContent = data[key] } }) }) } // 測試 vm.msg = 'Hello World' console.log(vm.msg) </script>
(2). Vue3.x是採用proxy代理的方式實現, 直接監聽對象,而非屬性。ES 6中新增,IE 不支持,性能由瀏覽器優化,具體可以詳見MDN - Proxy
<script> // 模擬 Vue 中的 data 選項 let data = { msg: 'hello', count: 0 } // 模擬 Vue 實例 let vm = new Proxy(data, { // 執行代理行爲的函數 // 當訪問 vm 的成員會執行 get (target, key) { console.log('get, key: ', key, target[key]) return target[key] }, // 當設置 vm 的成員會執行 set (target, key, newValue) { console.log('set, key: ', key, newValue) if (target[key] === newValue) { return } target[key] = newValue document.querySelector('#app').textContent = target[key] } }) // 測試 vm.msg = 'Hello World' console.log(vm.msg) </script>
2.Vue 響應式原理模擬
看圖,整體分析
- 把 data 中的成員注入到 Vue 實例,並且把 data 中的成員轉成 getter/setter
- 能夠對數據對象的所有屬性進行監聽,如有變動可拿到最新值並通知 Dep
- 解析每個元素中的指令/插值表達式,並替換成相應的數據
- 添加觀察者(watcher),當數據變化通知所有觀察者
- 數據變化更新視圖
(1) Vue
- 負責接收初始化的參數(選項)
- 負責把 data 中的屬性注入到 Vue 實例,轉換成 getter/setter
- 負責調用 observer 監聽 data 中所有屬性的變化
- 負責調用 compiler 解析指令/插值表達式
class Vue { constructor (options) { //1.通過屬性保存選項的數據 this.$options = options || {} this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el this.$data = options.data || {} //2.把data中的成員轉換成getter和setter方法,注入到vue實例中 this._proxyData(this.$data) //3.調用observer對象,監聽數據變化 new Observer(this.$data) //4.調用compiler對象, 解析指令和差值表達式 new Compiler(this) } _proxyData (data) { //遍歷data中的所有屬性 Object.keys(data).forEach( key => { //把data的屬性注入到vue實例中 Object.defineProperty(this, key, { enumerable: true, configurable: true, get () { return data[key] }, set (newValue) { if (newValue === data[key]) { return } data[key] = newValue } }) }) } }
(2)Observer
- 負責把 data 選項中的屬性轉換成響應式數據
- data 中的某個屬性也是對象,把該屬性轉換成響應式數據
- 數據變化發送通知
class Observer { constructor (data) { this.walk(data) } //1. walk (data) { //1.判斷data是不是對象 if (!data || typeof data !== 'object') { return } //遍歷data對象裏的所有屬性 Object.keys(data).forEach( key => { this.definedReactive(data, key, data[key]) }) } definedReactive (obj, key, value) { let that = this //負責收集依賴(觀察者), 併發送通知 let dep = new Dep() this.walk(value)//如果data裏的屬性是對象,對象裏面的屬性也得是響應式的,所以得判斷一下 Object.defineProperty (obj, key, { enumerable: true, configurable: true, get () { //收集依賴 Dep.target && dep.addSubs(Dep.target) return value // return obj[key]//這麼寫會引起堆棧溢出 }, set (newValue) { if (newValue === value) { return } value = newValue that.walk(newValue)//如果賦值爲對象,對象裏面的屬性得是響應式數據 //數據變換 ,發送通知給watcher的update ,在渲染視圖裏的數據 dep.notify() } }) } }
(3).Compiler
- 負責編譯模板,解析指令/插值表達式
- 負責頁面的首次渲染
- 當數據變化後重新渲染視圖
class Compiler { constructor (vm) {//傳個vue實例 this.el = vm.$el this.vm = vm this.compile(this.el) } //編譯模板, 處理文本節點和元素節點 compile (el) { let childNodes = el.childNodes //獲取子節點 僞數組 console.dir(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) { //console.log(node.attributes) Array.from(node.attributes).forEach( attr => { //判斷是不是指令 let attrName = attr.name //<div v-text="msg"></div> 裏的v-text if (this.isDirective(attrName)) { //v-text --> text attrName = attrName.substr(2) let key = attr.value //<div v-text="msg"></div> 裏的msg this.update(node , key, attrName) } }) } update (node, key, attrName) { let updateFn = this[attrName + 'Updater'] updateFn && updateFn.call(this, node, this.vm[key], key)//call方法改變this指向 } //處理v-text 命令 textUpdater (node, value, key) { node.textContent = value new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) } //v-model modelUpdater (node, value, key) { node.value = value new Watcher(this.vm, key, (newValue) => { node.value = newValue }) //雙向綁定,視圖改變,數據也會更新 node.addEventListener('input', () => { this.vm[key] = node.value }) } //編譯文本節點,處理差值表達式 compileText (node) { //console.dir(node) // {{ msg }} let reg = /\{\{(.+?)\}\}/ let value = node.textContent //裏面的內容, 也可以是nodeValue if (reg.test(value)) { let key = RegExp.$1.trim() //匹配到的第一個 node.textContent = value.replace(reg, this.vm[key]) //創建watcher對象, 當數據改變更新視圖 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 } }
(4).Dep(Dependency)
功能
- 收集依賴,添加觀察者(watcher)
- 通知所有觀察者
class Dep { constructor () { //收集觀察者 this.subs = [] } //添加觀察者 addSubs (watcher) { if (watcher && watcher.update) { this.subs.push(watcher) } } //數據變換,就調watcher的update方法 notify () { this.subs.forEach(watcher => { watcher.update() }); } }
(5).Watcher
功能
- 當數據變化觸發依賴, dep 通知所有的 Watcher 實例更新視圖
- 自身實例化的時候往 dep 對象中添加自己
class Watcher { constructor (vm, key, callback) { this.vm = vm //data中的屬性名 this.key = key this.callback = callback //將watcher對象記錄在Dep的靜態屬性target Dep.target = this //觸發get方法,觸發get裏的addsubs方法,添加watcher this.oldValue = vm[key] Dep.target = null } //當數據變化的時候,更新視圖 update () { let newValue = this.vm[this.key] if (this.oldValue === newValue) { return } this.callback(newValue) } }
總結:
Vue
- 記錄傳入的選項,設置 $data/$el
- 把 data 的成員注入到 Vue 實例
- 負責調用 Observer 實現數據響應式處理(數據劫持)
- 負責調用 Compiler 編譯指令/插值表達式等
- 數據劫持
- 負責把 data 中的成員轉換成 getter/setter
- 負責把多層屬性轉換成 getter/setter
- 如果給屬性賦值爲新對象,把新對象的成員設置爲 getter/setter
- 添加 Dep 和 Watcher 的依賴關係
- 數據變化發送通知
- 負責編譯模板,解析指令/插值表達式
- 負責頁面的首次渲染過程
- 當數據變化後重新渲染
- 收集依賴,添加訂閱者(watcher)
- 通知所有訂閱者
- 自身實例化的時候往dep對象中添加自己
- 當數據變化dep通知所有的 Watcher 實例更新視圖