兩百行代碼實現簡易vue框架

本文主要是經過vue原理及特色本身實現的簡易vue框架,和源碼相比不乏有些粗糙,可是對於JavaScript功底薄、閱讀源碼有些困難的同窗來講,也算是一種探究vue原理的有效方式。html

因此本文適合如下同窗閱讀vue

  • 已經會使用vue框架的常見功能
  • JavaScript功底較弱
  • 迫切想了解vue原理,但閱讀vue源碼感到困難

後續我會繼續實現更多的功能,若是有更好的實現方法,也能夠一塊兒交流改進,歡迎指教。node

源碼地址:github.com/mmdctjj/vue…react

在開始前,有必要介紹下幾個JavaScript函數git

1、準備

1. Object.defineProperty(obj, prop, desc)

功能:給對象定義屬性github

參數:數組

  • obj: 目標對象
  • prop: 定義的屬性
  • desc: 屬性描述符
var obj = {
    name: "曉明",
    age: 18
};
Object.defineProperty(obj, "info", {
    get: function () {
        return "我是黃" + this.name + ", 都聽個人";
    },
    set: function (nv) {
        this.name = nv;
    }
});
console.log(obj.info); // 我是黃曉明,都聽個人
obj.info = "總裁";
console.log(obj.info); // 我是黃總裁,都聽個人
複製代碼

2. Object.keys(obj)

功能:枚舉對象屬性app

參數:對象框架

返回:包含各個屬性的字符串數組對象dom

Object.keys(obj).forEach(key => {
     console.log(key, obj[key])
 })
 
 // name:曉明
 // age:18
複製代碼

3. Array.prototype.slice.call()

功能:給類數組對象添加數組的slice方法

參數:類數組對象

function args(n1,n2,n3){
    Array.prototype.slice.call(arguments).forEach(arg => {
        console.log(arg)
    })
}

args(1,2,3)

// 1
// 2
// 3
複製代碼

4. Node.nodeType

功能:返回Node的節點類型

返回值:1 表明元素節點;2 表明屬性節點;3 表明文本節點

5.RegExp

RegExp是JavaScript內置的正則構造函數

var regex1 = /\w+/; // 字面量方法
var regex2 = new RegExp('\\w+'); // 內置對象實例化建立
複製代碼

實例的方法:

test()

exec()

RegExp靜態屬性: $_

$1-$9

$`: 匹配左側文本,對應leftContent

$':匹配右側文本,對應rightContent

let reg = /\{\{(.*)\}\}/
let textContent = '123{{name}}456'
reg.test(textContent)

console.log(RegExp.$_) // 123{{name}}456
console.log(RegExp.$1) // name
console.log(RegExp["$`"]) // 123
console.log(RegExp["$'"]) // 456
複製代碼

另外還得說說正式的vue框架的特色,這對後面的實現是大有裨益的。

2、瞭解vue特色

1. mvvm模式

衆所周知,vue屬於mvvm模式,mvvm模式m表明數據model。v表明view視圖,而vm表明將view和model聯繫起來的橋樑

一個mvvm框架工做的基本原理就是vm經過解析模板對數據的需求,將model的數據渲染在view層供用戶預覽和交互,同時接受用戶的交互,根據交互內容,修改model中對應的數據,同時改變依賴該數據的view層節點,更新顯示的數據。用流程表示以下:

2. vue的基本特性

  1. 數據代理

數據代理是指Vue(構造函數)經過Object.defineProperty把data選項的屬性以getter和setter的方式所有轉vue實例的根屬性。以下例,訪問實例的a和訪問data的a是等價的

var data = { a: 1 }

// 直接建立一個實例
var vm = new Vue({
  data: data
})
vm.a === data.a // => true
複製代碼
  1. 能夠解析模板
  2. 支持事件綁定
  3. 經過數據劫持實現響應式

如今就開始構建本身的框架吧

3、實現

1.數據代理

實現數據代理的思路就是將data每一個屬性添加到vm實例上,這樣能夠經過訪問實例的屬性值來更改data中的屬性值,在理解了Object.defineProperty方法後實現是很簡單的,以下

