Vue由于其高效的性能和靈活入門簡單、輕量的特點(diǎn)下變得火熱。在當(dāng)今前端越來越普遍的使用,今天來動(dòng)手寫一下Vue
主要實(shí)現(xiàn)
1.構(gòu)建Vue實(shí)例查找指令和模板
2.數(shù)據(jù)驅(qū)動(dòng)界面更新
3.界面驅(qū)動(dòng)數(shù)據(jù)更新
4.事件相關(guān)指令
5.Vuex基本實(shí)現(xiàn)
6.Vue-Router基本實(shí)現(xiàn)
一溺蕉、構(gòu)建Vue實(shí)例
大家至此應(yīng)該已經(jīng)在項(xiàng)目中使用Vue,在此就不過多講解Vue使用悼做,直接從0開始盡量模仿Vue的語法疯特,首先我們看一下簡單的Vue案例
<body>
<div id="app">
<input type="text" v-model="name">
<p>{{ name }}</p>
<p>{{age}}</p>
<ul>
<li>6</li>
<li>6</li>
<li>6</li>
</ul>
</div>
<!--1.下載導(dǎo)入Vue.js-->
<script src="js/vue.js"></script>
<script>
// 2.創(chuàng)建一個(gè)Vue的實(shí)例對(duì)象
let vue = new Vue({
// 3.告訴Vue的實(shí)例對(duì)象, 將來需要控制界面上的哪個(gè)區(qū)域
el: '#app',
// 4.告訴Vue的實(shí)例對(duì)象, 被控制區(qū)域的數(shù)據(jù)是什么
data: {
name: "張三",
age : '18'
}
});
console.log(vue.$el);
console.log(vue.$data);
</script>
</body>
通過觀察我們得知,要想使用Vue必須先創(chuàng)建Vue的實(shí)例, 創(chuàng)建Vue的實(shí)例通過new來創(chuàng)建, 所以說明Vue是一個(gè)類
所以我們要想使用自己的Vue,就必須定義一個(gè)名稱叫做Vue的類只要?jiǎng)?chuàng)建好了Vue的實(shí)例, Vue就會(huì)根據(jù)指定的區(qū)域和數(shù)據(jù), 去編譯渲染這個(gè)區(qū)域
所以我們需要在自己編寫的Vue實(shí)例中拿到數(shù)據(jù)和控制區(qū)域, 去編譯渲染這個(gè)區(qū)域
注意點(diǎn): 創(chuàng)建Vue實(shí)例的時(shí)候指定的控制區(qū)域可以是一個(gè)ID名稱, 也可以是一個(gè)Dom元素
注意點(diǎn): Vue實(shí)例會(huì)將傳遞的控制區(qū)域和數(shù)據(jù)都綁定到創(chuàng)建出來的實(shí)例對(duì)象上
$el/$data
來到我們自己創(chuàng)建的js文件肛走,簡單命名為lue.js
Lue.js文件
class Lue {
constructor(options){
// 1.保存創(chuàng)建時(shí)候傳遞過來的數(shù)據(jù)
if(this.isElement(options.el)){
this.$el = options.el;
}else{
this.$el = document.querySelector(options.el);
}
this.$data = options.data;
// 2.根據(jù)指定的區(qū)域和數(shù)據(jù)去編譯渲染界面
if(this.$el){
new Compiler(this)
}
}
// 判斷是否是一個(gè)元素
isElement(node){
return node.nodeType === 1;
}
}
class Compiler {
constructor(vm){
this.vm = vm;
}
}
首先在第一步判斷傳遞的數(shù)據(jù)是否是元素漓雅,如果是則直接賦值給this.$el
,不是則通過選擇器查找一下再賦值朽色。然后再把外面?zhèn)鬟f的data賦值給this.$data
邻吞。
接下來在根據(jù)指定數(shù)據(jù),指定的區(qū)域葫男,去編譯渲染到界面中吃衅,專門定義一個(gè)Compiler類用于編譯渲染。
接下來就可以使用了腾誉,只需要導(dǎo)入我們剛剛創(chuàng)建的Lue.js文件,將Vue替換Lue即可
let vue = new Lue({
el: '#app',
data: {
name: "張三",
age : '18'
}
});
console.log(vue.$el);
console.log(vue.$data);
二峻呕、提取元素到內(nèi)存
在后面實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)界面時(shí)利职,數(shù)據(jù)發(fā)生改變,就替換元素瘦癌。在js每次操作dom時(shí)都會(huì)對(duì)DOM進(jìn)行一次重排猪贪,所謂重排也就是當(dāng)元素的大小,位置結(jié)構(gòu)發(fā)生變化的時(shí)候讯私,就會(huì)引起瀏覽器對(duì)當(dāng)前頁面的結(jié)構(gòu)進(jìn)行一次重新的計(jì)算热押,這是非常耗費(fèi)瀏覽器性能的西傀。
虛擬DOM的出現(xiàn)很好的解決了這一問題,而js中的文檔碎片就類似于虛擬DOM
可以使用文檔碎片(Fragment)桶癣,
類似于虛擬DOM的思想拥褂,在內(nèi)存中先開辟出一塊地方,對(duì)于所有節(jié)點(diǎn)的操作都在內(nèi)存中先完成牙寞,完成后再一次更新到頁面中饺鹃,優(yōu)化了瀏覽器的性能。
Compiler編譯渲染的類
class Compiler {
constructor(vm){
this.vm = vm;
// 1.將網(wǎng)頁上的元素放到內(nèi)存中
let fragment = this.node2fragment(this.vm.$el);
console.log(fragment);
// 2.利用指定的數(shù)據(jù)編譯內(nèi)存中的元素
// 3.將編譯好的內(nèi)容重新渲染會(huì)網(wǎng)頁上
}
node2fragment(app){
// 1.創(chuàng)建一個(gè)空的文檔碎片對(duì)象
let fragment = document.createDocumentFragment();
// 2.編譯循環(huán)取到每一個(gè)元素
let node = app.firstChild;
while (node){
// 注意點(diǎn): 只要將元素添加到了文檔碎片對(duì)象中, 那么這個(gè)元素就會(huì)自動(dòng)從網(wǎng)頁上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存儲(chǔ)了所有元素的文檔碎片對(duì)象
return fragment;
}
}
三间雀、編譯指令和模板數(shù)據(jù)
將網(wǎng)頁中的元素放到內(nèi)存中后,接下就是利用指定的數(shù)據(jù)編譯內(nèi)存中的元素悔详。
class Compiler {
constructor(vm){
this.vm = vm;
// 1.將網(wǎng)頁上的元素放到內(nèi)存中
let fragment = this.node2fragment(this.vm.$el);
// 2.利用指定的數(shù)據(jù)編譯內(nèi)存中的元素
this.buildTemplate(fragment);
// 3.將編譯好的內(nèi)容重新渲染會(huì)網(wǎng)頁上
this.vm.$el.appendChild(fragment);
}
node2fragment(app){
// 1.創(chuàng)建一個(gè)空的文檔碎片對(duì)象
let fragment = document.createDocumentFragment();
// 2.編譯循環(huán)取到每一個(gè)元素
let node = app.firstChild;
while (node){
// 注意點(diǎn): 只要將元素添加到了文檔碎片對(duì)象中, 那么這個(gè)元素就會(huì)自動(dòng)從網(wǎng)頁上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存儲(chǔ)了所有元素的文檔碎片對(duì)象
return fragment;
}
buildTemplate(fragment){
//為了實(shí)現(xiàn)遍歷,通過解構(gòu)賦值將偽數(shù)組轉(zhuǎn)成數(shù)組
let nodeList = [...fragment.childNodes];
nodeList.forEach(node=>{
// 需要判斷當(dāng)前遍歷到的節(jié)點(diǎn)是一個(gè)元素還是一個(gè)文本
// 如果是一個(gè)元素, 我們需要判斷有沒有v-model屬性
// 如果是一個(gè)文本, 我們需要判斷有沒有{{}}的內(nèi)容
if(this.vm.isElement(node)){
// 是一個(gè)元素
this.buildElement(node);
// 處理子元素(處理后代)
this.buildTemplate(node);
}else{
// 不是一個(gè)元素
this.buildText(node);
}
})
}
buildElement(node){
let attrs = [...node.attributes];
attrs.forEach(attr => {
let {name, value} = attr; // v-model="name" / name:v-model / value:name
if(name.startsWith('v-')){ // v-model / v-html / v-text / v-xxx
let [_, directive] = name.split('-'); // v-model -> [v, model]
CompilerUtil[directive](node, value, this.vm);
}
})
}
buildText(node){
let content = node.textContent;
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
console.log('是{{}}的文本, 需要我們處理', content);
}
}
}
buildTemplate方法中惹挟,通過fragment.childNodes可以拿到內(nèi)存中所有的子節(jié)點(diǎn)茄螃,然后遍歷判斷是否時(shí)元素還是文本。
但是子節(jié)點(diǎn)中可能包含多個(gè)子節(jié)點(diǎn)连锯,所以需要再次遞歸遍歷下
// 處理子元素(處理后代) this.buildTemplate(node);
如果是元素則來到buildElement方法中归苍。在遍歷元素的屬性,name為屬性名萎庭,value為屬性值霜医。再判斷是否包含v-,包含的話代表是vue指令,取出指令賦值給directive驳规。
CompilerUtil[directive](node, value, this.vm);
,調(diào)用工具對(duì)象的方法給節(jié)點(diǎn)賦值肴敛。
接著看工具CompilerUtil的實(shí)現(xiàn)
let CompilerUtil = {
//根據(jù)屬性名稱獲取值 vm:vue實(shí)例對(duì)象,value : 指令的值吗购,(v-model = "name",值就是name)
getValue(vm, value){
// time.h --> [time, h] 医男,http://www.reibang.com/p/e375ba1cfc47
return value.split('.').reduce((data, currentKey) => {
// 第一次執(zhí)行: data=$data, currentKey=time
// 第二次執(zhí)行: data=time, currentKey=h
return data[currentKey];
}, vm.$data);
},
/*
node : 當(dāng)前節(jié)點(diǎn)
value : 指令的值,(v-model = "name",值name)
vm : lue實(shí)例對(duì)象
*/
model: function (node, value, vm) { // value=time.h
/*node.value = vm.$data[value]; // vm.$data[time.h] --> vm.$data[time] --> time[h]*/
let val = this.getValue(vm, value);
node.value = val;
},
html: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerHTML = val;
},
text: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerText = val;
}
}
在getValue(vm, value)方法中又有注意點(diǎn)捻勉,此時(shí)我們修改下html代碼
<body>
<div id="app">
<input type="text" v-model="name">
<input type="text" v-model="time.h">
<input type="text" v-model="time.m">
<input type="text" v-model="time.s">
<div v-html="html">abc</div>
<div v-text="text">123</div>
<p>{{ name }}</p>
<p>{{age}}</p>
<p>{{time.h}}</p>
<p>{{name}}-{{age}}</p>
<ul>
<li>6</li>
<li>6</li>
<li>6</li>
</ul>
</div>
<script>
// 2.創(chuàng)建一個(gè)Vue的實(shí)例對(duì)象
// let vue = new Vue({
let vue = new Lue({
// 3.告訴Vue的實(shí)例對(duì)象, 將來需要控制界面上的哪個(gè)區(qū)域
el: '#app',
// el: document.querySelector('#app'),
// 4.告訴Vue的實(shí)例對(duì)象, 被控制區(qū)域的數(shù)據(jù)是什么
data: {
name: "張三",
age: 18,
time: {
h: 11,
m: 12,
s: 13
},
html: `<div>我是div</div>`,
text: `<div>我是div</div>`
}
});
</script>
</body>
在data中可能有多層數(shù)據(jù)镀梭,也就是對(duì)象類似的。
<input type="text" v-model="time.h">
<input type="text" v-model="time.m">
<input type="text" v-model="time.s">
time: {
h: 11,
m: 12,
s: 13
},
所以在CompilerUtil的model方法中僅僅使用 node.value = vm.$data[value]
不行的踱启,所以抽取一個(gè)getValue方法报账,通過reduce遍歷,取出data.time.h的值
//根據(jù)屬性名稱獲取值 vm:vue實(shí)例對(duì)象埠偿,value : 指令的值透罢,(v-model = "name",值就是name)
getValue(vm, value){
// time.h --> [time, h] ,http://www.reibang.com/p/e375ba1cfc47
return value.split('.').reduce((data, currentKey) => {
// 第一次執(zhí)行: data=$data, currentKey=time
// 第二次執(zhí)行: data=time, currentKey=h
return data[currentKey];
}, vm.$data);
},
元素中帶有v-model指令就能夠?qū)崿F(xiàn)綁定數(shù)據(jù)了冠蒋。接下來在看插值語法綁定模板數(shù)據(jù)羽圃。
buildText(node){
let content = node.textContent;
//看看是否匹配{{name}}(插值語法)
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
CompilerUtil['content'](node, content, this.vm);
}
}
通過正則表達(dá)式匹配{{}},匹配到后再調(diào)用CompilerUtil工具對(duì)象的content方法抖剿。
content: function (node, value, vm) {
// console.log(value); // {{ name }} -> name -> $data[name]
let val = this.getContent(vm, value);
node.textContent = val;
}
getContent(vm, value){
// console.log(value); // {{name}}-{{age}} ->張三-{{age}} -> 張三-18
// (.+?) 取出值
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
// 第一次執(zhí)行 args[1] = name
// 第二次執(zhí)行 args[1] = age
// console.log(args);
return this.getValue(vm, args[1]); // 張三, 18
});
// console.log(val);
return val;
},
注意點(diǎn):可能數(shù)據(jù)是{{name}--{{age}}朽寞,那么則用replace方法依次查找替換识窿。
附上本章節(jié)lue.js代碼
let CompilerUtil = {
/*
node : 當(dāng)前接單
value : 指令的值脑融,(v-model = "name",值name)
vm : Lue實(shí)例對(duì)象
*/
//根據(jù)屬性名稱獲取值 vm:vue實(shí)例對(duì)象喻频,value : 指令的值,(v-model = "name",值就是name)
getValue(vm, value){
// time.h --> [time, h]
return value.split('.').reduce((data, currentKey) => {
// 第一次執(zhí)行: data=$data, currentKey=time
// 第二次執(zhí)行: data=time, currentKey=h
return data[currentKey.trim()];
}, vm.$data);
},
getContent(vm, value){
// console.log(value); // {{name}}-{{age}} -> 張三-{{age}} -> 張三-18
// (.+?) 取出值
let reg = /\{\{(.+?)\}\}/gi;
let val = value.replace(reg, (...args) => {
// 第一次執(zhí)行 args[1] = name
// 第二次執(zhí)行 args[1] = age
// console.log(args);
return this.getValue(vm, args[1]); // 張三, 18
});
// console.log(val);
return val;
},
model: function (node, value, vm) {
let val = this.getValue(vm, value);
node.value = val;
},
html: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerHTML = val;
},
text: function (node, value, vm) {
let val = this.getValue(vm, value);
node.innerText = val;
},
content: function (node, value, vm) {
// console.log(value); // {{ name }} -> name -> $data[name]
let val = this.getContent(vm, value);
node.textContent = val;
}
}
class Lue {
constructor(options){
// 1.保存創(chuàng)建時(shí)候傳遞過來的數(shù)據(jù)
if(this.isElement(options.el)){
this.$el = options.el;
}else{
this.$el = document.querySelector(options.el);
}
this.$data = options.data;
// 2.根據(jù)指定的區(qū)域和數(shù)據(jù)去編譯渲染界面
if(this.$el){
new Compiler(this);
}
}
// 判斷是否是一個(gè)元素
isElement(node){
return node.nodeType === 1;
}
}
class Compiler {
constructor(vm){
this.vm = vm;
// 1.將網(wǎng)頁上的元素放到內(nèi)存中
let fragment = this.node2fragment(this.vm.$el);
// 2.利用指定的數(shù)據(jù)編譯內(nèi)存中的元素
this.buildTemplate(fragment);
// 3.將編譯好的內(nèi)容重新渲染會(huì)網(wǎng)頁上
this.vm.$el.appendChild(fragment);
}
node2fragment(app){
// 1.創(chuàng)建一個(gè)空的文檔碎片對(duì)象
let fragment = document.createDocumentFragment();
// 2.編譯循環(huán)取到每一個(gè)元素
let node = app.firstChild;
while (node){
// 注意點(diǎn): 只要將元素添加到了文檔碎片對(duì)象中, 那么這個(gè)元素就會(huì)自動(dòng)從網(wǎng)頁上消失
fragment.appendChild(node);
node = app.firstChild;
}
// 3.返回存儲(chǔ)了所有元素的文檔碎片對(duì)象
return fragment;
}
buildTemplate(fragment){
//為了實(shí)現(xiàn)遍歷膜宋,通過結(jié)構(gòu)賦值將偽數(shù)組變?yōu)閿?shù)組
let nodeList = [...fragment.childNodes];
nodeList.forEach(node=>{
// 需要判斷當(dāng)前遍歷到的節(jié)點(diǎn)是一個(gè)元素還是一個(gè)文本
if(this.vm.isElement(node)){
// 是一個(gè)元素
this.buildElement(node);
// 遞歸窿侈,處理子元素(處理后代)
this.buildTemplate(node);
}else{
// 不是一個(gè)元素
this.buildText(node);
}
})
}
buildElement(node){
let attrs = [...node.attributes];
attrs.forEach(attr => {
let {name, value} = attr; // v-model="name" / name:v-model / value:name
if(name.startsWith('v-')){ // v-model / v-html / v-text / v-xxx
let [_, directive] = name.split('-'); // v-model -> [v, model]
CompilerUtil[directive](node, value, this.vm);
}
})
}
buildText(node){
let content = node.textContent;
//看看是否匹配{{name}}(插值語法),'.+?' :至少一位數(shù)秋茫,'g':全局匹配史简,'i':是忽略大小寫
let reg = /\{\{.+?\}\}/gi;
if(reg.test(content)){
CompilerUtil['content'](node, content, this.vm);
}
}
}
本章節(jié)就暫時(shí)結(jié)束,下一章講解利用 Object.defineProperty,和發(fā)布訂閱者模式實(shí)現(xiàn)數(shù)據(jù)驅(qū)動(dòng)界面更新
文章主要代碼參考李南江web前端課程
其他參考鏈接:
https://www.cnblogs.com/suihang/p/9491359.html
https://segmentfault.com/a/1190000016434836