以前項目中一直在用vue,也邊作邊學摸滾打爬了近一年。對一些基礎原理性的東西有過了解,可是不深刻,例如面試常常問的vue的響應式原理,可能大多數人都能答出來Object.defineProperty進行數據劫持,可是深刻其實現細節,仍是有不少以前沒考慮到的東西,例如依賴收集後如何通知訂閱器,以及訂閱發佈模式如何實現等等。過程當中讀了部分源碼,受益不淺,除此以外,動手去實現它也是個很棒的學習方式,話很少說,看代碼,倉庫地址。javascript
vue的更新機制咱們簡單歸納一下就是,先對template進行解析,若檢測到template中使用了data中定義的屬性,則生成一個對應的watcher,經過劫持getter進行依賴(即watcher)收集,收集的內容保存在訂閱器Dep,經過劫持setter作到改變屬性從而通知訂閱器更新,那麼咱們首先要作的就是對屬性進行劫持。
vue2.0中使用的是Object.defineProperty,有傳言說vue 3.0將會使用Proxy來代替Object.defineProperty,其有諸多好處:vue
Proxy目前來看惟一的缺點就是兼容性可能存在問題,不過無傷大雅,咱們也順應潮流,使用Proxy來實現數據劫持,代碼很簡單:java
/** * 接受一個對象,對屬性進行依賴追蹤 */ function observable(obj) { const dep = new Dep() const proxy = new Proxy(obj, { get(target, property) { const value = target[property] if (value && typeof value === 'object') { // 若屬性爲object,遞歸處理 target[property] = observable(value) } if (Dep.target) { // Dep.target指向當前watcher dep.addWatcher(Dep.target) } return target[property] }, set(target, property, value) { target[property] = value dep.notify() // 通知訂閱器 } }) return proxy }
注意該方法須要返回proxy實例,由於只有經過proxy實例訪問屬性才具備劫持效果。咱們能夠看到代碼中有一個Dep,這個東西便是訂閱器,能夠理解爲它維護了一個依賴(watcher)的數組,並實現了一些管理數據的方法諸如addWatcher添加依賴,以及須要提供一個notify方法來遍歷全部的watcher執行其相應的更新函數,一樣代碼很簡單:git
/** * 依賴收集器,存放全部的watcher,並提供發佈功能(notify) */ class Dep { constructor() { this.watchers = [] } addWatcher(watcher) { // 添加watcher this.watchers.push(watcher) } notify() { // 通知方法,調用即依次遍歷全部watcher執行更新 this.watchers.forEach((watcher) => { watcher.update() }) } }
最後咱們來看下watcher,咱們知道watcher即咱們所說的依賴,它是在編譯template的時候,若找到data中聲明的屬性,即會生成一個對應的watcher實例,觸發依賴收集,加入訂閱器。同時還須要提供一個update函數,在觸發notify的時候調用來更新視圖,代碼以下:github
/** * watcher即所謂的依賴,監聽具體的某個屬性 */ class Watcher { constructor(proxy, property, cb) { this.proxy = proxy this.property = property this.cb = cb this.value = this.get() } update() { // 執行更新 const newValue = this.proxy[this.property] if (newValue !== this.value && this.cb) { // 對比property新舊值,決定是否更新 this.cb(newValue) } } get() { // 只在初始化時調用,用於依賴收集 Dep.target = this // 將自身指向Dep.target,執行完依賴收集再去釋放 const value = this.proxy[this.property] Dep.target = null return value } }
至此,響應式原理大體已經成形,接着咱們只要寫一個簡易的模板解析,demo就能跑起來啦。我這邊的實現比較挫,僅僅是經過正則匹配來實現了一個不帶diff的virture dom,純屬娛樂,重點仍是在實現響應式原理上,這邊貼一下代碼:面試
let init = false // 只在初始化時去生成watcher const eventMap = new Map() // 存放事件 const root = document.getElementById('root') // 根節點 /** * 用於將傳入RayActive的vm對象進行代理,可經過this.xx訪問this.data.xx * @param {Object} vm * @param {Proxy} proxydata 通過proxy代理的vm.data對象,使this.xx操做也能觸發視圖更新 */ function vmProxy(vm, proxydata) { return new Proxy(vm, { get(target, property) { return target.data[property] || target.methods[property] }, set(target, property, value) { proxydata[property] = value } }) } /** * 編譯vm,分別對data和render作相應處理 * @param {Object} vm 須要被編譯的vm對象 */ function compile(vm) { const proxydata = compileData(vm.data) compileRender(proxydata, vm.render) bindEvents(vm, vmProxy(vm, proxydata)) } /** * * @param {Object} data 須要被編譯的vm中的data對象 */ function compileData(data) { return observable(data) } /** * * @param {*} render 須要被編譯的render字符串 * @param {*} proxydata 經proxy轉換過的data */ function compileRender(proxydata, render) { if (render) { const variableRegexp = /\{\{(.*?)\}\}/g const variableResult = render.replace(variableRegexp, (a, b) => { // 替換變量爲相應的data值 if (!init) { // 只在初始化時去生成watcher new Watcher(proxydata, b, function() { compileRender(proxydata, render) }) } return proxydata[b] }) const eventRegexp = /(?<=<.*)@(.*)="(.*?)"(?=.*>)/ const result = variableResult.replace(eventRegexp, (a, b, c) => { // 爲綁定事件的標籤添加惟一id標識 const id = Math.random().toString(36).slice(2) eventMap.set(id, { type: b, method: c }) return a + ` id=${id}` }) init = true root.innerHTML = result } } /** * 經過root節點作事件代理,綁定模板中聲明的事件 * @param {*} vm * @param {*} proxyvm 通過proxy代理的vm */ function bindEvents(vm, proxyvm) { for (let [key, value] of eventMap) { root.addEventListener(value.type, (e) => { const method = vm.methods[value.method] if (method && e.target.id === key) { method.apply(proxyvm) // 將vm中methods方法的this指向通過proxy的vm對象 } }) } } /** * 可理解爲Vue中的Vue類,使用方式爲new RayActive(vm) */ class RayActive { constructor(vm) { compile(vm) } }
這個簡易實現僅僅是幫助你們學習vue的一些原理性的東西,跟vue比其餘來只是冰山一角。這個代碼還有很大的優化空間,好比執行notify時這裏會通知全部的watcher等等,值得有空去研究一下。同時,咱們能看到訂閱發佈模式帶來的好處。若是不引入訂閱器,那咱們更新dom的代碼得放到setter中去,那麼就耦合了數據劫持與操做dom的邏輯。引入訂閱器,能讓咱們在proxy中僅僅作依賴收集和通知的操做,剩下的各類複雜的或是個性化的邏輯能夠放到watcher中去實現,完美作到了關注點分離。數組