class Vue {
  constructor(options) {
    let vm = this;
    this.$options = options;
    this._data = this.$options.data;
    Object.keys(this._data).forEach(key => {
      vm._proxy(key);
    });
  }
  _proxy(key) {
    let vm = this;
    Object.defineProperty(vm, key, {
      configurable: false,
      enumerable: true,
      get: () => vm._data[key],
      set: newVal => (vm._data[key] = newVal)
    });
  }
}
複製代碼

2.模板解析

實現模板解析須要分析模板解析作了哪些事,而後才能一層一層的實現模板解析的功能。

總的來講,模板解析的時候作了以下三件事

  1. 將節點取出來
  2. 生成新的dom節點
  3. 將生成的dom插入頁面

代碼實現以下

// 這裏須要說明下如何獲取渲染的節點,
// 在vue中,一般會指定一個dom元素做爲容器,來掛載全部的vue組件
// 在讀取渲染的節點時,就是從這個容器開始一層一層的解析dom節點
// 獲取每一個節點的屬性和文本節點,亦或是子節點
// 因此在建立編譯類是須要將el做爲參數傳入,同時也須要vm實例
// 方便獲取實例的屬性值
class Compile {
	constructor(el, vm) {
		this.$vm = vm
		this.$el = document.querySelector(el);
		if (this.$el) {
			// 1.將el元素節點取出來
			this.$fragment = this.createFragmentObj(this.$el)
			// 2.生成相應的dom節點
			this.createVdom(this.$fragment);
			// 3.將生成的dom插入到頁面
			this.$el.appendChild(this.$fragment)
		}
	}
}
複製代碼

可是在實現每一步的 時候由於節點類型的不一樣,須要作不一樣的處理,接下來作進一步的分析。

首先,模板解析的時候須要讀取到須要渲染數據的節點,以及須要的是哪些數據;

vue裏使用插值表達式來存放須要渲染的屬性或者變量,另外還能夠經過v-text和v-html指令綁定須要渲染的屬性,

因此根據須要渲染的節點類型,分爲屬性節點和文本節點,

使用代碼實現上述過程以下:

class Compile {
	constructor(el, vm) {
		this.$vm = vm
		this.$el = document.querySelector(el);
		if (this.$el) {
			// 1.將el元素節點取出來
			this.$fragment = this.createFragmentObj(this.$el)
		}
	}
	// 建立fragment對象
	createFragmentObj(el) {
		let fragment = document.createDocumentFragment();
		let child;
		while (child = el.firstChild) {
			fragment.appendChild(child);
		}
		return fragment;
	}
}
複製代碼
其次,根據須要的屬性,生成相應的dom;
// 建立dom
createVdom(fragment) {
	// 取出全部子節點
	let childNodes = fragment.childNodes;
	Array.prototype.slice.call(childNodes).forEach(childNode => {
		// 取出文本節點
		let text = childNode.textContent;
		// 匹配出大括號表達式
		let reg = /\{\{(.*)\}\}/;
		// 根據節點類型分別編譯
		// 若是是文本節點而且包含大括號表達式
		if (childNode.nodeType === 3 && reg.test(text)) {
			// 若是是文本節點
			this.compileText(childNode, RegExp.$1);
		} else if (childNode.nodeType === 1 && !childNode.hasChildNodes()) {
			// 若是是dom節點且沒有子元素
			this.compileInnerHTML(childNode);
		} else if (childNode.nodeType === 1 && childNode.hasChildNodes()) {
			// 若是是dom節點而且還有子元素就調用createVdom回到上面(其實這是遞歸的方法)
			this.createVdom(childNode);
		}
	});
}
複製代碼

在屬性節點過濾找到渲染的指令,以及對應的屬性名稱;

