從documentfragement到實(shí)現(xiàn)手寫(xiě)vue

本文是lhyt本人原創(chuàng),希望用通俗易懂的方法來(lái)理解一些細(xì)節(jié)和難點(diǎn)。轉(zhuǎn)載時(shí)請(qǐng)注明出處。文章最早出現(xiàn)于本人github

0.劇透

vue的實(shí)現(xiàn),分為M-V,V-M,M-V三個(gè)階段,第一個(gè)階段主要利用fragement文檔片段來(lái)節(jié)點(diǎn)劫持,使得M和V層關(guān)聯(lián)起來(lái)。第二階段,利用defineProperty使得V層的變化能讓M層檢測(cè)到并更新M層。第三階段,利用了發(fā)布-訂閱模式,讓M層的變化實(shí)時(shí)反映到V層中,實(shí)現(xiàn)了手寫(xiě)的v-model

1.場(chǎng)景

首先,拋出一個(gè)問(wèn)題,在一個(gè)ul下面創(chuàng)建100個(gè)li,并且編號(hào)。于是,就有

var ul = document.getElementByTarName("ul");

for (var i = 0; i < 100; i++) {

var li = document.createElement('li');

li.innerHTML = i+1;

ul.appendChild(li)

}

看起來(lái)操作是很容易的,但是每一次插入都會(huì)引起重新渲染,會(huì)重新重繪頁(yè)面,因此會(huì)影響性能的

于是又有另一種方法,弄一個(gè)中轉(zhuǎn)站,最后一次性放進(jìn)去

var ul = document.getElementByTarName("ul");

var inHtml = '';

for (var i = 0; i <100; i++) {

inHtml +="<li>"+(i+1)+"</li>";

}

ul.innerHTML = inHtml;

然而這種方法不靈活,如果面對(duì)多變的dom結(jié)構(gòu),就難以操作

2.documentFragment

于是就有一種叫做文檔片段的東西documentFragment,是沒(méi)有父節(jié)點(diǎn)的最小文檔對(duì)象,常用于存儲(chǔ)html和xml文檔,有Node的所有屬性和方法,完全可以操作Node那樣操作。

DocumentFragment文檔片段是存在于內(nèi)存中的,沒(méi)有在DOM中,所以將子元素插入到文檔片段中不會(huì)引起頁(yè)面回流,因此使用DocumentFragment可以起到性能優(yōu)化作用。

上面的問(wèn)題就可以進(jìn)一步優(yōu)化。

var ul = document.getElementByTarName("ul");

var frag = document.createDocumentFragment();

var ihtml = '';

for (var i = 0; i < 100; i++) {

var li = document.createElement('li');

li.innerHTML = "index: " + i;

frag.appendChild(li);

}

ul.appendChild(frag);

3.節(jié)點(diǎn)劫持

既然有這樣的一個(gè)中轉(zhuǎn)站,那么他還可以做更多的事情。在開(kāi)發(fā)中,隨著代碼量增加,越來(lái)越需要講究性能,那么如果遇到需要操作很多節(jié)點(diǎn)的時(shí)候,直接創(chuàng)建節(jié)點(diǎn)的時(shí)候,頁(yè)面就不斷重排重繪,GPU負(fù)擔(dān)越來(lái)越大。這時(shí)候,需要一個(gè)中轉(zhuǎn)站,將需要用到的節(jié)點(diǎn)劫持,讓他不在dom中

html部分:

<div id="app"> 你看見(jiàn)我了 <p>hi</p></div>

js部分:

function myFragment(node){

var frag = document.createDocumentFragment()

var child

while(child = node.firstChild){//有子節(jié)點(diǎn)的時(shí)候,就給child賦值

frag.appendChild(child)//追加到frag,子節(jié)點(diǎn)少一個(gè)

}

return frag

}

var DOM = myFragment(document.getElementById('app'))

console.log(DOM)

console.log('這是innerHTML:'+document.getElementById('app').innerHTML)

