博客原文html
本文經過仿照 Vue ,簡單實現一個的 MVVM,但願對你們學習和理解 Vue 的原理有所幫助。vue
nodeType 爲 HTML 原生節點的一個屬性,用於表示節點的類型。node
Vue 中經過每一個節點的 nodeType 屬性是1仍是3判斷是元素節點仍是文本節點,針對不一樣類型節點作不一樣的處理。git
DocumentFragment是一個能夠被 js 操做但不會直接出發渲染的文檔對象,Vue 中編譯模板時是現將全部節點存到 DocumentFragment 中,操做完後再統一插入到 html 中,這樣就避免了屢次修改 Dom 出發渲染致使的性能問題。github
Object.defineProperty接收三個參數 Object.defineProperty(obj, prop, descriptor)
, 能夠爲一個對象的屬性 obj.prop t經過 descriptor 定義 get 和 set 方法進行攔截,定義以後該屬性的取值和修改時會自動觸發其 get 和 set 方法。數組
如下代碼的 git 地址: 如下代碼的 git 地址
├── vue │ ├── index.js │ ├── obsever.js │ ├── compile.js │ └── watcher.js └── index.html
實現的這個 類 Vue 包含了4個主要模塊:app
{{}}
的語法;在 index.html 中是經過 new Vue() 來使用的:frontend
<div id="app"> <input type="text" v-model="msg"> {{ msg }} {{ user.name }} </div> <script> const vm = new Vue({ el: '#app', data: { msg: 'hello', user: { name: 'pan' } } }) </script>
所以入口文件需提供這個 Vue 的類並進行一些初始化操做:dom
class Vue { constructor(options) { // 參數掛載到實例 this.$el = document.querySelector(options.el); this.$data = options.data; if (this.$el) { // 數據劫持 new Observer(this.$data); // 編譯模板 new Compile(this.$el, this); } } }
index.js 中調用了 new Compile()
進行模板編譯,所以這裏須要提供一個 Compile 類:mvvm
class Compile { constructor(el, vm) { this.el = el; this.vm = vm; if (this.el) { // 將 dom 轉入 fragment 內存中 const fragment = this.node2fragment(this.el); // 編譯 提取須要的節點並替換爲對應數據 this.compile(fragment); // 插回頁面中去 this.el.appendChild(fragment); } } // 編譯元素節點 獲取 Vue 指令並執行對應的編譯函數(取值並更新 dom) compileElement(node) { const attrs = node.attributes; Array.from(attrs).forEach(attr => { const attrName = attr.name; if (this.isDirective(attrName)) { const expr = attr.value; let [, ...type] = attrName.split('-'); type = type.join(''); // 調用指令對應的方法更新 dom CompileUtil[type](node, this.vm, expr); } }) } // 編譯文本節點 判斷文本內容包含 {{}} 則執行文本節點編譯函數(取值並更新 dom) compileText(node) { const expr = node.textContent; const reg = /\{\{\s*([^}\s]+)\s*\}\}/; if (reg.test(expr)) { // 調用文本節點對應的方法更新 dom CompileUtil['text'](node, this.vm, expr); } } // 遞歸遍歷 fragment 中全部節點判斷節點類型並編譯 compile(fragment) { const childNodes = fragment.childNodes; Array.from(childNodes).forEach(node => { if (this.isElementNode(node)) { // 元素節點 編譯並遞歸 this.compileElement(node); this.compile(node); } else { // 文本節點 this.compileText(node); } }) } // 循環將 el 中每一個節點插入 fragment 中 node2fragment(el) { const fragment = document.createDocumentFragment(); let firstChild; while (firstChild = el.firstChild) { fragment.appendChild(firstChild); } return fragment; } isElementNode(node) { return node.nodeType === 1; } isDirective(name) { return name.startsWith('v-'); } }
這裏利用了 nodeType 區分 元素節點 仍是 文本節點,分別調用了 compileElement 和 compileText。
compileElement 及 compileText 中最終調用了 CompileUtil 的方法更新 dom。
CompileUtil = { // 獲取實例上對應數據 getVal(vm, expr) { expr = expr.split('.'); return expr.reduce((prev, next) => { return prev[next]; }, vm.$data); }, // 文本節點需先去除 {{}} 並利用正則匹配多組 getTextVal(vm, expr) { return expr.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (...arguments) => { return this.getVal(vm, arguments[1]); }) }, // 從 vm.$data 上取值並更新節點的文本內容 text(node, vm, expr) { expr.replace(/\{\{\s*([^}\s]+)\s*\}\}/g, (...arguments) => { // 添加數據監聽,數據變化時調用回調函數 new Watcher(vm, arguments[1], () => { this.updater.textUpdater(node, this.getTextVal(vm, expr)); }) }) this.updater.textUpdater(node, this.getTextVal(vm, expr)); }, // 從 vm.$data 上取值並更新輸入框內容 model(node, vm, expr) { // 添加數據監聽,數據變化時調用回調函數 new Watcher(vm, expr, () => { this.updater.modelUpdater(node, this.getVal(vm, expr)); }) // 輸入框輸入時修改 data 中對應數據 node.addEventListener('input', e => { const newValue = e.target.value; this.setVal(vm, expr, newValue); }) this.updater.modelUpdater(node, this.getVal(vm, expr)); }, updater: { textUpdater(node, value) { node.textContent = value; }, modelUpdater(node, value) { node.value = value; } } }
getVal 方法用於處理嵌套對象的屬性,如傳入表達式 expr 爲 user.name
的狀況,利用 reduce 從 vm.$data 上拿到。
index.js 中調用了 new Observer()
進行數據劫持,Vue 實例 data 屬性的每項數據都經過 defineProperty 方法添加 getter setter 攔截數據操做將其定義爲響應式數據,所以這裏首先須要提供一個 Observer 類:
class Observer { constructor(data) { // 遍歷 data 將每一個屬性定義爲響應式 this.observer(data); } observer(data) { if (!data || typeof data !== 'object') { return; } for (const [key, value] of Object.entries(data)) { this.defineReactive(data, key, value); // 當屬性爲對象則需遞歸遍歷 this.observer(value); } } // 定義響應式屬性 defineReactive(obj, key, value) { const that = this; const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: false, // 獲取數據時調用 get() { // 將 Watcher 實例存入依賴 Dep.target && dep.addSub(Dep.target); return value; }, // 設置數據時調用 set(newVal) { if (newVal !== value) { // 當新值爲對象時,需遍歷並定義對象內屬性爲響應式 that.observer(newVal); value = newVal; // 通知依賴更新 dep.notify(); } } }) } }
定義爲響應式數據後再對其取值和修改是會觸發對應的 get 和 set 方法。
取值時將改值自己返回,並先判斷是否有依賴目標 Dep.target,若是有則保存起來。
修改值時先手動將原值修改並通知保存的全部依賴目標進行更新操做。
這裏對每項數據都經過建立一個 Dep 類實例進行保存依賴和通知更新的操做,所以須要寫一個 Dep 類:
class Dep { constructor() { this.subs = []; } addSub(watcher) { this.subs.push(watcher); } notify() { this.subs.forEach(watcher => watcher.update()); } }
Dep 中有一個數組,用於保存數據的依賴目標(watcher),notify 遍歷全部依賴並調用其 update 方法進行更新。
經過上面的 Observer 能夠知道,每項數據在被調用時可能會有依賴目標,依賴目標須要被保存並在取值時調用 notify 通知更新,且經過 Dep 能夠知道依賴目標是一個有 update 方法的對象實例。
所以須要建立一個 Watcher 類:
class Watcher { constructor(vm, expr, cb) { this.vm = vm; this.expr = expr; this.cb = cb; // 記錄舊值 this.value = this.get(); } getVal(vm, expr) { expr = expr.split('.'); return expr.reduce((prev, next) => { return prev[next]; }, vm.$data); } get() { Dep.target = this; // 獲取 data 會觸發對應數據的 get 方法,get 方法中從 Dep.target 拿到 Watcher 實例 let value = this.getVal(this.vm, this.expr); Dep.target = null; return value; } // 對外暴露的方法,獲取新值與舊值對比後若不一樣則觸發回調函數 update() { let newValue = this.getVal(this.vm, this.expr); let oldValue = this.value; if (newValue !== oldValue) { this.cb(newValue); } } }
依賴目標就是 Watcher 的實例,對外提供了 update 方法,調用 update 時會從新根據表達式 expr 取值與老值對比並調用回調函數。
這裏的回調函數就是對應的更新 dom 的方法,在 compile.js 中的 model 及 text 方法中有執行 new Watcher()
,在模板解析時就爲每項數據添加了監聽:
model(node, vm, expr) { // 添加數據監聽,數據變化時調用回調函數 new Watcher(vm, expr, () => { this.updater.modelUpdater(node, this.getVal(vm, expr)); }) this.updater.modelUpdater(node, this.getVal(vm, expr)); },
Watcher 中很巧妙的一點就是,模板編譯以前已經將全部添加了數據攔截,在 Watcher 的 get 方法中調用 getVal 取值時會觸發該數據的 getter 方法,所以這裏在取值前經過 Dep.target = this;
將該 Watcher 實例暫存,對應數據的 getter 方法中又將該實例做爲依賴目標保存到了自身對應的 Dep 實例中。
這樣就實現了一個簡易的 MVVM 原理,裏面的一些思路仍是很是值得反覆體會學習的。