// 編譯innerHTML節點
compileInnerHTML(node) {
	Object.keys(node.attributes).forEach(key => {
		let exp = node.attributes[key]["nodeValue"];
		let val = this.$vm[node.attributes[key]["nodeValue"]];
		// 普通指令渲染
		switch (node.attributes[key]["nodeName"]) {
			case "v-text":
				this.updataDomVal(node, exp, "textContent", val);
				break;
			case "v-html":
				this.updataDomVal(node, exp, "innerHTML", val);
				break;
		}
	});
}
複製代碼

在文本節點,須要匹配插值表達式,以及表達式中的屬性名稱。

// 編譯文本節點
compileText(node, temp) {
	this.updataDomVal(node, temp, "textContent", this.$vm[temp]);
}
複製代碼

原本各個節點的更新均可以在各自的處理函數中完成更新,可是前面說過,mvvm會在每一個更新的節點設置監聽器Watcher,當這個節點的屬性值發生變化時會通知全部依賴這個屬性的節點做出更新,若是這樣咱們依然在每一個節點的處理函數裏設置監聽器就顯得十分笨重和多餘,因此這裏將全部的更新封裝在一個函數裏了,這樣會使代碼簡潔不少

// 更新節點
updataDomVal(node, exp, domType, domValue) {
    // 你不懂Watcher類不要緊,先忽略這些,後面會慢慢講到這個類
	// 標記每一個使用data屬性對象的dom節點位置, 並一直監聽,當有變化時,會被dep實例捕獲
	new Watcher(this.$vm, node, exp, (newVal, oldVal) => {
		node[domType] = newVal;
	});
	// 這裏是具體的賦值
	node[domType] = domValue;
}
複製代碼
最後,將生成的dom插入到當前節點;
// 將生成的Vdom插入到頁面
// 和傳統的dom操做不一樣,這樣的操做能夠減小頻繁操做dom的性能損耗
this.$el.appendChild(this.$fragment)
複製代碼

3.事件綁定

作完上面的工做,一個簡易的vue渲染功能已經完成了,做爲和用戶的交互平臺,最重要的就是交互,因此接下來實現事件綁定機制。

事件綁定和使用指令十分相似,都是利用節點的attributes屬性來實現的,只是指令的名稱不用,事件綁定專用的指令是v-on,因此將全部有v-on的屬性過濾出來,在methods中尋找綁定的方法

