當我們對vue的用法較為熟練的時候,但有時候在排查bug的時候還是會有點迷惑。主要是因為對vue各種用法和各種api使用都是只知其然而不知其所以然。這時候我們想到可以去看看源碼,但是源碼太長,其實我們只要把大概實現流程實現一遍,很多開發中想不明白的地方就會豁然開朗。下面我們就來實現一個簡單的vue.js
vue采取數據劫持,配合觀察者模式,通過Object.defineProperty() 來劫持各個屬性的setter和getter,在數據變動時,發布消息給依賴收集器dep,去通知觀察者,做出對應的回調函數,去更新視圖。(也就是在getter中收集依賴,在setter中通知依賴更新。)
其實vue主要就是整合Observer,compile和watcher三者,通過Observer來監聽 model數據變化表,通過compile來解析編譯模板指令,最終利用Watcher搭起observer 和compile的通信橋梁,達到數據變化=>視圖變化,視圖變化=>數據變化的雙向綁定效果。
下面來一張圖↓

這個流程圖已經非常形象深刻的表達了vue的運行模式,當你理解了這個流程,再去看vue源碼時就會容易很多了
聲明一下,下面的代碼只簡單實現了vue里的
- v-model(數據的雙向綁定)
- v-bind/v-on
- v-text/v-html
- 沒有實現虛擬dom,采用文檔碎片(createDocumentFragment)代替
- 數據只劫持了Object,數組Array沒有做處理
代碼大致結構如下,初步定義了6個類

