原文轉自 https://const_white.gitee.io/gitee-blog/blog/vue/mini-vue/
Vue響應式原理
圖片引自 孟思行 - 圖解 Vue 響應式原理
乞丐版 mini-vue
實現mini-vue之前,先看看官網的描述。在Vue官網,深入響應式原理中,是這樣說明的:
每個組件實例都對應一個 watcher實例,它會在組件渲染的過程中把「接觸」過的數據 property 記錄為依賴。之後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件重新渲染。
起步
技術原因,這裡不做Virtual DOM、render部分,而選擇直接操作DOM
簡單來說,mini vue在創建Vue實例時
Vue類負責把data中的屬性注入到Vue實例,並調用Observer類和Compiler類。Observer類負責數據劫持,把每一個data轉換成getter和setter。其核心原理是通過Object.defineProperty實現。Compiler類負責解析指令和插值表達式(更新視圖的方法)。Dep類負責收集依賴、添加觀察者模式。通知data對應的所有觀察者Watcher來更新視圖。在Observer類把每一個data轉換成getter和setter時,會創建一個Dep實例,用來負責收集依賴並發送通知。在每一個data中在getter中收集依賴。在setter中通知依賴,既通知所有Watcher實例新視圖。Watcher類負責數據更新後,使關聯視圖重新渲染。
實現代碼都添加了詳細的注釋,無毒無害,可放心查看
Vue類
classVue{constructor(options) {// 1. 保存 options的數據this.$options = options || {}this.$data = options.data || {}this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el// 2. 為方便調用(vm.msg),把 data中的成員轉換成 getter和 setter,並注入到 Vue實例中this._proxyData(this.$data)// 3. 調用 Observer類,監聽數據的變化new Observer(this.$data)// 4. 調用 compiler類,解析指令和插值表達式new Compiler(this) } _proxyData(data) {Object.keys(data).forEach(key => {Object.defineProperty(this, key, {enumerable: true,configurable: true,get() {return data[key] },set(newValue) {if (newValue === data[key]) {return } data[key] = newValue } }) }) }}Observer類
classObserver{constructor(data) {this.walk(data) }// 遍歷 data($data)中的屬性,把屬性轉換成響應式數據 walk(data) {if (!data || typeof data !== 'object') {return }Object.keys(data).forEach((key) => {this.defineReactive(data, key, data[key]) }) }// 定義響應式數據 defineReactive(obj, key, value) {const that = this// 負責收集依賴並發送通知let dep = new Dep()// 利用遞歸使深層(內部)屬性轉換成響應式數據this.walk(value)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get() {// 收集依賴 Dep.target && dep.addSub(Dep.target)return value },set(newValue) {if (value === newValue) {return } value = newValue// 如果新設置的值為對象,也轉換成響應式數據 that.walk(newValue)// 發送通知 dep.notify() } }) }}Compiler類
classCompiler{constructor(vm) {this.vm = vmthis.el = vm.$elthis.compiler(this.el) }// 編譯模板,處理文本節點和元素節點 compiler(el) {const childNodes = el.childNodesArray.from(childNodes).forEach(node => {// 處理文本節點if (this.isTextNode(node)) {this.compilerText(node) } elseif (this.isElementNode(node)) {// 處理元素節點this.compilerElement(node) }// 判斷 node節點是否有子節點。如果有,遞歸調用 compileif (node.childNodes.length) {this.compiler(node) } }) }// 編譯元素節點,處理指令 compilerElement(node) {// 遍歷所有屬性節點Array.from(node.attributes).forEach(attr => {// 判斷是否 v-開頭指令let attrName = attr.nameif (this.isDirective(attrName)) {// 為了更優雅的處理不同方法,減去指令中的 v- attrName = attrName.substr(2)const key = attr.valuethis.update(node, key, attrName) } }) }// 執行對應指令的方法 update(node, key, attrName) {let updateFn = this[attrName + 'Updater']// 存在指令才執行對應方法 updateFn && updateFn.call(this, node, this.vm[key], key) }// 處理 v-text指令 textUpdater(node, value, key) { node.textContent = value// 創建 Watcher對象,當數據改變時更新視圖new Watcher(this.vm, key, (newValue) => { node.textContent = newValue }) }// 處理 v-model指令 modelUpdater(node, value, key) { node.value = value// 創建 Watcher對象,當數據改變時更新視圖new Watcher(this.vm, key, (newValue) => { node.value = newValue })// 雙向綁定 node.addEventListener('input', () => {this.vm[key] = node.value }) }// 編譯文本節點,處理插值表達式 compilerText(node) {const reg = /\{\{(.+?)\}\}/let value = node.textContentif (reg.test(value)) {// 只考慮一層的對象,如 data.msg = 'hello world',不考慮嵌套的對象。且假設只有一個插值表達式。const 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 }}Dep類
classDep{constructor() {this.subs = [] }// 添加觀察者 addSub(sub) {if (sub && sub.update) {this.subs.push(sub) } }// 發送通知 notify() {this.subs.forEach(sub => { sub.update() }) }}Watcher類
classWatcher{constructor(vm, key, cb) {this.vm = vm// data中的屬性名this.key = key// 回調函數負責更新視圖this.cb = cb// 把 watcher對象記錄到 Dep類的靜態屬性 target中 Dep.target = this// 觸發 get方法,在 get方法中會調用 addSubthis.oldValue = vm[key] Dep.target = null }// 當數據發生變化的時候更新視圖 update() {const newValue = this.vm[this.key]// 數據沒有發生變化直接返回if (this.oldValue === newValue) {return }// 更新視圖this.cb(newValue) }}完整版思維導圖
對於數組的監聽
這裡直接把數組的每一項都添加上了getter和setter,所以vm.items[1] = 'x'也是響應式的。
Vue中為什麼沒這樣做呢?參考 為什麼vue沒有提供對數組屬性的監聽