compileInnerHTML(node) {
	Object.keys(node.attributes).forEach(key => {
        // 事件指令解析
        if (node.attributes[key]["nodeName"].search("v-on") != -1) {
            // 獲取事件類型
        	let eventType = node.attributes[key]["nodeName"].split(":")[1];
        	// 獲取事件名稱
        	let eventName = node.attributes[key]["nodeValue"];
        	// 在methods中尋找綁定的方法
        	let event = this.$vm.$options.methods[eventName];
        	// 給當前節點添加相應事件
        	node.addEventListener(eventType, () => {
        	    // 將事件中的this指定爲vm實例
        		event.bind(this.$vm)();
        	});
        	// 執行完以後移除相應事件
        	node.removeEventListener(eventType, () => {
        		event.bind(this.$vm)();
        	});
        }
    }
}
複製代碼

4.響應式系統

關於響應式原理,官網說的已經有過專門的介紹。和官網不一樣的是,我沒有使用組件,或者你能夠將每一個使用屬性的節點當作一個組件,每一個使用過的屬性的地方都對應一個 watcher,實例在第一次渲染的時候把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的setter觸發時,會通知 watcher從而使它關聯的屬性的節點從新渲染

在實現響應式系統以前,咱們須要理清依賴和監聽器的對應關係,搞清楚這個,整個過程就會一目瞭然。爲了說明它們關係,我特地作了一個關係圖來幫助理解

首先,只要視圖中使用了某個屬性,就會爲該屬性實例化一個依賴類,該類會有一個訂閱列表subList,來存放全部使用該屬性的節點的watcher;

// 這個標誌是爲了保存watcher實例
Dep.target = null
// 建立依賴類,捕獲每一個監聽點的變化
class Dep {
	constructor() {
		this.subList = [];
	}
	// 創建依賴給dep和watcher
	depend() {
		Dep.target.addDep(this)
	}
	// 添加watcher到sublist中
	addSub(sub) {
		this.subList.push(sub)
	}
	// 通知全部watcher值改變了
	notify(newVal) {
		this.subList.forEach(sub => {
			sub.updata(newVal)
		})
	}
}
複製代碼

其次,每一個使用屬性的節點都會實例化一個watcher類,該類就是監聽器,它關聯了屬性和節點,當屬性的setter被觸發時會通知節點從新渲染。

let uid = 0;
// 建立監聽類,監聽每一個渲染數據地方
class Watcher {
	constructor(vm, node, exp, callback) {
		// 每一個watcher的惟一標識
		this.uid = uid++;
		this.$vm = vm;
		// 每一個watcher監聽節點
		this.node = node;
		// 每一個watcher監聽節點的屬性名稱表達式
		this.exp = exp;
		// 每一個watcher監聽節點的回調函數
		this.callback = callback;
		// 每一個watcher監聽的節點列表
		this.depList = {};
		// 每一個監聽節點的初始值
		this.value = this.getVal();
	}
	addDep(dep) {
		if (!this.depList.hasOwnProperty(dep.uid)) {
			dep.addSub(this);
		}
	}
	updata(newVal) {
		this.callback.call(this.$vm, newVal, this.value)
	}
	getVal() {
		// 獲取值時將當前watcher指向Dep.target,方便在數據劫持get函數裏創建依賴關係
		Dep.target = this;
		// 獲取當前節點位置值
		let val = this.$vm[this.exp];
		// 獲取完以後將Dep.target設置爲null
		Dep.target = null;
		return val;
	}
}
複製代碼

須要重點說明的是並非直接在數據代理的時候就創建watcher和dep聯繫的,由於有的時候會直接給vm實例添加新的屬性,可是data中並不存在該屬性,這也是官網特地說明要注意的,正確的作法是在data對象裏檢測屬性的變化觸發setter,因此正在數據變化到觸發watcher總共經歷了兩次setter,第一次是數據代理時觸發的setter,在該setter觸發了data中屬性的setter

// 建立觀察者類,觀察data屬性的變化
class Observer {
	constructor(data, vm) {
		this.data = data;
		this.$vm = vm;
		this.walk();
	}
	walk() {
		Object.keys(this.data).forEach(key => {
			this.defineReactive(key, this.data[key]);
		})
	}
	defineReactive(key, val){
		// 每一個屬性實例化dep對象,存放它全部的監聽者
		let dep = new Dep();
		// 從新定義data對象的屬性,以便給屬性添加get方法和set方法
		Object.defineProperty(this.data, key, {
			configurable: false,
			enumerable: true,
			get: () => {
				if (Dep.target) {
					dep.depend();
				}
				return val;
			},
			set: (newVal) => {
				if (val !== newVal) {
					dep.notify(newVal);
				}
				val = newVal;
				return
			}
		})
	}
}
複製代碼

全部須要的類都已經實現了,在實例化vue過程當中,會開始一系列的工做

每一個 Vue 實例在被建立時都要通過一系列的初始化過程——例如,
須要設置數據監聽、編譯模板、將實例掛載到 DOM 並在數據變化
時更新 DOM 等。同時在這個過程當中也會運行一些叫作生命週期鉤
子的函數,這給了用戶在不一樣階段添加本身的代碼的機會。
複製代碼

因此,還須要將監聽數據變化、模板編譯的過程加入到實例化vm的過程當中

class Vue {
	constructor(options) {
		let vm = this;
		this.$options = options;
		this._data = this.$options.data;
		// 代理data中的每一個屬性
		Object.keys(this._data).forEach(key => {
			vm.proxy(key);
		});
		// 劫持data中的屬性,當值發生變化時從新編譯變化的節點
		new Observer(this._data, vm)
		// 編譯節點到頁面
		this.$compile = new Compile(
			this.$options.el ? this.$options.el : document.body,
			vm
		);
	}
}
複製代碼

以上就是全部的實現過程,謝謝你們

相關文章
相關標籤/搜索