代碼如下,具體操作案例可以看==>GitHub
// 定義Vue類
class Vue {
constructor(options) {
// 把數據對象掛載到實例上
this.$el = options.el;
this.$data = options.data;
this.$options = options;
// 如果有需要編譯的模板
if (this.$el) {
// 數據劫持 就是把對象的所有屬性 改成get和set方法
new Observer(this.$data);
// 用數據和元素進行編譯
new Compiler(this.$el, this);
// 3. 通過數據代理實現 主要給methods里的方法this直接訪問data
this.proxyData(this.$data);
}
}
//用vm代理vm.$data
proxyData(data){
for(let key in data){
Object.defineProperty(this,key,{
get(){
return data[key];
},
set(newVal){
data[key] = newVal;
}
})
}
}
}
// 編譯html模板
class Compiler {
// vm就是vue對象
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);
// console.log(fragment);
// 2.編譯 => 提取想要的元素節點 v-model和文本節點{{}}
this.compile(fragment);
// 3.把編譯好的fragment再放到頁面里
this.el.appendChild(fragment);
}
}
/* 一些輔助方法 */
isElementNode(node) {
return node.nodeType === 1;
}
isDirective(name) { // 判斷是不是指令
return name.includes('v-');
}
isEventName(attrName){ // 判斷是否@開頭
return attrName.startsWith('@');
}
isBindName(attrName){ // 判斷是否:開頭
return attrName.startsWith(':');
}
/* 核心方法區 */
node2fragment(el){ // 需要將el中的內容全部放到內存中
// 文檔碎片
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){
fragment.appendChild(firstChild);
}
return fragment; // 內存中的節點
}
compile(fragment){
// 1.獲取子節點
let childNodes = fragment.childNodes;
// 2.遞歸循環編譯
[...childNodes].forEach(node=>{
if(this.isElementNode(node)){
this.compileElement(node); // 這里需要編譯元素
this.compile(node); // 是元素節點,還需要繼續深入的檢查
}else{
// 文本節點
// 這里需要編譯文本
this.compileText(node);
}
});
}
compileElement(node){ // 編譯元素
// 帶v-model v-html ...
let attrs = node.attributes; // 取出當前節點的屬性
// attrs是類數組,因此需要先轉數組
[...attrs].forEach(attr=>{
// console.log(attr); // type="text" v-model="content" v-on:click="handleclick" @click=""...
let attrName = attr.name; // type v-model v-on:click @click
if(this.isDirective(attrName)){ // 判斷屬性名字是不是包含v-
// 取到對應的值放到節點中
let expr = attr.value; // content/變量 handleclick/方法名
// console.log(expr)
let [, type] = attrName.split('-'); // model html on:click
let [compileKey, detailStr] = type.split(':'); // 處理 on: bind:
// node this.vm.$data expr
CompileUtil[compileKey](node, this.vm, expr, detailStr);
// 刪除有指令的標簽屬性 v-text v-html等,普通的value等原生html標簽不必刪除
node.removeAttribute('v-' + type);
}else if(this.isEventName(attrName)){ // 如果是事件處理 @click='handleClick'
let [, detailStr] = attrName.split('@');
CompileUtil['on'](node, this.vm, attr.value, detailStr);
// 刪除有指令的標簽屬性
node.removeAttribute('@' + detailStr);
}else if(this.isBindName(attrName)){ // 如果是:開頭,動態綁定值
let [, detailStr] = attrName.split(':');
CompileUtil['bind'](node, this.vm, attr.value, detailStr);
// 刪除有指令的標簽屬性
node.removeAttribute(':' + detailStr);
}
})
}
compileText(node){ // 編譯文本
// 帶{{}}
let expr = node.textContent; // 取文本中的內容
let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}}
if(reg.test(expr)){
// node this.$data
// console.log(expr); // {{content}}
CompileUtil['text'](node, this.vm, expr);
}
}
}
// 編譯模版具體執行
const CompileUtil = {
getVal(vm, expr){ // 獲取實例上對應的數據
expr = expr.split('.'); // [animal,dog]/[animal,cat]
return expr.reduce((prev, next)=>{ // vm.$data.
return prev[next];
}, vm.$data)
},
// 這里實現input輸入值變化時 修改綁定的v-model對應的值
setVal(vm, expr, inputValue){ // [animal,dog]
let exprs = expr.split('.'), len = exprs.length;
exprs.reduce((data,currentVal, idx)=>{
if(idx===len-1){
data[currentVal] = inputValue;
}else{
return data[currentVal]
}
}, vm.$data)
},
getTextVal(vm, expr){ // 獲取編譯文本后的結果
return expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
// console.log(args); // ["{{title}}", "title", 0, "{{title}}"]
// ["{{ animal.dog }}", " animal.dog ", 0, "{{ animal.dog }}-vs-{{ animal.cat }}"]
return this.getVal(vm, args[1].trim());
});
},
text(node, vm, expr){ // 文本處理
let updateFn = this.updater['textUpdater'];
// {{content}} => "welcome to animal world"
let value;
if(expr.indexOf('{{')!==-1){ // dom里直接寫{{}}的時候
value = this.getTextVal(vm, expr);
// {{a}} {{b}} 對多個值進行監控
expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
new Watcher(vm, args[1].trim(), ()=>{
// 如果數據變化了,文本節點需要重新獲取依賴的屬性更新文本中的內容
updateFn && updateFn(node, this.getTextVal(vm, expr));
})
});
}else{ // v-text 的時候
value = this.getVal(vm, expr);
new Watcher(vm, expr, (newVal)=>{
// 當值變化后會調用cb 將新值傳遞過來
updateFn && updateFn(node, newVal);
});
}
updateFn && updateFn(node, value);
},
html(node, vm, expr) { //
let updateFn = this.updater['htmlUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr));
},
model(node, vm, expr){ // 輸入框處理
let updateFn = this.updater['modelUpdater'];
// console.log(this.getVal(vm, expr)); // "welcome to animal world"
// 這里應該加一個監控 數據變化了 應該調用這個watch的callback
new Watcher(vm, expr, (newVal)=>{
// 當值變化后會調用cb 將新值傳遞過來
updateFn && updateFn(node, newVal);
});
// 視圖 => 數據 => 視圖
node.addEventListener('input', (e)=>{
this.setVal(vm, expr, e.target.value);
})
updateFn && updateFn(node, this.getVal(vm, expr));
},
on(node, vm, expr, detailStr) {
let fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(detailStr, fn.bind(vm), false);
},
bind(node, vm, expr, detailStr){
// v-bind:src='...' => href='...'
node.setAttribute(detailStr, expr);
},
updater:{
// 文本更新
textUpdater(node, value){
node.textContent = value;
},
// html更新
htmlUpdater(node, value){
node.innerHTML = value;
},
// 輸入框更新
modelUpdater(node, value){
node.value = value;
}
}
}
// 觀察者
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 dep = new Dep();
// 在獲取某個值的時候
Object.defineProperty(obj, key, {
enumerable: true, // 可枚舉
configurable: true, // 可修改
get(){ // 當取值的時候
// 訂閱數據變化時,往Dev中添加觀察者
Dep.target && dep.addSub(Dep.target);
return value;
},
// 采用箭頭函數在定義時綁定this的定義域
set: (newVal)=>{ // 更改data里的屬性值的時候
if(value === newVal) return;
this.observe(newVal); // 如果設置新值是對象,劫持
value = newVal;
// 通知watcher數據發生改變
dep.notify();
}
})
}
}
// 觀察者的目的就是給需要變化的那個元素增加一個觀察者,當數據變化后執行對應的方法
class Watcher{
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先獲取一下老的值
this.oldVal = this.getOldVal();
}
// 獲取實例上對應的老值
getOldVal(){
// 在利用getValue獲取數據調用getter()方法時先把當前觀察者掛載
Dep.target = this;
const oldVal = CompileUtil.getVal(this.vm, this.expr);
// 掛載完畢需要注銷,防止重復掛載 (數據一更新就會掛載)
Dep.target = null;
return oldVal;
}
// 對外暴露的方法 通過回調函數更新數據
update(){
const newVal = CompileUtil.getVal(this.vm, this.expr);
if(newVal !== this.oldVal){
this.cb(newVal); // 對應watch的callback
}
}
}
// Dep類存儲watcher對象,並在數據變化時通知watcher
class Dep{
constructor(arg) {
// 訂閱的數組
this.subs = []
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){ // 數據變化時通知watcher更新
this.subs.forEach(w=>w.update());
}
}
