目錄html
Vue
數據獨立在 data
裏面,視圖在 template
中dom
操做被封裝
dom
來修改視圖,例如 ducument.getElementById('xx').innerHTML="xxx"
Model
數據 → View
視圖 → Controller
控制器MVVM
不算是一種創新ViewModel
是一種創新ViewModel
是真正結合前端應用場景的實現MVVM - Model View ViewModel
,數據,視圖,視圖模型Vue
的對應:view
對應 template
,vm
對應 new Vue({…})
,model
對應 data
view
能夠經過事件綁定的方式影響 model
,model
能夠經過數據綁定的形式影響到view
,viewModel
是把 model
和 view
連起來的鏈接器MVVM
框架的三大要素前端
Vue
如何監聽到 data
的每一個屬性變化Vue
的模板如何被解析,指令如何處理Vue
的模板如何被渲染成 html
,渲染過程是怎樣的data
屬性以後,Vue
馬上監聽到,馬上渲染頁面data
屬性被代理到 vm
上JavaScript
對象,作屬性修改,咱們監聽不到,因此須要用到 Object.defineProperty
html
來顯示vnode
,解決了模板中的邏輯(v-if, v-for)問題code.render
,將code打印出來,就是生成的render函數html
:vm._c
vm._c
和 snabbdom
中的 h
函數的實現很像,都是傳入標籤,屬性,子元素做爲參數Vue.js
的 vdom
實現借鑑了 snabbdom
updateComponent
中實現了 vdom
的 patch
updateComponent
data
中每次修改屬性,都會執行 updateComponent
render
函數
updateComponent
,執行 vm._render()
render
函數,會訪問到 data
中的值,訪問時會被響應式的 get
方法監聽到updateComponent
,會走到 vdom
的 patch
方法patch
將 vnode
渲染成 dom
,初次渲染完成get
,而不是直接監聽 set
?
data
中有不少屬性,有些被用到,有些可能不被用到get
get
中的屬性,set
的時候咱們也無需關心data
屬性變化,觸發 re-render
set
監聽到set
中執行 updateComponent
updateComponent
從新執行 vm._render()
vnode
和 prevVnode
,經過 patch
進行對比html
中index.htmlvue
這是最終的測試代碼,咱們本身實現的 Vue 在 XVue.js
和 compile.js
兩個文件中,加起來大概200行代碼左右,主要包括功能以下:node
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title></title> </head> <body> <div id="app"> {{test}} <div v-text="test"></div> <p> <input type="text" v-model="test" /> </p> <p v-html="html"></p> <p> <button @click="onClick">按鈕</button> </p> </div> <script src="./compile.js"></script> <script src="./XVue.js"></script> <script> const o = new XVue({ el: '#app', data: { test: '123', foo: { bar: 'bar' }, html: '<button>html test</button>' }, methods: { onClick() { alert('按鈕點擊了') } } }) console.log(o.$data.test) //123 o.$data.test = 'hello, Xvue!' console.log(o.$data.test) //hello, Xvue! </script> </body> </html>
Mini Vue 的組成部分:app
XVue.js框架
class XVue { constructor(options) { this.$data = options.data; this.observe(this.$data); // 執行編譯 new Compile(options.el, this); } observe(value) { if (!value || typeof value !== 'object') { return; } Object.keys(value).forEach(key => { this.defineReactive(value, key, value[key]); // 爲vue的data作屬性代理 this.proxyData(key); }); } defineReactive(obj, key, val) { // 遞歸查找嵌套屬性 this.observe(val); // 建立Dep const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 收集依賴 Dep.target && dep.addDep(Dep.target); // console.log(dep.deps); return val; }, set(newVal) { if (newVal === val) { return; } val = newVal; dep.notify(); }, }); } 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); } // 通知全部watcher執行更新 notify() { this.deps.forEach(dep => { dep.update(); }); } } // Watcher: 具體的更新執行者 class Watcher { constructor(vm, key, cb) { this.vm = vm; this.key = key; this.cb = cb; // 未來 new 一個監聽器時,將當前 Watcher 實例附加到 Dep.target // 未來經過 Dep.target 就能拿到當時建立的 Watcher 實例 Dep.target = this; // 讀取操做,主動觸發 get,當前 Watcher 實例被添加到依賴管理器中 this.vm[this.key]; // 清空操做,避免沒必要要的重複添加(再次觸發 get 就不須要再添加 watcher 了) Dep.target = null; } update() { // console.log('from Watcher update: 視圖更新啦!!!'); // 通知頁面作更新 this.cb.call(this.vm, this.vm[this.key]); } }
compile.jsdom
// 掃描模板中全部依賴(指令、插值、綁定、事件等)建立更新函數和watcher class Compile { // el是宿主元素或其選擇器 // vm當前Vue實例 constructor(el, vm) { this.$el = document.querySelector(el); this.$vm = vm; if (this.$el) { // 將dom節點轉換爲Fragment提升執行效率 this.$fragment = this.node2Fragment(this.$el); // 執行編譯,編譯完成之後全部的依賴已經替換成真正的值 this.compile(this.$fragment); // 將生成的結果追加至宿主元素 this.$el.appendChild(this.$fragment); } } node2Fragment(el) { // 建立一個新的Fragment const fragment = document.createDocumentFragment(); let child; // 將原生節點移動至fragment while ((child = el.firstChild)) { // appendChild 是移動操做,移動一個節點,child 就會少一個,最終結束循環 fragment.appendChild(child); } return fragment; } // 編譯指定片斷 compile(el) { let childNodes = el.childNodes; Array.from(childNodes).forEach(node => { // 判斷node類型,作相應處理 if (this.isElementNode(node)) { // 元素節點要識別v-xx或@xx this.compileElement(node); } else if ( this.isTextNode(node) && /\{\{(.*)\}\}/.test(node.textContent) ) { // 文本節點,只關心{{msg}}格式 this.compileText(node, RegExp.$1); // RegExp.$1匹配{{}}之中的內容 } // 遍歷可能存在的子節點 if (node.childNodes && node.childNodes.length) { this.compile(node); } }); } compileElement(node) { // console.log('編譯元素節點'); // <div v-text="test" @click="onClick"></div> const attrs = node.attributes; Array.from(attrs).forEach(attr => { const attrName = attr.name; // 獲取屬性名 v-text const exp = attr.value; // 獲取屬性值 test if (this.isDirective(attrName)) { // 指令 const dir = attrName.substr(2); // text this[dir] && this[dir](node, this.$vm, exp); } else if (this.isEventDirective(attrName)) { // 事件 const dir = attrName.substr(1); // click this.eventHandler(node, this.$vm, exp, dir); } }); } compileText(node, exp) { // console.log('編譯文本節點'); this.text(node, this.$vm, exp); } isElementNode(node) { return node.nodeType == 1; //元素節點 } isTextNode(node) { return node.nodeType == 3; //元素節點 } isDirective(attr) { return attr.indexOf('v-') == 0; } isEventDirective(dir) { return dir.indexOf('@') == 0; } // 文本更新 text(node, vm, exp) { this.update(node, vm, exp, 'text'); } // 處理html html(node, vm, exp) { this.update(node, vm, exp, 'html'); } // 雙向綁定 model(node, vm, exp) { this.update(node, vm, exp, 'model'); let val = vm.exp; // 雙綁還要處理視圖對模型的更新 node.addEventListener('input', e => { vm[exp] = e.target.value; // 這裏至關於執行了 set }); } // 更新 // 可以觸發這個 update 方法的時機有兩個:1-編譯器初始化視圖時觸發;2-Watcher更新視圖時觸發 update(node, vm, exp, dir) { let updaterFn = this[dir + 'Updater']; updaterFn && updaterFn(node, vm[exp]); // 當即執行更新;這裏的 vm[exp] 至關於執行了 get new Watcher(vm, exp, function (value) { // 每次建立 Watcher 實例,都會傳入一個回調函數,使函數和 Watcher 實例之間造成一對一的掛鉤關係 // 未來數據發生變化時, Watcher 就能知道它更新的時候要執行哪一個函數 updaterFn && updaterFn(node, value); }); } textUpdater(node, value) { node.textContent = value; } htmlUpdater(node, value) { node.innerHTML = value; } modelUpdater(node, value) { node.value = value; } eventHandler(node, vm, exp, dir) { let fn = vm.$options.methods && vm.$options.methods[exp]; if (dir && fn) { node.addEventListener(dir, fn.bind(vm), false); } } }