MVVM是指Model-View-ViewModeljavascript
init
過程當中初始化生命週期,初始化事件,初始化渲染,執行beforeCreate
周期函數,初始化data
,props
,computed
,watcher
,執行create
周期函數$mount
方法對vue實例進行掛載,包括模板編譯,渲染,更新template
,則須要進行編譯:將template
字符串編譯爲render function
$mount
的mountComponent
方法,先執行beforeCreate
周期函數,實例化一個渲染watcher
,在它的回調函數(初始化及數據變化時執行)中調用updateComponent
方法。render
方法將render function
渲染成虛擬domupdate
方法,update
方法會調用pacth
方法把虛擬DOM轉換成真正的DOM節點init
時會調用Object.defineProperty方法
監聽實例的數據變化(get和set方法),從而實現數據劫持。get
函數會進行訂閱收集(把監聽watcher
實例放到訂閱者Dep
的數組sub中),這是數據劫持和訂閱發佈模式就造成了ViewModelset
方法,set
會通知Dep
中相應的watcher
,watcher
調用update
方法來更新視圖。MVVM雙向數據綁定原理是經過 數據劫持+發佈訂閱 實現的。html
經過
Object.defineProperty()
來給對象的屬性添加get,set方法,在數據變更時觸發相應的監聽回調。vue
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。java
要實現一個MVVM的思路爲:node
vue.js數組
//Vue構造函數
function Vue(option = {}) {
this.$option = option;
let data = this._data = this.$option.data;
observe(data); // 數據劫持
// 數據代理,簡化data數據的寫法,如vue._data.name變成vue.name
for(let key in data) {
Object.defineProperty(this, key, {
configurable: true,
get() {
return this._data[key];
},
set(newVal) {
this._data[key] = newVal;
}
})
}
//初始化computed,將this指向實例
initComputed.call(this);
// 數據編譯,解析{{}}的內容
new Compile(option.el, this);
//執行mounted鉤子函數
option.mounted.call(this);
}
//數據劫持就是給對象增長get,set
function Observe(data) {
let dep =new Dep();
for(let key in data) {
let val = data[key];
observe(val) //遞歸繼續向下,實現深度的數據劫持
// Object.defineProperty定義對象的屬性
Object.defineProperty(data, key, {
configurable: true, // 能夠配置對象,刪除屬性
get() {
Dep.target && dep.addSub(Dep.target); //將watcher實例添加到訂閱事件中
return val
},
set(newVal){ //修改值的時候
if(val == newVal) { //值相同就不理
return;
}
val = newVal;
observe(newVal); //把新值也定義成屬性
dep.notify(); //執行watcher中的update方法
}
})
}
}
//遞歸函數
function observe(data) {
if(!data || typeof data != 'object') return;
return new Observe(data);
}
//編譯函數
function Compile(el, vm){
vm.$el = document.querySelector(el); // 將el掛載到實例上
let fragment = document.createDocumentFragment(); // 建立一個新的空白文檔片斷
while(child = vm.$el.firstChild) { //將el的內容都拿到,放入內存中,節省開銷
fragment.appendChild(child);
}
//替換內容
function replace(frag){
Array.from(frag.childNodes).forEach(node => {
let txt = node.textContent;
let reg = /\{\{(.*?)\}\}/g; // 正則匹配{{}}
if(node.nodeType === 1 && reg.test(txt)) { //既是文本節點又是大括號{{}}
function replaceTxt() {
node.textContent = txt.replace(reg, (matched, placholder) => {
//placholder匹配到的分組,name,age
new Watcher(vm, placholder, replaceTxt); // 監聽數據變化,替換{{}}的內容
return placholder.split('.').reduce((val, key) => { //reduce爲數組的每一個元素依次執行回調函數
return val[key]; //將vm的數據傳給val作初始值
}, vm)
})
}
replaceTxt();
}
//實現雙向綁定
if(node.nodeType === 1) {
let nodeAttr = node.attributes; //獲取元素上的屬性,類數組
Array.from(nodeAttr).forEach(attr => {
let name = attr.name; // v-model type
let exp = attr.value; // c
if(name.includes('v-')){
node.value = vm[exp]; // 將vm中的c的值,掛載到節點上
}
//監聽數據變化
new Watcher(vm, exp, function(newVal){
node.value = newVal;
})
node.addEventListener('input', e => {
let newVal = e.target.value;
//給vm中的值賦值
vm[exp] = newVal;
})
})
}
//子節點
if(node.childNodes && node.childNodes.length) {
replace(node);
}
})
}
replace(fragment);
vm.$el.appendChild(fragment);
}
//發佈訂閱,把函數放入數組就是訂閱,發佈就是讓函數執行
function Dep(){
this.subs = [];
}
Dep.prototype.addSub = function(sub) {
this.subs.push(sub);
}
Dep.prototype.notify = function() {
this.subs.forEach(sub => sub.update());
}
//監聽函數,給這個類建立的實例,添加update方法
function Watcher(vm, exp, fn){
this.fn = fn; //將fn放到實例上
this.vm = vm;
this.exp = exp;
// 定義一個屬性,target是Dep的一個靜態屬性,是一個全局watcher,dep其實是對watcher的一種管理
Dep.target = this;
let arr = exp.split('.');
let val = vm;
arr.forEach(key => {
val = val[key]; //獲取值的時候調用get()方法
})
Dep.target = null;
}
Watcher.prototype.update = function() {
// 值已經修改,再經過vm,exp來獲取新的值
let arr = this.exp.split('.');
let val = this.vm;
arr.forEach(key => {
val = val[key]; //經過get()獲取到新的值
})
this.fn(val); //fn爲替換{{}}的內容
}
//實現Computed
function initComputed() {
let vm = this;
let computed = this.$option.computed; // 從option上拿到computed屬性
Object.keys(computed).forEach(key => {
Object.defineProperty(vm, key, {
// 判斷computed的key是對象仍是函數,若是是函數會調get方法,若是是對象,手動調get方法
// sum獲取a,b的值會調get方法
get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set() {}
})
})
}
複製代碼
測試:app
<div id="mvvm">
<p>{{name}}</p>
<p>{{age}}</p>
<input type="text" v-model='c'/>
<p>{{c}}</p>
</div>
<script> let mvvm = new Vue({ el:'#mvvm', data: { name: '小明', age: 20, a: 10, b: 30, c: '' }, computed: { sum() { return this.a + this.b }, noop() {} }, mounted() { setTimeout(() => { console.log('完成'); }, 1000); } }) </script>
複製代碼
參考:dom