let vm = new MVVM({
el:'#app',
data:{
message:{
a: 'hello zfpx',
},
a:1
}
})
複製代碼
vue中初始化一個vue實例是這樣用的,那麼MVVM內部原理到底是怎麼實現的呢?vue
MVVM實現圖示以下 node
如圖主要分爲5部分:MVVM類,Compile, Observer,Dep,Watcher1. MVVM類實現數組
MVVM類主要功能:bash
class MVVM{
constructor(options){
// 先把可用的東西掛載在實例上
this.$el = options.el;
this.$data = options.data;
// 若是有要編譯的模板,就開始編譯
if(this.$el){
// 數據劫持 就是把對想的全部屬性 改爲get和set方法
new Observer(this.$data);
//將$data代理到this上
this.proxyData(this.$data);
// 用數據和元素進行編譯
new Compile(this.$el, this);
}
}
proxyData(data){
Object.keys(data).forEach(key=>{
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newValue){
data[key] = newValue
}
})
})
}
}
複製代碼
2. Observer類實現 Observer類主要功能:app
class Observer{
constructor(data){
this.observe(data);
}
observe(data){
// 要對這個data數據將原有的屬性改爲set和get的形式
if(!data || typeof data !== 'object'){
return;
}
// 要將數據 一一劫持 先獲取取到data的key和value
Object.keys(data).forEach(key=>{
// 劫持
this.defineReactive(data,key,data[key]);
this.observe(data[key]);// 深度遞歸劫持
});
}
// 定義響應式
defineReactive(obj,key,value){
let that = this;
// 每一個屬性 都會對應一個數組,這個數組是存放全部更新的操做
let dep = new Dep();
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){
// 當取值時,將Dep.target(watcher即set屬性時,要執行的回調函數)推入數組
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue){
// 當給data屬性中設置值的時候 更改獲取的屬性的值
if(newValue!=value){
// 這裏的this不是實例
// 若是是對象繼續劫持
// 好比你this.$data = {a: '2'}換成了一個新對象
that.observe(newValue);
value = newValue;
dep.notify(); // 通知全部watcher數據更新了
}
}
});
}
}
複製代碼
3. Dep類實現 Dep類的主要功能dom
class Dep{
constructor(){
// 訂閱的數組
this.subs = []
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
複製代碼
4.watcher類實現函數
watcher功能ui
class Watcher{
// vm: vm實例 expr: {{message.a}}中的message.a cb:屬性更新後的回調函數
constructor(vm,expr,cb){
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先獲取一下老的值,進入get方法,將watcher實例賦給Dep.target
//而後 執行this.getval()方法時,會讀取data對象上的屬性
// 一旦讀取屬性(數據劫持)
// 就會執行Observer類中的defineReactive方法
// 執行這裏:Dep.target && dep.addSub(Dep.target);
//將watcher實例推入訂閱的數組
this.value = this.get(); // 讀取老值
}
// 獲取data對象上對應的expr屬性值
getVal(vm, expr) {
expr = expr.split('.'); // [message,a]
return expr.reduce((prev, next) => { // vm.$data.a
return prev[next];
}, vm.$data);
}
get(){
Dep.target = this;
let value = this.getVal(this.vm,this.expr);
Dep.target = null;
return value;
}
// 對外暴露的方法
//一旦給data對象的屬性設置值,就會執行Observer類defineReactive方法中的
// dep.notify()--》執行訂閱的數組中的watcher隊列的update方法
update(){
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if(newValue != oldValue){
this.cb(newValue); // 對應watch的callback
}
}
}
// 用新值和老值進行比對 若是放生變化 就調用更新方法
複製代碼
5. Compile類實現 Compile類功能this
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;
}
// 是否是指令
isDirective(name) {
return name.includes('v-');
}
/* 核心的方法 */
compileElement(node) {
// 帶v-model v-text
let attrs = node.attributes; // 取出當前節點的屬性
Array.from(attrs).forEach(attr => {
// 判斷屬性名字是否是包含v-model
let attrName = attr.name;
if (this.isDirective(attrName)) {
// 取到對應的值放到節點中
let expr = attr.value;
let [, type] = attrName.split('-');
// node this.vm.$data expr
CompileUtil[type](node, this.vm, expr);
}
})
}
compileText(node) {
// {{}}的處理
let expr = node.textContent; // 取文本中的內容
let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
if (reg.test(expr)) {
// node this.vm.$data text
CompileUtil['text'](node, this.vm, expr);
}
}
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);
}
});
}
node2fragment(el) {
// 須要將el中的內容所有放到內存中
// 文檔碎片 內存中的dom節點
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment; // 內存中的節點
}
}
CompileUtil = {
getVal(vm, expr) { // 獲取實例上對應的數據
expr = expr.split('.'); // [message,a]
return expr.reduce((prev, next) => { // vm.$data.a
return prev[next];
}, vm.$data);
},
getTextVal(vm, expr) {
// 獲取編譯文本後的結果 {{message.a}}--> message.a-->data.message.a
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1]);
})
},
text(node, vm, expr) { // 文本處理
let updateFn = this.updater['textUpdater'];
// {{message.a}} => hello,zfpx;
let value = this.getTextVal(vm, expr);
// {{a}} {{b}}
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
//每一個屬性都對應一個watcher
new Watcher(vm, arguments[1],(newValue)=>{
// 若是數據變化了,文本節點須要從新獲取依賴的屬性更新文本中的內容
updateFn && updateFn(node,this.getTextVal(vm,expr));
});
})
updateFn && updateFn(node, value)
},
setVal(vm,expr,value){ // [message,a]
expr = expr.split('.');
// 收斂
return expr.reduce((prev,next,currentIndex)=>{
if(currentIndex === expr.length-1){
return prev[next] = value;
}
return prev[next];
},vm.$data);
},
model(node, vm, expr) { // 輸入框處理
let updateFn = this.updater['modelUpdater'];
// 這裏應該加一個監控 數據變化了 應該調用這個watch的callback
new Watcher(vm,expr,(newValue)=>{
// 當值變化後會調用cb 將新的值傳遞過來 ()
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));
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 輸入框更新
modelUpdater(node, value) {
node.value = value;
}
}
}
複製代碼