這是本人的學習的記錄,由於最近在準備面試,不少狀況下會被提問到:請簡述 mvvm
? 通常狀況下我可能這麼答:mvvm
是視圖和邏輯的一個分離,是model view view-model
的縮寫,經過虛擬dom的方式實現雙向數據綁定(我隨便答得)javascript
那麼問題來了,你知道 mvvm
是怎麼實現的? 回答: mvvm
主要經過 Object
的 defineProperty
屬性,重寫 data
的 set
和get
函數來實現。 ok,回答得60分,那麼你知道具體實現過程麼?想一想看,就算他沒問到而你答了出來是否是更好?前提下,必定要手擼一下簡單的mvvm
纔會對它有印象~html
話很少說,接下來是參考自張仁陽老師的教學視頻而做,採用的是ES6語法,其中也包含了我我的的理解,若是能幫助到您,我將十分高興。若有錯誤之處,請各位大佬指正出來,不勝感激~~~vue
在實現以前,請先了解基本的mvvm
的編譯過程以及使用java
編譯的流程圖 node
總體分析 git
能夠發現new MVVM()
後的編譯過程主體分爲兩個部分:es6
Compile
<div>我很帥</div>
不執行編譯Observer
Dep
發佈訂閱,將全部須要通知變化的data
添加到一個數組中Watcher
若是數據發生改變,在Object
的defineProperty
的set
函數中調用Watcher
的update
方法Vue
實例中的屬性能夠正確綁定在標籤中,而且渲染在頁面中
{{}}
node.textContent
或者input
的value
編譯出來observe
類劫持數據變化Object.defineProperty
在get
鉤子中addSub
,set
鉤子中通知變化dep.notify()
dep.notify()
調用的是Watcher
的update
方法,也就是說須要在input
變化時調用更新先明確咱們的目標是:視圖的渲染和雙向的數據綁定以及通知變化!步驟:先從怎麼使用Vue入手一步步解析,從入口類Vue到編譯compile 目標【實現視圖渲染】,在此以前還有observe對數據進行劫持後再調用視圖的更新,watcher 類監聽變化到最後通知全部視圖的更新等等。github
如何入手?首先從怎麼使用Vue
開始。讓咱們一步步解析Vue
的使用:面試
let vm = new Vue({
el: '#app'
data: {
message: 'hello world'
}
})
複製代碼
上面代碼能夠看出使用Vue
,咱們是先new
一個Vue
實例,傳一個對象參數,包含 el
和 data
。json
ok,以上獲得了信息,接下來讓咱們實現目標1:將Vue
實例的data
編譯到頁面中
先看看頁面的使用:index.html
<div id="app">
<input type="text" v-model="jsonText.text">
<div>{{message}}</div>
{{jsonText.text}}
</div>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./vue.js"></script>
<script> let vm = new Vue({ el: '#app', data: { message: 'gershonv', jsonText:{ text: 'hello Vue' } } }) </script>
複製代碼
第一步固然是添加
Vue
類做爲一個入口文件。
新建一個vue.js
文件,其代碼以下 構造函數中定義$el
和$data
,由於後面的編譯要使用到
class Vue {
constructor(options) {
this.$el = options.el; // 掛載
this.$data = options.data;
// 若是有要編譯的模板就開始編譯
if (this.$el) {
// 用數據和元素進行編譯
new Compile(this.$el, this)
}
}
}
複製代碼
obeserve
,實現目標1暫時未用到,後續再添加el
和相關數據,上面代碼執行後會有編譯,因此咱們新建一個執行編譯的類的文件這裏在入口文件
vue.js
中new
了一個Compile
實例,因此接下來新建compile.js
Compile
須要作什麼? 咱們知道頁面中操做dom
會消耗性能,因此能夠把dom
移入內存處理:
dom
移入到內存中 (在內存中操做dom
速度比較快)
fragment
compile(fragment){}
v-model
{{}}
,而後進行相關操做。fragment
塞回頁面裏去class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {// 若是這個元素能獲取到 咱們纔開始編譯
// 1.先把這些真實的DOM移入到內存中 fragment[文檔碎片]
let fragment = this.node2fragment(this.el)
// 2.編譯 => 提取想要的元素節點 v-model 和文本節點 {{}}
this.compile(fragment)
// 3.編譯好的fragment在塞回頁面裏去
this.el.appendChild(fragment)
}
}
/* 專門寫一些輔助的方法 */
isElementNode(node) { // 判斷是否爲元素及節點,用於遞歸遍歷節點條件
return node.nodeType === 1;
}
/* 核心方法 */
node2fragment(el) { // 將el的內容所有放入內存中
// 文檔碎片
let fragment = document.createDocumentFragment();
while (el.firstChild) { // 移動DOM到文檔碎片中
fragment.appendChild(firstChild)
}
return fragment;
}
compile(fragment) {
}
}
複製代碼
補充:將el
中的內容移入文檔碎片fragment
中是一個進出棧的過程。el 的子元素被移到fragment
【出棧】後,el
下一個子元素會變成firstChild
。
編譯的過程就是把咱們的數據渲染好,表如今視圖中
{{}}
isElementNode
表明是節點元素,也是遞歸的終止的判斷條件。compileElement
和 編譯文本{{}}
的方法
compileElement
對v-model
、v-text
等指令的解析compileText
編譯文本節點 {{}}
class Compile{
// ...
compile(fragment) {
// 遍歷節點 可能節點套着又一層節點 因此須要遞歸
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 是元素節點 繼續遞歸
// 這裏須要編譯元素
this.compileElement(node);
this.compile(node)
} else {
// 文本節點
// 這裏須要編譯文本
this.compileText(node)
}
})
}
}
複製代碼
node.attributes
先判斷是否包含指令v-html v-text v-model...
) 調用不同的數據更新方法
CompileUtil
CompileUtil[type](node, this.vm, expr)
CompileUtil.類型(節點,實例,v-XX 綁定的屬性值)
class Compile{
// ...
// 判斷是不是指令 ==> compileElement 中遞歸標籤屬性中使用
isDirective(name) {
return name.includes('v-')
}
compileElement(node) {
// v-model 編譯
let attrs = node.attributes; // 取出當前節點的屬性
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
// 判斷屬性名是否包含 v-
if (this.isDirective(attrName)) {
// 取到對應的值,放到節點中
let expr = attr.value;
// v-model v-html v-text...
let [, type] = attrName.split('-')
CompileUtil[type](node, this.vm, expr);
}
})
}
compileText(node) {
// 編譯 {{}}
let expr = node.textContent; //取文本中的內容
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(expr)) {
CompileUtil['text'](node, this.vm, expr)
}
}
// compile(fragment){...}
}
CompileUtil = {
getVal(vm, expr) { // 獲取實例上對應的數據
expr = expr.split('.'); // 處理 jsonText.text 的狀況
return expr.reduce((prev, next) => {
return prev[next] // 譬如 vm.$data.jsonText.text、vm.$data.message
}, vm.$data)
},
getTextVal(vm, expr) { // 獲取文本編譯後的結果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1])
})
},
text(node, vm, expr) { // 文本處理 參數 [節點, vm 實例, 指令的屬性值]
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
},
model(node, vm, expr) { // 輸入框處理
let updateFn = this.updater['modelUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr))
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 輸入框更新
modelUpdater(node, value) {
node.value = value;
}
}
}
複製代碼
到如今爲止 就完成了數據的綁定,也就是說new Vue
實例中的 data
已經能夠正確顯示在頁面中了,如今要解決的就是如何實現雙向綁定
結合開篇的vue
編譯過程的圖能夠知道咱們還少一個observe
數據劫持,Dep
通知變化,添加Watcher
監聽變化, 以及最終重寫data
屬性
vue.js
中劫持數據class Vue{
//...
if(this.$el){
new Observer(this.$data); // 數據劫持
new Compile(this.$el, this); // 用數據和元素進行編譯
}
}
複製代碼
observer.js
文件代碼步驟:
observe
data
是否存在, 是不是個對象(new Vue 時可能不寫data
屬性)data
中的key
和value
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
// 要對這個數據將原有的屬性改爲 set 和 get 的形式
if (!data || typeof data !== 'object') {
return
}
// 將數據一一劫持
Object.keys(data).forEach(key => {
// 劫持
this.defineReactive(data, key, data[key])
this.observe(data[key]) //遞歸深度劫持
})
}
defineReactive(obj, key, value) {
let that = this
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 取值時調用的方法
return value
},
set(newValue) { // 當給data屬性中設置的時候,更改屬性的值
if (newValue !== value) {
// 這裏的this不是實例
that.observe(newValue) // 若是是對象繼續劫持
value = newValue
}
}
})
}
}
複製代碼
雖然有了
observer
,可是並未關聯,以及通知變化。下面就添加Watcher
類
新建watcher.js
文件
先回憶下watch
的用法:this.$watch(vm, 'a', function(){...})
咱們在添加發布訂閱者時須要傳入參數有: vm實例,v-XX綁定的屬性, cb回調函數 (getVal
方法拷貝了以前 CompileUtil
的方法,其實能夠提取出來的...)
class Watcher {
// 觀察者的目的就是給須要變化的那個元素增長一個觀察者,當數據變化後執行對應的方法
// this.$watch(vm, 'a', function(){...})
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先獲取下老的值
this.value = this.get();
}
getVal(vm, expr) { // 獲取實例上對應的數據
expr = expr.split('.');
return expr.reduce((prev, next) => { //vm.$data.a
return prev[next]
}, vm.$data)
}
get() {
let value = this.getVal(this.vm, this.expr);
return value
}
// 對外暴露的方法
update(){
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value
if(newValue !== oldValue){
this.cb(newValue); // 對應 watch 的callback
}
}
}
複製代碼
Watcher
定義了可是尚未調用,模板編譯的時候,須要調觀察的時候觀察一下 Compile
class Compile{
//...
}
CompileUtil = {
//...
text(node, vm, expr) { // 文本處理 參數 [節點, vm 實例, 指令的屬性值]
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], () => {
// 若是數據變化了,文本節點須要從新獲取依賴的屬性更新文本中的內容
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
},
//...
model(node, vm, expr) { // 輸入框處理
let updateFn = this.updater['modelUpdater'];
// 這裏應該加一個監控,數據變化了,應該調用watch 的callback
new Watcher(vm, expr, (newValue) => {
// 當值變化後會調用cb 將newValue傳遞過來()
updateFn && updateFn(node, this.getVal(vm, expr))
});
node.addEventListener('input', e => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue)
})
updateFn && updateFn(node, this.getVal(vm, expr))
},
//...
}
複製代碼
實現了監聽後發現變化並無通知到全部指令綁定的模板或是{{}}
,因此咱們須要Dep
監控、實例的發佈訂閱屬性的一個類,咱們能夠添加到observer.js
中
注意 第一次編譯的時候不會調用Watcher
,dep.target
不存在,new Watcher
的時候target
纔有值 有點繞,看下面代碼:
class Watcher {
constructor(vm, expr, cb) {
//...
this.value = this.get()
}
get(){
Dep.target = this;
let value = this.getVal(this.vm, this.expr);
Dep.target = null;
return value
}
//...
}
// compile.js
CompileUtil = {
model(node, vm, expr) { // 輸入框處理
//...
new Watcher(vm, expr, (newValue) => {
// 當值變化後會調用cb 將newValue傳遞過來()
updateFn && updateFn(node, this.getVal(vm, expr))
});
}
}
複製代碼
class Observer{
//...
defineReactive(obj, key, value){
let that = this;
let dep = new Dep(); // 每一個變化的數據 都會對應一個數組,這個數組存放全部更新的操做
Object.defineProperty(obj, key, {
//...
get(){
Dep.target && dep.addSub(Dep.target)
//...
}
set(newValue){
if (newValue !== value) {
// 這裏的this不是實例
that.observe(newValue) // 若是是對象繼續劫持
value = newValue;
dep.notify(); //通知全部人更新了
}
}
})
}
}
class Dep {
constructor() {
// 訂閱的數組
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
複製代碼
以上代碼 就完成了發佈訂閱者模式,簡單的實現。。也就是說雙向綁定的目標2已經完成了
板門弄斧了,本人無心譁衆取寵,這只是一篇個人學習記錄的文章。想分享出來,這樣纔有進步。 若是這篇文章幫助到您,我將十分高興。有問題能夠提issue
,有錯誤之處也但願你們能提出來,很是感激。
具體源碼我放在了個人github了,有須要的自取。 源碼連接