Vue源碼中實現依賴收集(觀察者模式),實現了三個類:
Dep
:扮演觀察目標
的角色,每一個數據都會有Dep
類實例,它內部有個subs隊列,subs就是subscribers的意思,保存着依賴本數據的觀察者
,當本數據變更時,調用dep.notify()
通知觀察者Watcher
:扮演觀察者
的角色,進行觀察者函數
的包裝處理。如render()
函數,會被進行包裝成一個Watcher
實例Observer
:輔助的可觀測類
,數組/對象通過它的轉化,可成為可觀測數據
index.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> </head> <body> <div id="app"> <div @click="getValue"> <p k-text="inputData"></p> </div> <p @click="getValue">阿斯加德八級考試{{ form.test }}你很快就發{{ form.test }}{{ obj.a.c }}</p> <p>{{ inputData }}</p> <input :value="inputData" @input="setInput" /> </div> </body> <!-- 瀏覽器不支持es6語法 --> <script src="./kvue.js"></script> <script> new Kvue({ el: 'app', data: { form: { test: '1124dsa' }, inputData: '', test: 111, obj: { a:{ c:3 } } }, watch: { inputData(val) { console.log(val) } }, created(){ console.log(this.test) console.log(this.obj) }, methods: { getValue() { console.log(this) }, setInput(event) { this.inputData = event.target.value } } }) </script> </html>
kvue.js
function getData(data, vm) { return data.call(vm, vm) } // 多層對象時獲取對象的值 function parsePath (path) { const segments = path.split('.'); return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) { return } obj = obj[segments[i]] } return obj } } function initMethods(vm, methods) { for(let key in methods) { vm[key] = typeof methods[key] !== 'function' ? function(){} : methods[key].bind(vm) } } function initWatch(vm, watch) { for(let key in watch) { new Watcher(vm, key, watch[key]) } } const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/ class Kvue{ constructor(option) { // 初始化data let data = option.data data = this.$data = typeof data === 'function' ? getData(data, this) : data || {} const keys = Object.keys(data) var i = keys.length while(i--) { this.proxyData(keys[i]) } this.observe(this.$data) // 初始化watch initWatch(this, option.watch) // 初始化methods initMethods(this, option.methods) // 模板編譯- new Compile(this,option.el) // 生命周期 if(option.created){ option.created.call(this) } } // 觀察者 observe(obj) { // 判斷是否符合標准,有值並且是個對象,如果不是對象則不進行遍歷操作 if(!obj || Object.prototype.toString.call(obj) !== '[object Object]') return // 遍歷obj對象的屬性,獲取屬性值后執行數據響應化處理 Object.keys(obj).forEach((key) => { // 響應化處理 this.defineProperty(obj, key, obj[key]) }) } defineProperty(obj,key,val) { // 調用觀察者,如果val是對象會再執行遍歷對象屬性值的操作 this.observe(val) // 對象的每個屬性都會執行defineProperty方法,也意味着每個屬性都有一個dep實例, // 用addDep來收集這個屬性在使用的時候的Dep.tergat(前提是Dep.tergat有值) // 使用數組是因為一個屬性在模板中可能有多個地方都在用 const dep = new Dep() Object.defineProperty(obj, key, { get(){ // 依賴收集-收集 Watcher Dep.tergat&&dep.addDep(Dep.tergat) return val }, set(newVal) { if(newVal === val) { return } val = newVal // 當數據發生變化時執行 dep.notify() } }) } // 代理函數-一次賦值就可以影響this[key]和this.$data[key],也不會影響this.$data綁定的依賴 proxyData(key) { Object.defineProperty(this, key, { get() { return this.$data[key] }, set(newVal) { this.$data[key] = newVal } }) } } class Dep { constructor() { // deps用來儲存watcher this.deps = [] } addDep(dep) { this.deps.push(dep) } notify() { // 更新watcher方法 this.deps.forEach((watcher) => { watcher&&watcher.update() }) } } // 模板編譯的時候會獲取watcher class Watcher { constructor(vm,key,cb) { this.vm = vm this.key = key // 將Dep.tergat綁定上watcher Dep.tergat = this // 獲取this.vm[key]的時候會執行key的get方法,從而將Watcher收集到deps parsePath(this.key)(this.vm) // 回調方法用來更新模板內容 this.cb = cb // 初始化更新 this.update() } update() { this.cb.call(this.vm, parsePath(this.key)(this.vm)) } } class Compile { // vm是指vue的this,el用來獲取html數據 constructor(vm, el) { this.$vm = vm this.$el = document.getElementById(el) // 如果存在$el節點 if(this.$el) { this.$fragment = this.nodeFragment(this.$el) // 執行編譯 this.compile(this.$fragment) // 將編譯后的元素添加到el this.$el.appendChild(this.$fragment) } } nodeFragment(el) { // DocumentFragment節點不屬於文檔樹,繼承的parentNode屬性總是null。 // 它有一個很實用的特點,當請求把一個DocumentFragment節點插入文檔樹時,插入的不是DocumentFragment自身,而是它的所有子孫節點,即插入的是括號里的節點。 // 這個特性使得DocumentFragment成了占位符,暫時存放那些一次插入文檔的節點。它還有利於實現文檔的剪切、復制和粘貼操作。 // 另外,當需要添加多個dom元素時,如果先將這些元素添加到DocumentFragment中,再統一將DocumentFragment添加到頁面,會減少頁面渲染dom的次數,效率會明顯提升。 // 如果使用appendChid方法將原dom樹中的節點添加到DocumentFragment中時,會刪除原來的節點 // 創建一個虛擬的節點對象 const frag = document.createDocumentFragment() // 將el的子元素添加到createDocumentFragment節點 let child while (child = el.firstChild) { // 使用appendChid方法在向frag添加子元素的同時刪除了el的子元素 frag.appendChild(child) } return frag } compile(frag) { const nodes = frag.childNodes || [] // Array.from將nodelist轉為可以循環的數組 Array.from(nodes).forEach((node, index) => { // html文檔中的回車空格等也是一個node節點(#text) // console.log(frag,node,node.nodeType) if(this.isElement(node)) { // 如果是一個元素節點則獲取它的attributes,根據attributes來獲取指令和方法綁定等 const attributes = node.attributes Array.from(attributes).forEach((attr) => { const name = attr.name const value = attr.value if(this.isDirective(name)){ // 獲取指令名稱 const directive = name.substring(2) // 如果存在這個指令,則執行這個指令 this[directive] && this[directive](node, this.$vm, value) } if(this.isEvent(name)) { // 指定事件名。 const event = name.substring(1) this.eventHandler(node, this.$vm, event, value) } }) } if(this.isTextNode(node)) { this.textNode(node, this.$vm) } if(node.childNodes){ // 遞歸 this.compile(node) } }) } update(node, vm, exp, type) { const updateFn = this[`update${type}`] // 依賴綁定 new Watcher(vm,exp,function(value){ updateFn&&updateFn(node,value) }) } // nodeType 屬性返回以數字值返回指定節點的節點類型 // Node.ELEMENT_NODE 1 一個 元素 節點,例如 <p> 和 <div>。 // Node.TEXT_NODE 3 Element 或者 Attr 中實際的 文字 // Node.CDATA_SECTION_NODE 4 一個 CDATASection,例如 <!CDATA[[ … ]]>。 // Node.PROCESSING_INSTRUCTION_NODE 7 一個用於XML文檔的 ProcessingInstruction ,例如 <?xml-stylesheet ... ?> 聲明。 // Node.COMMENT_NODE 8 一個 Comment 節點。 // Node.DOCUMENT_NODE 9 一個 Document 節點。 // Node.DOCUMENT_TYPE_NODE 10 描述文檔類型的 DocumentType 節點。例如 <!DOCTYPE html> 就是用於 HTML5 的。 // Node.DOCUMENT_FRAGMENT_NODE 11 一個 DocumentFragment 節點 // 是否是元素節點 isElement(node) { return node.nodeType === 1 } isTextNode(node) { return node.nodeType === 3 } // 是否是指令,以k-開頭 isDirective(attrName) { return attrName.startsWith('k-') } // 是否是方法 isEvent(attrName) { return attrName.startsWith('@') } /* * @作用: text指令函數 * @params: node 操作的節點 * @params: vm kvue的實例 * @params: exp 節點的屬性value值(以此來綁定對應的kvue的data) */ text(node, vm, exp) { // 綁定更新方法 this.update(node, vm, exp, 'Text') } textNode(node, vm) { const execs = defaultTagRE.exec(node.textContent) if(execs){ const exp = execs[1].trimStart().trimEnd() this.update(node, vm, exp, 'TextNode') // 有多個{{}}時需要進行遞歸修改 this.textNode(node, vm) } } // 文本指令更新方法 updateText(node, value) { node.textContent = value } // 更新文本節點信息 updateTextNode(node, value) { const textContent = node.textContent if(textContent) { node.textContent = textContent.replace(defaultTagRE, value) } } // 綁定方法 eventHandler(node, vm, event, exp) { const fn = vm[exp] node.addEventListener(event,fn) } }