大多數初學者只知道vue中data變化,數據就會隨之變化,模版數據也會隨之變化。咱們須要有知其然而知其因此然的態度,下面就簡單的實現下數據響應式和模版渲染。html
MVVM框架的三要素:數據響應式、模板引擎及其渲染
數據響應式:監聽數據變化並在視圖中更新vue
模版引擎:爲模版語法翻譯node
渲染:把虛擬dom轉化爲真實dom面試
面試時候關於vue你們都會被問到其響應式原理,這個很簡單都知道是利用
Object.defineProperty()實現變動檢測,下面簡單實現下。app
每隔1秒obj.foo的值取當前時間,一直在變動,每次變動都會調用Object.defineProperty中的set方法,這時能拿到新的value值,通知update去更新視圖。框架
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"></div> <script> // 數據響應式 function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { console.log('get', key); return val }, set(newVal) { if (newVal !== val) { console.log('set', key, newVal); val = newVal // 更新函數 update() } }, }) } const obj = {} defineReactive(obj, 'foo', 'foo') function update() { app.innerText = obj.foo } setInterval(() => { obj.foo = new Date().toLocaleTimeString() }, 1000); </script> </body> </html>
最終效果圖爲以下,能夠看出每次obj.foo的變動都會觸發get和set方法。
dom
實現目標:counter變化時候模版語法獲得解析,nvue.js是咱們實現響應式的vue原理代碼。函數
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"> <p @click="add">{{counter}}</p> <p n-text="counter"></p> <p n-html="desc"></p> </div> <script src="nvue.js"></script> <script> const app = new NVue({ el:'#app', data: { counter: 1, desc: '<span style="color:red">數據響應</span>' }, methods: { add() { this.counter++ } }, }) setInterval(() => { app.counter++ }, 1000); </script> </body> </html>
原理分析
ui
1.首先如圖咱們須要在初始化過程當中,對data進行響應化處理,劫持監聽data內的全部屬性。
2.同時對模板執行編譯,找到其中動態綁定的數據,解析指令。例如解析上面中的n-text,從data中獲取並初始化視圖,這個過程發生在 Compile中。
3.同時定義一個更新函數和Watcher,未來對應數據變化時Watcher會調用更新函數。
4.因爲data的某個key在一個視圖中可能出現屢次,因此每一個key都須要一個管家Dep來管理多個Watcher。
5.data中數據一旦發生變化,會首先找到對應的Dep,通知全部Watcher執行更新函數。this
具體實現nvue
1.執行初始化,對data執行響應化處理,nvue.js,其中
// 數據響應式 function defineReactive(obj, key, val) {} // 讓咱們使一個對象全部屬性都被攔截observe function observe(obj) { if (typeof obj !== 'object' || obj == null) { return } // 建立Observer實例:之後出現一個對象,就會有一個Observer實例 new Observer(obj) } // 1.響應式操做 class NVue { constructor(options) { // 保存選項 this.$options = options this.$data = options.data; // 響應化處理 observe(this.$data) } } // 作數據響應化 class Observer { constructor(value) { this.value = value this.walk(value) } // 遍歷對象作響應式 walk(obj) { Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } }
2.爲$data作代理,這裏data中的參數都要寫成vm.$data[key],而咱們但願每次vm[key]更新時候就能夠觸發,因此這裏作了個代理,把vm.$data[key]值塞入vm[key]中
class NVue { constructor(options) { ... // 代理 proxy(this) // 編譯器 new Compiler('#app', this) } } // 代理data中數據,轉發做用 function proxy(vm) { Object.keys(vm.$data).forEach(key => { Object.defineProperty(vm, key, { get() { return vm.$data[key] }, set(v) { vm.$data[key] = v } }) }) }
3.編譯Compile,編譯模板中vue模板特殊語法,初始化視圖、更新視圖。
// Compiler: 解析模板,找到依賴,並和前面攔截的屬性關聯起來 // new Compiler('#app', vm) class Compiler { constructor(el, vm) { this.$vm = vm this.$el = document.querySelector(el) // 執行編譯 this.compile(this.$el) } compile(el) { // 遍歷這個el el.childNodes.forEach(node => { // 是不是元素,編譯元素 if (node.nodeType === 1) { this.compileElement(node) // 是否爲{{}}文本,編譯文本 } else if (this.isInter(node)) { this.compileText(node) } // 遞歸 if (node.childNodes) { this.compile(node) } }) } // 解析綁定表達式{{}} compileText(node) { // 獲取正則匹配表達式,從vm裏面拿出它的值 // node.textContent = this.$vm[RegExp.$1] console.log(RegExp.$1) this.update(node, RegExp.$1, 'text') } // 編譯元素 compileElement(node) { // 處理元素上面的屬性,典型的是n-,@開頭的 const attrs = node.attributes Array.from(attrs).forEach(attr => { // attr: {name: 'n-text', value: 'counter'} console.log(attr) const attrName = attr.name const exp = attr.value if (attrName.indexOf('n-') === 0) { // 截取指令名稱 text const dir = attrName.substring(2) // 看看是否存在對應方法,有則執行 this[dir] && this[dir](node, exp) } }) } // n-text text(node, exp) { // node.textContent = this.$vm[exp] this.update(node, exp, 'text') } // n-html html(node, exp) { // node.innerHTML = this.$vm[exp] this.update(node, exp, 'html') } // dir:要作的指令名稱 // 一旦發現一個動態綁定,都要作兩件事情,首先解析動態值;其次建立更新函數 // 將來若是對應的exp它的值發生變化,執行這個watcher的更新函數 update(node, exp, dir) { // 初始化 const fn = this[dir + 'Updater'] fn && fn(node, this.$vm[exp]) // 更新,建立一個Watcher實例 new Watcher(this.$vm, exp, val => { fn && fn(node, val) }) } // 更新v-text文本內容 textUpdater(node, val) { node.textContent = val } // 更新v-html文本內容 htmlUpdater(node, val) { node.innerHTML = val } // 文本節點且形如{{xx}} isInter(node) { return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent) } }
4.依賴收集
視圖中會用到data中某key,這稱爲依賴。同一個key可能出現屢次,每次都須要收集出來用一個Watcher來維護它們,此過程稱爲依賴收集。 多個Watcher須要一個Dep來管理,須要更新時由Dep統一通知,經過上面的原理分析圖能夠很容易看出。
實現思路:
1.在defineReactive
函數中爲每個key建立一個Dep實例。
2.初始化視圖每個key建立對應的一個watcher實例。
3.因爲觸發name1的getter方法,便將watcher1添加到name1對應的Dep中。
4.當name1更新,setter觸發時,即可經過對應Dep通知其管理全部Watcher更新。
聲明watcher和Dep
// 管理一個依賴,將來執行更新 class Watcher { constructor(vm, key, updateFn) { this.vm = vm this.key = key this.updateFn = updateFn // 讀一下當前key,觸發依賴收集 Dep.target = this vm[key] Dep.target = null } // 將來會被dep調用 update() { this.updateFn.call(this.vm, this.vm[this.key]) } } // Dep: 保存全部watcher實例,當某個key發生變化,通知他們執行更新 class Dep { constructor() { this.deps = [] } addDep(watcher) { this.deps.push(watcher) } notify() { this.deps.forEach(dep => dep.update()) } }
依賴收集,建立Dep實例
// 數據響應式 function defineReactive(obj, key, val) { // 遞歸處理 observe(val) // 建立一個Dep實例 const dep = new Dep() Object.defineProperty(obj, key, { get() { console.log('get', key); // 依賴收集: 把watcher和dep關聯 // 但願Watcher實例化時,訪問一下對應key,同時把這個實例設置到Dep.target上面 Dep.target && dep.addDep(Dep.target) return val }, set(newVal) { if (newVal !== val) { console.log('set', key, newVal); observe(newVal) val = newVal // 通知更新 dep.notify() } }, }) }
最終實現效果
那麼事件處理怎麼作呢,能夠本身試一下,後面文章也會說明。但願本文可以讓初學者對vue數據響應真正瞭解。