控制臺(tái)

先創(chuàng)建一個(gè)文檔片段,再將節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)添加到文檔片段里面,再第二個(gè)......直到?jīng)]有,跳出循環(huán),此時(shí)innerhtml沒(méi)有內(nèi)容,都在文檔片段里面了。這就是節(jié)點(diǎn)劫持,無(wú)論怎么改樣式,整個(gè)div沒(méi)有內(nèi)容高度也是0。

4.看看劫持的是什么(掃描)

在上面的基礎(chǔ)上,我們可以看一下每一個(gè)標(biāo)簽、每一個(gè)屬性的怎樣的

html:

<div id="app">

<input type="text" name="hi" size="1" v-model="text" \>

</div>

在frag.appendChild(child)這句前面加上一段代碼來(lái)看一下里面的節(jié)點(diǎn)

js:

function myFragment(node){

var frag = document.createDocumentFragment()

var child

while(child = node.firstChild){

if(child.nodeType === 1){//如果是元素節(jié)點(diǎn)

var attr = child.attributes //將元素節(jié)點(diǎn)所有的屬性集合存放在attr

console.log(child.attributes)

}

frag.appendChild(child)//將子節(jié)點(diǎn)追加到文檔片段。非常重要,沒(méi)有這句就死循環(huán)

}

return frag

}

myFragment(document.getElementById('app'))

手滑,不小心寫(xiě)多了一個(gè)v-model="text",不過(guò)還是被顯示到了

v-model?這不就是vue的一個(gè)指令嗎

既然能拿到他,那么我們現(xiàn)在開(kāi)始手寫(xiě)一個(gè)迷你版vue試試看

5.迷你版vue準(zhǔn)備工作

一貫使用的IIFE

對(duì)于全局環(huán)境,存在exports對(duì)象的話,說(shuō)明引入環(huán)境是node或者其他commonjs環(huán)境。如果是amd標(biāo)準(zhǔn),如requirejs,就用define(factory)引入邏輯代碼

(function(global,factory){

typeof exports === 'object'&& typeof module !== 'undefined'?module.exports = factory():

typeof define === 'function' && define.amd?define(factory) :

(global.Vue = factory())

})(this,function(){

//主體在這里

})

這段國(guó)際常規(guī)的hello word代碼放在最后

var app = new Vue({

el:"app",

data:{

????text:"hello word",

????message:{name:'pp'}

????}

})

6.M-V綁定

data中的值,反映到input中,也就是M->V層的過(guò)程

html:

<div id="app">

<input v-model="text" type="text" name="n" size="10" \>

{{text}}

</div>

6.1定義Vue構(gòu)造函數(shù)

傳入的參數(shù)就是new Vue里面的對(duì)象,獲得el、data,再劫持id為app的元素里面的節(jié)點(diǎn),并進(jìn)行操作

var Vue = function(opts){

var id = opts.el||body

this.data = opts.data||{}

var DOM = myFragment(document.getElementById(id),this)

document.getElementById(id).appendChild(DOM)//劫持到節(jié)點(diǎn),添加到app上

}

6.2myFragment方法的完善

上面已經(jīng)講到怎么劫持節(jié)點(diǎn),并console看到了節(jié)點(diǎn)的內(nèi)容

遍歷attr,如果發(fā)現(xiàn)v-model這個(gè)屬性,就給他賦值,此時(shí)輸入框內(nèi)容就是hello word

for(var i = 0;i<attr.length;i++){

????if(attr[i].nodeName == 'v-model'){

????var name = attr[i].nodeValue

????console.log(name) //text

????node.value = vm.data[name]//輸入框內(nèi)容:hello word

????}

}

6.3替換mustache的內(nèi)容

已經(jīng)搞定了輸入框,接下來(lái)就是雙大括號(hào)了{(lán){ }},繼續(xù)在掃描的方法中添加另一個(gè)分支:當(dāng)掃描到文本節(jié)點(diǎn),就使用正則匹配雙大括號(hào)并進(jìn)行替換

