vue之mvvm原理解析
路過的朋友,可以點個贊,關注一下~~~
mvvm 面試論述
MVVM分為Model、View、ViewModel三者
Model
:代表資料模型,資料和業務邏輯都在Model
層中定義;View
:代表UI檢視,負責資料的展示;ViewModel
:負責監聽Model
中資料的改變並且控制檢視的更新,處理使用者互動操作;
這種模式實現了Model
和View
的資料自動同步,也就是雙向繫結,mvvm
雙向繫結,採用的是資料劫持結合釋出者-訂閱者模式的方式,通過Object.defineProperty()
來劫持各個屬性的 setter、getter
,在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。
大致的過程:
-
實現一個指令解析器
Compile
,對每個元素節點的之類進行解析,根據指令模板替換資料,以及搬到相應的更新函式 -
實現一個資料監控器
Observer
,將所有資料設定成響應式,並進行監聽,如有變動可以拿到最新值並通知訂閱者 -
實現一個訂閱者
Watcher
,作為連線Observer(資料劫持)
與Compile(模板)
的橋樑,在對應模板資料更新處,新增監聽資料的訂閱者,並將其新增到訂閱者容器Dep
中,當屬性變動時,通過Dep
釋出通知,執行指令繫結的相應回撥函式,從而更新檢視 -
mvvm的入口函式,主要是整合調控以上的,模板編譯(compile)、資料劫持(Observe)、訂閱者(Watcher),
mvvm的編譯過程以及使用
- 編譯的流程圖
- 整體分析
過程分析
當 new MVVM()
後的編譯主要分為兩個部分
-
一部分是模板的編譯
Compile
- 編譯元素和文字,將插值表示式進行替換
- 編譯模板指令的標籤,例如:
v-model
-
一部分是資料劫持
Observer
- 將所有的資料響應式處理
- 給模板的每個編譯處設定一個觀察者,並將觀察者存放在Dep中
Watcher
如果資料發生改變,在Object
的defineProperty
的set
函式中呼叫Watcher的update方法Dep
釋出訂閱,將所有需要通知變化的data
新增到一個陣列中
具體步驟
1、需要 observe 的資料物件進行遞迴遍歷,包括子屬性物件的屬性,都加上 setter 和 getter,這樣的話,給這個物件的某個值賦值,就會觸發 setter,那麼就能監聽到了資料變化
2、 compile 解析模板指令,將模板中的變數替換成資料,然後初始化渲染頁面檢視,並將每個指令對應的節點繫結更新函式,新增監聽資料的訂閱者,一旦資料有變動,收到通知,更新檢視
3、 Watcher 訂閱者是 Observer 和 Compile 之間通訊的橋樑,主要做的事情是:
- 在自身例項化時往屬性訂閱器(dep)裡面新增自己
- 自身必須有一個 update() 方法
- 待屬性變動 dep.notice() 通知時,能呼叫自身的 update() 方法,並觸發 Compile 中繫結的回撥,則功成身退。
4、MVVM 作為資料繫結的入口,整合 Observer、Compile 和 Watcher 三者,通過Observer來監聽自己的 model 資料變化,通過 Compile 來解析編譯模板指令,最終利用 Watcher 搭起 Observer 和 Compile 之間的通訊橋樑,達到資料變化 -> 檢視更新;檢視互動變化(input) -> 資料 model 變更的雙向繫結效果。
分解vue例項
vue的使用,
let vm = new Vue({
el:"#app",
data:{
school:{
name:"beida",
age:100
}
},
})
複製程式碼
首先使用
new
建立一個vue
例項,傳遞一個物件引數,包含el
和data
實現Complie編譯模板
index.html
頁面的使用
vue類-入口檔案
在入口之處,先處理了模板的編譯(Compile),資料劫持(Observe)在後期進行使用。
編譯模板
編譯模板的主要的入口,分為,將節點轉成文件碎片,替換模板中的常量資料
節點轉文件碎片
將節點轉換成文件碎片,然後返回,
編譯模板
//編譯模板
class Compiler{
//判斷一個節點是否是元素節點
isElementNode(node){
return node.nodeType ```= 1;
}
//判斷一個屬性是否是一個指令
isDirective(attrName){
return attrName.startsWith("v-");
}
//編譯模板
compile(node){
//node.childNodes 包含了元素節點與文字節點
//得到的是一個偽陣列
let childNodes = node.childNodes;
[...childNodes].forEach(child=>{
if(this.isElementNode(child)){ //元素節點
//編譯元素節點
this.compileElementNode(child)
//遞迴編譯所有的節點
this.compile(child)
}else{ //文字節點
//編譯文字節點
this.compileText(child)
}
})
}
//編譯元素節點
compileElementNode(node){
//獲取元素的屬性節點(偽陣列)
let attributes = node.attributes;
[...attributes].forEach(attr=>{
let {name,value: expr} = attr;
//判斷是否是一個指令
if(this.isDirective(name)){
let [,directive] = name.split("-")
//根據指令,呼叫對呀的指令方法
CompilerUtil[directive](node,expr,this.vm);
}
})
}
//編譯文字節點
compileText(node){
//得到所有的文字節點
let content = node.textContent;
//使用正則得到所有文字里面的內容
let reg = /\{\{(.+?)\}\}/;
if(reg.test(content)){
//{{}} 是v-text的語法糖,所有呼叫text指令
CompilerUtil['text'](node,content,this.vm)
}
}
//將節點轉成文件碎片
node2fragme(node){
//建立鍵一個文件碎片
let fragment = document.createDocumentFragment();
let firstChild ;
while(firstChild = node.firstChild){
fragment.appendChild(firstChild)
}
return fragment;
}
}
//編譯指令處理物件--處理不同的指令
CompilerUtil = {
//獲取到data中對應的資料
getVal(vm,expr){
return expr.split(".").reduce((data,current)=>{
return data[current]
},vm.$data)
},
//設定$data中的資料
setVal(vm,expr,value){
expr.split(".").reduce((data,current,index,arr)=>{
if(index ``` arr.length -1){
return data[current] = value;
}
return data[current]
},vm.$data)
},
//處理 v-model 指令的資料
model(node,expr,vm){
//更新模板中在data中對應的資料
let fn = this.updater['modelUpdater']
//當input 框的資料相互繫結
node.addEventListener("input",(e)=>{
let value = e.target.value;
//當輸入框資料改變時,同步更改$data中的資料
this.setVal(vm,expr,value);
})
let value = this.getVal(vm,expr)
//替換模板中的資料
fn(node,value)
},
// 處理v-text指令的資料
text(node,expr,vm){
let fn = this.updater['textUpdater'];
//獲取到要替換的內容
let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
return this.getVal(vm,args[1])
})
fn(node,content)
},
//更新模板中的資料
updater:{
//將v-model繫結的資料進行替換
modelUpdater(node,value){
node.value = value;
},
//將v-text繫結的資料進行替換
textUpdater(node,content){
node.textContent = content;
}
}
}
複製程式碼
編譯模板,主要是對模板中的一些資料常量進行替換,對於一些指令進行相關的處理,特別是指令v-model的資料的繫結。
資料劫持
資料劫持,實在模板進行編譯之前進行,將data中的所有的資料都變成響應式資料,
Observer
的呼叫 vue類
資料劫持 observer類
//資料劫持,將資料變成響應式資料
class Observer{
constructor(data){
//將資料變成響應式資料
this.observer(data)
}
//將資料變成響應式資料
observer(data){
//判斷資料是否是一個物件
if(data && typeof data ``` 'object'){
for(let key in data){
//設定響應式
this.defindReactive(data,key,data[key])
}
}
}
//設定響應式
defindReactive(obj,key,value){
//如果資料是一個物件,繼續遞迴設定
this.observer(value);
let dep = new Dep(); //不用的watcher存放到不同的dep中
Object.defineProperty(obj,key,{
//當獲取資料時會呼叫get
get(){
return value;
},
//當設定資料時會呼叫set
set: (newValue)=>{
if(newValue != value){
//將新資料設定成響應式
this.observer(newValue);
value = newValue;
}
}
})
}
}
複製程式碼
訂閱者的Watcher的實現
訂閱者watcher
//觀察者
class Watcher{
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb; //狀態改變後要進行的操作
//獲取老資料--儲存一個老狀態
this.oldValue = this.get();
}
//獲取狀態的方法
get(){
Dep.target = this;
//當獲取舊的值得時候便已經觸發響應式資料
let value = CompilerUtil.getVal(this.vm,this.expr)
Dep.target = null;
return value;
}
//當狀態發生改變的時候,觀察者更新當前的狀態
update(){
let newVal = CompilerUtil.getVal(this.vm,this.expr);
if(this.oldValue !``` newVal){
this.cb(newVal)
}
}
}
複製程式碼
存放訂閱者Dep
//儲存觀察者的類
class Dep {
constructor(){
this.subs = []; //存放所有的watcher
}
//新增watcher 訂閱
addSub(watcher){
this.subs.push(watcher)
}
//通知釋出
notify(){
this.subs.forEach(watcher=>watcher.update())
}
}
複製程式碼
訂閱者,連線編譯模板與資料劫持
編譯模板處
//編譯指令處理物件--處理不同的指令
CompilerUtil = {
// 此處省略若該程式碼 ......
//處理 v-model 指令的資料
model(node,expr,vm){
//更新模板中在data中對應的資料
let fn = this.updater['modelUpdater']
//給輸入框新增一個觀察者,如果資料改變,通知data資料改變
new Watcher(vm,expr,(newValue) =>{
fn(node,newValue)
})
// 此處省略若該程式碼 ......
},
// 處理v-text指令的資料
text(node,expr,vm){
let fn = this.updater['textUpdater'];
let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
//新增一個訂閱者
new Watcher(vm,args[1],()=>{
fn(node,this.getVal(vm,args[1]))
})
return this.getVal(vm,args[1])
})
fn(node,content)
},
// 此處省略若該程式碼 ......
}
複製程式碼
資料劫持處
//資料劫持,將資料變成響應式資料
class Observer{
// 此處省略若該程式碼 ...
//設定響應式
defindReactive(obj,key,value){
// 此處省略若該程式碼 ...
Object.defineProperty(obj,key,{
//當獲取資料時會呼叫get
get(){
Dep.target && dep.subs.push(Dep.target)
return value;
},
//當設定資料時會呼叫set
set: (newValue)=>{
if(newValue != value){
//將新資料設定成響應式
this.observer(newValue);
value = newValue;
//當資料發生改變時,通知觀察者
dep.notify();
}
}
})
}
}
複製程式碼
總述:訂閱者是,編譯模板與資料劫持之間的橋樑,模板編譯之處新增訂閱者,並將訂閱者儲存在Dep中,在資料劫持處新增發布者,當資料發生改變的時候,通知訂閱者。