<!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"> <p>{{ name }}</p> <div v-text="name"></div> <input v-model="name" type="text"> </div> <script> // 手寫一個mvvm 簡易版的vuejs // options就是選項 全部vue屬性都帶$ function Vue (options) { this.subs = {} // 事件管理器 this.$options = options // 放置選項 this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el // 將dom對象賦值給$el 和官方vuejs保持一致 this.$data = options.data || {} // 數據代理 但願 vm可以代理 $data的數據 // 但願 vm.name 就是$data.name this.$proxyData() // 代理數據 把$data中數據 代理給vm實例 this.$observer() // 數據劫持 劫持 $data中的數據變化 this.$compile(this.$el) // 模板第一次編譯渲染 遞歸的要求 這裏必須傳入參數 // 遞歸是一種簡單的算法 => 通常用在處理樹形數據,嵌套數據 中國/北京/海淀/中關村/知春路/海淀橋/982/人 // 遞歸其實就是函數自身調用自身 => 傳入下一次遞歸的條件 => 兩次遞歸條件同樣 => 死循環了 } // 數據代理好的方法 Vue.prototype.$proxyData = function () { // this 指的就是 當前的實例 // key 就是 data數據中的每個key Object.keys(this.$data).forEach(key => { // 代理數據 讓vm實例代理$data中的數據 Object.defineProperty(this, key, { // 存取描述符 get () { return this.$data[key] // 返回$data中的數據 }, // 設置數據時 須要 將 值設置給 $data的值 並且要判斷設置以前數據是否相等 set (value) { // value是新值 若是新值等於舊值 就不必再設置了 if (this.$data[key] === value ) return this.$data[key] = value // 若是不等再設置值 } }) }) } // 數據劫持 Vue.prototype.$observer = function () { // 要劫持誰 ? $data // 遍歷 $data中的全部key Object.keys(this.$data).forEach(key => { // 劫持 =>劫持數據的變化 -> 監聽 data中的數據的變化 => set方法 // obj / prop / desciptor let value = this.$data[key] // 從新開闢一個空間 value的空間 Object.defineProperty(this.$data, key, { // 描述 => 描述符有幾種 ? 數據描述符(value,writable) 存取描述符 (get/set) get () { return value }, set: (newValue) => { if(newValue === value) return value = newValue // 一旦進入set方法 表示 MVVM中的 M 發生了變化 data變化了 // MVVVM => Model => 發佈訂閱模式 => 更新Dom視圖 // 總體編譯只執行一次 經過發佈訂閱模式來作 觸發一個事件 視圖層監聽一個事件 // 觸發一個事件 this.$emit(key) // 把屬性當成事件名 觸發一個事件 } }) }) } // 編譯模板 數據發生變化 => 模板數據更新到最新 // 編譯模板的一個總方法 構造函數執行時執行 // rootnode是傳入本次循環的根節點 => 找rootnode下全部的子節點 => 子節點 => 子節點=> 子節點 > 子節點 ... 找到沒有子節點爲止 Vue.prototype.$compile = function (rootnode) { let nodes = Array.from(rootnode.childNodes) // 是一個僞數組 將僞數組轉成真數組 nodes.forEach(node => { // 循環每一個節點 判斷節點類型 若是你是文本節點 就要用文本節點的處理方式 若是元素節點就要元素節點的處理方式 if(this.$isTextNode(node)) { // 若是是文本節點 this.$compileTextNode(node) // 處理文本節點 當前的node再也不有 子節點 沒有必要繼續找了 } if(this.$isElementNode(node)) { // 若是是元素節點 this.$compileElementNode(node) // 處理元素節點 // 若是是元素節點 下面必定還有子節點 只有文本節點纔是終點 // 遞歸了 => 自身調用自身 this.$compile(node) // 傳參數 保證一層一層找下去 找到 node.chidNodes的長度爲0的時候 自動中止 // 能夠保證 把 $el下的全部節點都遍歷一遍 } }) } // 處理文本節點 nodeType =3 Vue.prototype.$compileTextNode = function (node) { // console.log(node.textContent) // 拿到文本節點內容以後 要作什麼事情 {{ name }} => 真實的值 // 正則表達式 const text = node.textContent // 拿到文本節點的內容 要看一看 有沒有插值表達式 const reg = /\{\{(.+?)\}\}/g // 將匹配全部的 {{ 未知內容 }} if (reg.test(text)) { // 若是能匹配 說明 此時這個文本里有插值表達式 // 表示 上一個匹配的正則表達式的值 const key = RegExp.$1.trim() // name屬性 => 取name的值 $1取的是第一個的key node.textContent = text.replace(reg, this[key] ) // 獲取屬性的值 而且替換 文本節點中的插值表達式 this.$on(key, () => { // 若是 key這個屬性所表明的值發生了變化 回調函數裏更新視圖 node.textContent = text.replace(reg, this[key] ) // 把原來的帶大括號的內容替換成最新值 賦值給textContent }) } } // 處理元素節點 nodeType = 1的時候是元素節點 Vue.prototype.$compileElementNode = function (node) { // 指令 v-text v-model => 數據變化 => 視圖更新 更新數據變化 // v-text = '值' => innerText上 textContent // 拿到該node全部的屬性 let attrs = Array.from(node.attributes) // 把全部的屬性轉化成數組 // 循環每一個屬性 屬性是否帶 v- 若是帶 v- 表示指令 attrs.forEach(attr => { if (this.$isDirective( attr.name)) { // 判斷指令類型 if(attr.name === 'v-text') { // v-text的指令的含義是 v-text後面的表達的值 做用在 元素的innerText或者textContent上 node.textContent = this[attr.value] // 賦值 attr.value => v-text="name" this.$on(attr.value, () => { node.textContent = this[attr.value] //此時數據已經更新 }) } if(attr.name === 'v-model') { // 表示我要對當前節點進行雙向綁定 node.value = this[attr.value] // v-model要給value賦值 並非textContent this.$on(attr.value, () => { node.value = this[attr.value] //此時數據已經更新 }) node.oninput = () => { // 須要把當前最新的節點的值 賦值給 自己的數據 this[attr.value] = node.value // 視圖 發生 => 數據發生變化 } // 若是一個元素綁定了v-model指令 應該監聽這個元素的值改變事件 } } // 若是以 v-開頭表示 就是指令 }) } // 判斷一個節點是不是文本節點 nodeType ===3 Vue.prototype.$isTextNode = function (node) { return node.nodeType === 3 // 表示就是文本節點 } // 判斷 一個節點是不是元素節點 Vue.prototype.$isElementNode = function (node) { return node.nodeType === 1 // 表示就是元素節點 } // 判斷一個屬性是不是指令 全部的指令都以 v-爲開頭 Vue.prototype.$isDirective = function (attrname) { return attrname.startsWith('v-') } // Vue的發佈訂閱管理器 $on $emit // 監聽事件 Vue.prototype.$on = function (eventName, fn) { // 事件名 => 回調函數 => 觸發某個事件的時候 找到這個事件對應的回調函數 而且執行 // if(this.subs[eventName]) { // this.subs[eventName].push(fn) // }else { // this.subs[eventName] = [fn] // } this.subs[eventName] = this.subs[eventName] || [] this.subs[eventName].push(fn) } // 觸發事件 Vue.prototype.$emit = function (eventName, ...params) { // 拿到了事件名 應該去咱們的開闢的空間裏面 找有沒有回調函數 if(this.subs[eventName]) { // 有人監聽你的事件 // 調用別人的回調函數 this.subs[eventName].forEach(fn => { // 改變this指向 // fn(...params) // 調用該回調函數 而且傳遞參數 // 三種方式 改變回調函數裏的this指向 // fn.apply(this, [...params]) // apply 參數 [參數列表] // fn.call(this, ...params) // 若干參數 fn.bind(this, ...params)() // bind用法 bind並不會執行函數 而是直接將函數this改變 }); } } var vm = new Vue({ el: '#app', // 還有多是其餘選擇器 還有多是dom對象 data: { name: '呂布', wife: '貂蟬' } }) </script> </body> </html>