看了Vue
的一些思想以後,開始有想法去模仿Vue
寫一個小的MVVM
,奈何當本身真正開始寫的時候才知道有多難,不過也讓本身明白,自身的編碼水平和設計代碼的思惟還有很大的提高空間,哈哈哈。html
先來一個基本的index.html
文件,而後咱們模仿Vue
的寫法,實例化一個MVVM
類和定義data
對象(Vue
裏爲了擁有本身的命名空間data
應該爲函數)數組
<!DOCTYPE html><html lang="en">
<head> ```</head>
<body>
<div id="app">
<div>
<div>
<span>{{hello}}</span>
</div>
<div>{{msg}}</div>
</div>
</div>
<script src="./src/index.js"></script>
<script>
const app = new MVVM({
$el: '#app',
data: {
msg: 'mvvm',
hello: 'david'
},
})
</script>
</body>
</html>複製代碼
咱們設想是這樣來操做滴,而後就能夠編寫咱們的MVVM
類了。我感受寫這個的話一種由上而下的思路會比較好,就是先把最頂層的思路想好,而後再慢慢往下寫細節。bash
class MVVM {
constructor(options) {
this.$el = options.$el
this.data = options.data
if (this.$el) {
const wathcers = new Compiler(this.$el, this)
new Observer(this.data, wathcers)
}
}
}複製代碼
這裏咱們定義了一個MVVM
類,在options
裏面能夠拿到$el
和data
參數,由於咱們上面的模板裏面就是這麼傳的。若是傳入的$el
節點確實存在的話,就能夠開始咱們的初始化編譯模板操做。app
function Compiler(el, vm) {}複製代碼
看上面咱們知道,Compiler
的參數有兩個,一個是$el字符串
,還有一個就是咱們的MVVM
實例,上面我傳了this
。dom
首先咱們先來思考,編譯模板的時候但願的是將相似{{key}}
的部分用咱們的data
對象中的對應的value
來取代。因此咱們應該先遍歷全部的dom
節點,找到形如{{key}}
所在的位置,再進行下一步操做。先來兩個函數mvvm
this.forDom = function (root) {
const childrens = root.children
this.forChildren(childrens)
}複製代碼
這是一個獲取dom
節點的子節點的函數,而後將子節點傳入下一個函數函數
this.forChildren = function (children) {
for (let i = 0; i < children.length; i++) {
//每一個子節點
let child = children[i];
//判斷child下面有沒有子節點,若是還有子節點,那麼就繼續的遍歷
if (child.children.length !== 0) {
this.forDom(child);
} else {
//將vm與child傳入一個新的Watcher中
let key = child.innerText.replace(/^\{\{/g, "").replace(/\}\}$/g, "")
let watcher = new Watcher(child, vm, key)
//初始轉換模板
compilerTextNode(child, vm)
watchers.push(watcher)
}
}
}複製代碼
若是子節點還有子節點,就繼續調用forDOM函數。不然就將標籤中{{key}}
裏面的key
拿出來(這裏我只考慮了形如<div>{{key}}</div>
的狀況,大佬輕噴),拿到key
以後就實例化一個watcher
,讓咱們來看看watcher
作了啥。ui
function Watcher(child, vm, initKey) {
this.initKey = initKey
this.update = function (key) {
if (key === initKey) {
compilerTextNode(child, vm, initKey)
}
}
}複製代碼
首先把所對應的子節點child
傳入,而後vm
實例也要傳入,由於下面有一個函數須要用到vm實例,而後這個initKey
是我本身的一些騷操做(流下了沒有技術的淚水),它的做用主要是記錄一開始的那個key
值,爲啥要記錄呢,請看下面的方法。this
compilerTextNode = function (child, vm, initKey) {
if (!initKey) {
//第一次初始化
const keyPrev = child.innerText.replace(/^\{\{/g, "").replace(/\}\}$/g, "") //獲取key的內容
if (vm.data[keyPrev]) {
child.innerText = vm.data[keyPrev]
} else {
throw new Error(
`${key} is not defined`
)
}
} else {
child.innerText = vm.data[initKey]
}複製代碼
首先這個函數會有兩個邏輯,一個是初始化的時候,還有一個是數據更新的時候。能夠看到初始化的時候咱們是這樣作的compilerTextNode(child, vm)
,也就是會進入這個if
邏輯。這裏就是拿到了模板中的key
值,而後節點的值替換成咱們data
對象裏面的值。爲啥要記錄這個initKey
呢,就是在這裏若是模板的innerText
直接被整個替換掉了,例如說本來模板中是{{msg}}
,它通過這個函數處理以後,會變成mvvm
,那咱們的data
中是沒有mvvm
這個key
的,這裏記錄是爲了更新的時候用。最後,全部的watcher
都會被push
進watchers
數組裏,而且返回。編碼
function Observer(data, watchers) {}複製代碼
而後就到了咱們熟悉的響應式數據啦,這個函數接受兩個參數,一個就是咱們一開始定義的data
對象,還有一個就是剛纔咱們拿到的watchers
數組。
this.observe = function (data) {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
this.observe(data[key]) //遞歸深度劫持
})
}複製代碼
首先咱們先來對data
作一下判斷,而後調用defineReactive
方法對data
作響應式處理,最後來個遞歸深度劫持data
。
this.defineReactive = function (obj, key, value) {
let that = this
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(newValue) {
if (newValue !== value) {
that.observe(newValue)
value = newValue
//從新賦值以後 應該通知編譯器
watchers.forEach(watcher => {
watcher.update(key)
})
}
}
})
}複製代碼
get
方法調用時直接返回value
,set
方法調用時若是value
有從新賦值,那麼應該從新監聽value
的新值,而後用watcher
通知編譯器從新渲染模板。
而後調用observe方法,this.observe(data)
這裏咱們再看回watcher.update
方法,在defineReactive
方法中調用時傳入的key
是咱們data
中定義的,而這個initKey
也就是咱們以前在初始化模板的時候保存的,當這兩個相等的時候才從新渲染對應的模板塊
this.update = function (key) {
if (key === initKey) {
compilerTextNode(child, vm, initKey)
}
}複製代碼
最後讓咱們來看一眼效果,加上一小段改變數據的代碼。
setTimeout(() => {
app.data.msg = 'change'
}, 2000)複製代碼
咱們來思考一下Observer
、Watcher
、Compiler
三者之間的關係。Observer
最重要的職責是把數據變成響應式的,換句話說就是咱們能夠在數據被取值或者賦值的時候加入一些本身的操做。Compiler
就是把HTML
模板中的{{key}}
變成咱們data
中的值。Watcher
就是它們兩者之間的橋樑了,在一開始的時候觀察全部存在插值的節點,當data
中的數據更新時,能夠通知模板,讓其從新渲染同步data
中的數據。
最後,其實我也不知道寫的這個算不算MVVM
(捂臉),編碼能力真心還有待提升,繼續加油吧!