if(node.nodeType === 3){//匹配文本節(jié)點(diǎn)

if(/{{(.*)}}/.test(node.nodeValue)){

var name = RegExp.$1//獲得文本內(nèi)容

console.log(name)

name = name.trim()

node.nodeValue = vm.data[name]//替換雙大括號(hào)的內(nèi)容

}

}

現(xiàn)在,文本框和雙大括號(hào)值都是hello world 了

注意:vm.data[name]可以理解為初步綁定,他就是data里面的text的內(nèi)容,接下來(lái)肯定不是綁死他的

6.4數(shù)據(jù)監(jiān)聽(tīng)

定義一個(gè)observer函數(shù),徹底地監(jiān)聽(tīng)每一個(gè)數(shù)據(jù),而且需要無(wú)視對(duì)象中的對(duì)象。先檢測(cè)obj是不是對(duì)象類型,如果不是就跳出(此時(shí)已經(jīng)是對(duì)象多層嵌套的最里面那層的key),如果是對(duì)象,就調(diào)用calation方法遞歸。

function observer(obj,vm){

if(typeof obj!=='object'){return}

Object.keys(obj).forEach(function(key){

console.log(key)//text,message,name

calation(vm,obj,key,obj[key])

})

}

function calation(vm,obj,key,value){

observer(value,vm)

}

綜上,在IIFE主體里面添加下面代碼,這部分是M->V的過(guò)程

var Vue = function (opts) {

? ? var id = opts.el || body

? ? this.data = opts.data || {}

? ? var DOM = myFragment(document.getElementById(id), this)

? ? document.getElementById(id).appendChild(DOM)

}

function myFragment(node, vm) {

? ? var frag = document.createDocumentFragment()

? ? var child

? ? while (child = node.firstChild) {

? ? ? ? comp(child, vm)

? ? ? ? frag.appendChild(child)

? ? }

? ? return frag

}

function comp(node, vm) {

? ? if (node.nodeType === 1) {

? ? ? ? var attr = node.attributes

? ? ? ? for (var i = 0;i< attr.length;i++){

if (attr[i].nodeName == 'v-model') {

? ? ? ? ? ? ? ? var name = attr[i].nodeValue

? ? ? ? ? ? ? ? console.log(name)

? ? ? ? ? ? ? ? node.value = vm.data[name]

? ? ? ? ? ? }

? ? }

}

if (node.nodeType === 3) {

? ? if (/{{(.*)}}/.test(node.nodeValue)) {

? ? ? ? var name = RegExp.$1

? ? ? ? console.log(name)

? ? ? ? name = name.trim()

? ? ? ? node.nodeValue = vm.data[name]

? ? }

}

}

function observer(obj, vm) {

? ? if (typeof obj !== 'object') { return }

? ? Object.keys(obj).forEach(function (key) {

? ? ? ? console.log(key)

? ? ? ? calation(vm, obj, key, obj[key])

? ? })

}

function calation(vm, obj, key, value) {

? ? observer(value, vm)

}

return Vue

第一次M-V綁定,可以說(shuō)是初始化,就是讓input和Vue的實(shí)例對(duì)象里面?zhèn)魅氲膮?shù)中的data聯(lián)系起來(lái),也就是‘’搭建起溝通的橋梁‘’

7.V-M綁定

用戶輸入改變input的值(V層)時(shí),data中(M層)也改變對(duì)應(yīng)的值

7.1關(guān)于defineProperty

終于到了江湖中流傳的defineProperty了,這個(gè)api究竟是怎么用的,先舉個(gè)小栗子

var obj = {name:'pp'}

console.log(obj.name)//pp

Object.defineProperty(obj,'name',{

get:function(){

return 1

},

set:function(newVal){

console.log(newVal)

}

})

console.log(obj.name)//1

obj.name = 2;//2

console.log(obj.name)//1

