本文是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