1. 引言
學(xué)習(xí)vue有段時(shí)間了,mvvm在vue中是個(gè)典型應(yīng)用,最近參考了參考網(wǎng)上一些資料,整理了一下,也加入了自己的理解,實(shí)現(xiàn)一個(gè)簡單版的demo,也方便有些面試的同學(xué)遇到設(shè)計(jì)一個(gè)mvvm的面試題。
2. 邏輯結(jié)構(gòu)
mvvm的設(shè)計(jì)模式是“發(fā)布與訂閱者”模式(observe/watcher),主要步驟有三步:
- observe來劫持并監(jiān)聽所有的屬性(也就是vue中的data)
- 給每一個(gè)需要監(jiān)聽的屬性,綁定一個(gè)訂閱者(watcher)
- 當(dāng)observe監(jiān)聽到屬性變化時(shí),通知watcher去更新視圖
ok,步驟講完了,接下來就開始實(shí)現(xiàn)每一步
3. observe
observe的主要功能:
- 劫持和監(jiān)聽數(shù)據(jù)
- 當(dāng)數(shù)據(jù)更新時(shí),觸發(fā)通知(后面的dep會講,這里跳過)
那么observe劫持和監(jiān)聽數(shù)據(jù)的呢?用Object.defineProperty來實(shí)現(xiàn)
先看一個(gè)例子:
function observe (obj) { var keys = Object.keys(obj) keys.forEach(function(key){ var val = void 0; Object.defineProperty(obj, key, { enumerable : true, configurable : true, get : function () { console.log('這個(gè)屬性是', key) return val; }, set : function ( newValue ) { val = newValue console.log('屬性' + key + '已經(jīng)被監(jiān)聽了,此時(shí)的值是:' + newValue) } }) }) } var book = {page : 300} observe(book) var b = book.page // 后臺會打印 這個(gè)屬性是page book.page = 400 // 后臺會打印 屬性page已經(jīng)被監(jiān)聽,此時(shí)的值是400在這里我們就實(shí)現(xiàn)了屬性的監(jiān)聽,上述例子中,我們用
Object.defineProperty重寫了set和get函數(shù),使得屬性的值變化時(shí)可以被我們監(jiān)聽到(如果不了解Object.defineProperty的,可以查閱Object.defineProperty
ok,原理我們清楚了,接下來就開始寫observe了
//data對象,key和val分別是data的鍵值對
function defineReactive(data, key, val){
observe(val) //遞歸調(diào)用data中的子對象
Object.defineProperty(data, key, {
enumerable : true, //可枚舉,可在for in 和 Object.keys中得到
configurable : true,
get : function () {
return val;
},
set : function ( newValue ) {
if(val === newValue){
return;
}
val = newValue
console.log('屬性' + key + '已經(jīng)被監(jiān)聽了,此時(shí)的值是:' + newValue)
}
})
}
//觀察者,用來監(jiān)聽數(shù)據(jù)
function observe (obj) {
if(!obj || typeof obj !== 'object'){
return;
}
Object.keys(obj).forEach(function(key){
defineReactive(obj, key, obj[key])
})
}
-
defineReactive函數(shù)的三個(gè)參數(shù),分別是要注冊的對象,對象的key,以及對象的值 -
defineReactive中調(diào)用observe,目的是遞歸調(diào)用所有的屬性
3. watcher
observe寫完了,接下來我們就要看watcher了,因?yàn)槊總€(gè)屬性都綁定一個(gè)watcher,所以可能會有很多的watcher,因此我們需要一個(gè)調(diào)度中心(暫時(shí)定義為Dep),來統(tǒng)一指揮watcher
Dep的主要功能:
- 將每個(gè)watcher都push進(jìn)去
- 當(dāng)接收到observe的屬性更新通知時(shí),通知對應(yīng)的watcher來更新視圖
接下來上代碼
//訂閱器,用來收集訂閱者,并且通知訂閱者更新函數(shù)
function Dep(){
this.subs = []
}
Dep.prototype = {
addSub : function (sub){
this.subs.push(sub)
},
notify : function (){
this.subs.forEach(function(sub){
sub.update()
})
}
}
Dep已經(jīng)定義好了,接下來我們需要改一下observe,將dep加進(jìn)去,這樣我們就實(shí)現(xiàn)了在get函數(shù)中將屬性注冊一個(gè)watcher再push進(jìn)dep中,并且set函數(shù)中數(shù)據(jù)更新時(shí)通dep,dep會再通知watcher去更新視圖
function defineReactive(data, key, val){
observe(val) //遞歸調(diào)用data中的子對象
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable : true, //可枚舉,可在for in 和 Object.keys中得到
configurable : true,
get : function () {
// 這里目的是定義一個(gè)flag,用來判斷什么時(shí)候需要push一個(gè)sub
//因?yàn)椴荒苊看握{(diào)用屬性都push一個(gè)sub,只有在第一次時(shí)才需要push
if(Dep.target){
dep.addSub(Dep.target)
}
return val;
},
set : function ( newValue ) {
if(val === newValue){
return;
}
val = newValue
console.log('屬性' + key + '已經(jīng)被監(jiān)聽了,此時(shí)的值是:' + newValue)
dep.notify()
}
})
}
到這里Dep調(diào)度中心就完成了,接下來我們實(shí)現(xiàn)watcher
watcher的主要功能:
- observe中g(shù)et函數(shù)只是定義了一個(gè)watcher,但是觸發(fā)這個(gè)get函數(shù)需要在這里,這樣就完成了注冊
- 接到dep的更新通知后,調(diào)用更新函數(shù)
ok,先實(shí)現(xiàn)代碼:
function Watcher (vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
this.value = this.get()
}
Watcher.prototype = {
update : function(){
this.run()
},
run : function (){
var value = this.vm.data[this.exp]
var oldVal = this.value
if (value !== oldVal) {
this.value = value
this.cb.call(this.vm, value, oldVal)
}
},
get:function(){
Dep.target = this // 這里設(shè)置一下target,下一行代碼會直接調(diào)用屬性的get函數(shù),會用到target
var value = this.vm.data[this.exp]
Dep.target = null
return value
}
}
-
Watcher的三個(gè)參數(shù),分別是vue,要訂閱的屬性,以及回調(diào)函數(shù)(觸發(fā)更新時(shí)調(diào)用的函數(shù)) -
this.value = this.get()這行代碼就是初始化就去獲取這個(gè)屬性值,這樣就會調(diào)用observe中的get函數(shù),然后將watcher加入到Dep中去。 - Dep.target = this 就是上文中提到的只有target有值時(shí)才會將watcher加入到Dep中
ok,到這里最簡易版本的mvvm已經(jīng)完成了
然后我們定義一個(gè)vue:
// data 是所有的屬性,el是綁定的元素節(jié)點(diǎn)(#app),exp是綁定的屬性
function dVue (data, el, exp) {
this.data = data;
observe(data);
el.innerHTML = this.data[exp]; // 初始化模板數(shù)據(jù)的值
new Watcher(this, exp, function (value) {
el.innerHTML = value;
});
return this;
}
在html中調(diào)用
<body>
<h1 id="app">{{math}}</h1>
</body>
<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/index.js"></script>
<script type="text/javascript">
var ele = document.querySelector('#app');
var dVue = new dVue({
math : '1'
}, ele, 'math');
setInterval(function () {
dVue.data.math = Math.random() * 100
}, 1000);
</script>
OK,接下來我們完善一下,實(shí)現(xiàn)vue中的{{ }}綁定
4. compile
compile主要功能:
- 獲取模板,并且解析模板,將數(shù)據(jù)替換模板,完成初始化視圖
- 給模板中綁定的屬性,new初始化一個(gè)watcher(之前是在dVue函數(shù)中完成的,現(xiàn)在移到這里)
function Compile(el, vm){
this.vm = vm
this.el = document.querySelector(el)
this.fragment = null
this.init()
}
Compile.prototype = {
init : function(){
if(this.el){
this.fragment = this.nodeToFragment(this.el)
this.compileElement(this.fragment)
this.el.appendChild(this.fragment)
}else{
console.log('節(jié)點(diǎn)不存在')
}
},
nodeToFragment : function(el){
//創(chuàng)建一個(gè)虛擬的文檔片段,用來操作dom節(jié)點(diǎn),因?yàn)檫@個(gè)片段是存在于內(nèi)存中
//所以相對于直接操作dom,性能會更好一點(diǎn)
var fragment = document.createDocumentFragment()
var child = el.firstChild
while(child){
fragment.appendChild(child)
child = el.firstChild
}
return fragment
},
compileElement :function(el){
var childNodes = el.childNodes
var self = this;
Array.prototype.slice.call(childNodes).forEach(function(node){
var reg = /\{\{\s*(.*?)\s*\}\}/
var text = node.textContent;
//判斷該節(jié)點(diǎn)是否含有{{ }}這個(gè)指令
if(self.isTextNode(node) && reg.test(text)){
self.compileText(node, reg.exec(text)[1])
}
if(node.childNodes && node.childNodes.length){
self.compileElement(node)
}
})
},
compileText :function(node, exp){
var self = this
var initText = this.vm[exp]
this.updateText(node, initText) //初始化視圖
new Watcher(this.vm, exp, function(value){
self.updateText(node, value)
})
},
updateText : function(node, value){
node.textContent = typeof value == 'undefined' ? '' : value
},
isTextNode : function(node){
return node.nodeType == 3
}
}
-
nodeToFragment是在內(nèi)存建立一個(gè)虛擬的節(jié)點(diǎn),然后將模板賦值給它,再繼續(xù)操作模板,這樣可以提升性能,參考文檔nodeToFragment -
compileElement這個(gè)函數(shù),解析模板,找到{{ }}指令的文本節(jié)點(diǎn),然后運(yùn)行核心函數(shù)compileText,解析文本節(jié)點(diǎn) -
compileText這個(gè)函數(shù)中,做了兩件事,第一件事是初始化視圖,也就是調(diào)用updateText函數(shù),第二件事就是給這個(gè)文本節(jié)點(diǎn)綁定一個(gè)watcher,用于訂閱該屬性,當(dāng)屬性值改變時(shí),會調(diào)用里面的回調(diào)函數(shù)
到這里compile就完成了,這樣我們需要把dVue重新修改一下
function dVue (options) {
var self = this
this.data = options.data
this.vm = this
// 將data上的屬性掛載到vue上,this.data.a === this.a
Object.keys(this.data).forEach((key)=>{
this.proxyKeys(key)
})
//重寫所有的data屬性的set和get方法,用于劫持監(jiān)聽數(shù)據(jù)
observe(this.data)
//編譯模板,得到綁定的節(jié)點(diǎn),初始化視圖,并且給該節(jié)點(diǎn)所綁定的屬性注冊一個(gè)watcher
new Compile(options.el, this)
return this
}
//代理一下屬性,這樣的話 dVue.name = dVue.data.name ,不用每次都帶著data了
//相當(dāng)于把data的所有屬性都注冊到了dVue上
dVue.prototype = {
proxyKeys : function(key){
var self = this
Object.defineProperty(this, key, {
enumerable : false,
configurable : true,
get : function(){
return self.data[key]
},
set :function(val){
self.data[key] = val
}
})
}
}
到這里就基本結(jié)束了,我們在html中調(diào)用一下
<html>
<head>
<title>實(shí)現(xiàn)一個(gè)簡單的mvvm</title>
</head>
<body>
<div id = "app">
<div>{{name}}</div>
<div>{{age}}</div>
<div>{{like}}</div>
</div>
<script type="text/javascript" src="./observe.js"></script>
<script type="text/javascript" src="./watcher.js"></script>
<script type="text/javascript" src="./compile.js"></script>
<script type="text/javascript" src="./dVue.js"></script>
<script>
let dVueInit = new dVue({
el : '#app',
data:{
name : 'ding',
age : 14,
like : '讀書'
}
})
</script>
</body>
</html>
到此結(jié)束!
參考文章:
https://www.cnblogs.com/libin-1/p/6893712.html
https://github.com/canfoo/self-vue/tree/master/v2