當(dāng)訪問(wèn)這個(gè)屬性的時(shí)候,調(diào)用的是get方法,這里輸出1,當(dāng)試圖改變屬性的值的時(shí)候,調(diào)用的是set方法,console這個(gè)值,也就是這里輸出2的原因。再次回頭訪問(wèn),還是輸出1。(我這里set方法只是console而已,再回頭看obj.name當(dāng)然還是1)

7.2小型雙向綁定demo

html:

<input id="app" type="text" \>

<p id="p"></p>

js:

document.getElementById('app').addEventListener('input',function(e){

document.getElementById('p').innerHTML=e.target.value;

})

回過(guò)頭來(lái),我們的vue也是要這樣做的

7.3在帶有屬性v-model上添加事件監(jiān)聽(tīng)

在comp函數(shù)里面,匹配到了v-model=‘text’ 這個(gè)屬性時(shí),取得v-model的屬性的值text,Vue的實(shí)例對(duì)象vm的text屬性的值,等于輸入框更新的值。輸入框輸入什么,這個(gè)

data:{

????text:"hello word",

????message:{name:'pp'}

}

里面的 text就是什么,不再是helloworld了(前面數(shù)據(jù)監(jiān)聽(tīng)的時(shí)候,有做過(guò)observer的遞歸,所以無(wú)論多少層嵌套對(duì)象,總會(huì)能徹底取得key-value的形式)

if(attr[i].nodeName == 'v-model'){

var name = attr[i].nodeValue

node.addEventListener('input',function(e){

vm[name]=e.target.value;//Vue的實(shí)例對(duì)象vm的text屬性的值,賦值并觸發(fā)該屬性的set函數(shù)

});

接著,把輸入框改變的值賦值node.value = vm[name],前面是node.value = vm.data[name]的初步嘗試,讓input和data關(guān)聯(lián)起來(lái),現(xiàn)在需要改

同理,文本節(jié)點(diǎn)那里也要改(為最后一步做鋪墊,當(dāng)然現(xiàn)在還是沒(méi)有效果)

通過(guò)正則獲得雙大括號(hào)里面的值(text),定義一個(gè)name='text' ,從而能改變雙大括號(hào)的值

node.nodeValue=vm[name];

7.4監(jiān)聽(tīng)屬性

再定義一個(gè)監(jiān)聽(tīng)器defineReactive,在observer里面執(zhí)行,用到了Object.defineProperty

function defineReactive(obj,key,val){

Object.defineProperty(obj,key,{

get:function(){

return val

},

set:function(newVal){

if(newVal===val)return ;

val=newVal;//數(shù)據(jù)在改變

console.log(val)

}

})

}

遞歸完成后就開(kāi)始監(jiān)聽(tīng)屬性

function observer(obj,vm){

if(typeof obj!=='object'){return}

Object.keys(obj).forEach(function(key){

console.log(key)

calation(vm,obj,key,obj[key])

defineReactive(vm,key,obj[key])

})

}

現(xiàn)在,輸入框?qū)懥耸裁?,就console了什么

8.M-V再次綁定

這次是,當(dāng)用戶主動(dòng)改變M層數(shù)據(jù),V層也跟著改變,第一次是默認(rèn)的,只是讓他們建立起關(guān)聯(lián)。(其實(shí)這就是雞生蛋,蛋生雞的過(guò)程,總得有一個(gè)開(kāi)頭吧,為什么不VMMV而是MVVM,也可以想到,難道一個(gè)軟件需要用戶設(shè)置初始值?那么真的需要用戶設(shè)置初始值呢?那就第一次MV給他設(shè)置默認(rèn)值為空,前面也有處理)

8.1初探發(fā)布-訂閱模式

它是一種一對(duì)多的關(guān)系,讓多個(gè)訂閱者(也可以叫觀察者)者對(duì)象同時(shí)監(jiān)聽(tīng)某一個(gè)主題對(duì)象,當(dāng)一個(gè)主題對(duì)象發(fā)生改變時(shí),發(fā)布者將會(huì)發(fā)布變化的通知,所有依賴于它的對(duì)象都(訂閱者)將得到通知。多個(gè)訂閱者對(duì)象監(jiān)視主題對(duì)象,當(dāng)發(fā)生變化,就由發(fā)布者通知訂閱者

//定義2個(gè)訂閱者

var subscriber1 = {update:function(){console.log(1)}}

var subscriber2 = {update:function(){console.log(2)}}

var pub = {//定義發(fā)布者

????publish:function(){

????????dep.notify()//主題對(duì)象的實(shí)例調(diào)用發(fā)布通知

????}

}

function Dep(){//主題對(duì)象構(gòu)造函數(shù)

this.subs=[ subscriber1, subscriber2]

}

Dep.prototype.notify = function(){//主題對(duì)象的原型上定義通知函數(shù)

this.subs.forEach(function(sub){//通知每一個(gè)訂閱者并執(zhí)行相應(yīng)的方法

????sub.update()

????})

}

var dep = new Dep()//主題對(duì)象實(shí)例化

pub.publish()//發(fā)布者發(fā)布信息

最后控制臺(tái)打印結(jié)果就是1,2

8.2監(jiān)聽(tīng)器defineReactive中綁定主題對(duì)象與訂閱者

data每一個(gè)屬性被監(jiān)聽(tīng)的時(shí)候添加一個(gè)主題對(duì)象,當(dāng)data發(fā)生改變將觸發(fā)Object.defineProperty里面的set方法,去通知訂閱者們

function Dep(){

????this.subs=[];//訂閱者集合

}

Dep.prototype={

????addSub:function(sub){//主題對(duì)象的原型上添加訂閱者的方法

????this.subs.push(sub);

},

notify:function(){ //發(fā)布信息

????this.subs.forEach(function(sub){

????????sub.update();//訂閱者的方法

????})

}

}

在Object.defineProperty方法前面實(shí)例化Dep:var dep=new Dep();

那么sub.update()的訂閱者方法呢,接下來(lái)將會(huì)解釋

8.3訂閱者的定義

觀察主題對(duì)象(有v-model屬性的input)變化,將變化展示到視圖層(雙大括號(hào)里面)

function Watcher(vm,node,name){

????Dep.target=this;//Dep的靜態(tài)屬性target指向當(dāng)前訂閱者的實(shí)例

????this.name=name;

????this.node=node;

????this.vm=vm;

????this.update(); //先初始化視圖

????Dep.target=null;

}

Watcher.prototype={

????get:function(){

????????this.value=this.vm[this.name]//得到實(shí)例對(duì)象的屬性的值

????},

update:function(){

????this.get();

????this.node.nodeValue=this.value;

????}

}

再回到獲得文本節(jié)點(diǎn)的時(shí)候(if(node.nodeType === 3))

在內(nèi)部最后一句加上 new Watcher(vm,node,name); 實(shí)例化訂閱者

8.4 監(jiān)聽(tīng)器defineReactive的get與set

在comp方法中,通過(guò)初始化value值,觸發(fā)set函數(shù),在set函數(shù)中為主題對(duì)象添加訂閱者。

在defineProperty的get方法中當(dāng)某個(gè)訂閱者存在,就添加訂閱者

get:function(){

????if(Dep.target){dep.addSub(Dep.target)}

????return val

},

set方法改變了數(shù)據(jù)后,主題對(duì)象的實(shí)例發(fā)布通知

set:function(newVal){

????if(newVal===val){return ;}

????val=newVal;

????console.log(val)

????dep.notify();

}

9.大功告成

終于全部搞定了,上完整代碼

html:

< div id="app" >

< input v-model="text" type="text" name="n" size="10"? >

{{text}}

</div>

js:

(function(global,factory){

typeof exports === 'object'&& typeof module !== 'undefined'?module.exports = factory():

typeof define === 'function' && define.amd?define(factory) :

(global.Vue = factory())

})(this,function(){

var Vue = function(opts){

var id = opts.el||body

this.data = opts.data||{}

data = this.data

observer(data,this)

var DOM = myFragment(document.getElementById(id),this)

document.getElementById(id).appendChild(DOM)

}

function myFragment(node,vm){

var frag = document.createDocumentFragment()

var child

while(child = node.firstChild){

comp(child,vm)

frag.appendChild(child)

}

return frag

}

function comp(node,vm){

if(node.nodeType === 1){

var attr = node.attributes

for(var i = 0;i< attr.length;i++){

if(attr[i].nodeName == 'v-model'){

var name = attr[i].nodeValue

console.log(name)

node.addEventListener('input',function(e){

vm[name]=e.target.value;

//console.log('vm[name]'+vm[name])

//console.log('vm.data[name]'+vm.data[name])

});

node.value = vm[name]

}

}

}

if(node.nodeType === 3){

if(/{{(.*)}}/.test(node.nodeValue)){

var name = RegExp.$1

console.log(name)

name = name.trim()

node.nodeValue=vm[name];

new Watcher(vm,node,name);

}

}

}

function observer(obj,vm){

if(typeof obj!=='object'){return}

Object.keys(obj).forEach(function(key){

console.log(key)

calation(vm,obj,key,obj[key])

defineReactive(vm,key,obj[key])

})

}

function calation(vm,obj,key,value){

observer(value,vm)

}

function defineReactive(obj,key,val){

var dep=new Dep();

Object.defineProperty(obj,key,{

get:function(){

if(Dep.target){dep.addSub(Dep.target)}

return val

},

set:function(newVal){

if(newVal===val)return ;

val=newVal;

// console.log(val)

dep.notify();

}

})

}

function Dep(){

this.subs=[];

}

Dep.prototype={

addSub:function(sub){

this.subs.push(sub);

},

notify:function(){

this.subs.forEach(function(sub){

sub.update();

})

}

}

function Watcher(vm,node,name){

this.vm=vm;

this.node=node;

this.name=name;

Dep.target=this;

this.update();

Dep.target=null;

}

Watcher.prototype={

update:function(){

this.get();

this.node.nodeValue=this.value;

},

get:function(){

this.value=this.vm[this.name]

}

}

return Vue

})

//引入了vue,開(kāi)始常規(guī)操作

var app = new Vue({

el:"app",

data:{

text:"hello word",

message:{name:'pp'}

}

})


原文來(lái)源于:lhyt的github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 工廠模式類似于現(xiàn)實(shí)生活中的工廠可以產(chǎn)生大量相似的商品,去做同樣的事情,實(shí)現(xiàn)同樣的效果;這時(shí)候需要使用工廠模式。簡(jiǎn)單...
    舟漁行舟閱讀 8,110評(píng)論 2 17
  • 單例模式 適用場(chǎng)景:可能會(huì)在場(chǎng)景中使用到對(duì)象,但只有一個(gè)實(shí)例,加載時(shí)并不主動(dòng)創(chuàng)建,需要時(shí)才創(chuàng)建 最常見(jiàn)的單例模式,...
    Obeing閱讀 2,311評(píng)論 1 10
  • 深入響應(yīng)式 追蹤變化: 把普通js對(duì)象傳給Vue實(shí)例的data選項(xiàng),Vue將使用Object.defineProp...
    冥冥2017閱讀 4,952評(píng)論 6 16
  • 我實(shí)在無(wú)法把目光從他臉上移開(kāi),那樣一張小丑的臉。 它對(duì)非日常性、犯罪、畸形與病變的暗示令人興奮。我想知道,一個(gè)人可...
    Pinocchio閱讀 2,551評(píng)論 3 20
  • 話說(shuō)某一天去游族面試,被問(wèn)到知不知道HanderThread,當(dāng)時(shí)就懵逼了,梗了半天回答不出來(lái),沒(méi)有了解過(guò)...結(jié)...
    boboyuwu閱讀 327評(píng)論 0 0

友情鏈接更多精彩內(nèi)容