框架到底為我們做了什麼?
- 資料和檢視分離,解耦(開放封閉原則)
- 所有資料和檢視不分離的,都會命中開放封閉原則
Vue
資料獨立在data
裡面,檢視在template
中
- 以資料驅動檢視,只關心資料變化,
dom
操作被封裝- 使用原生js是直接通過操作
dom
來修改檢視,例如ducument.getElementById('xx').innerHTML="xxx"
- 以資料驅動檢視就是,我們只管修改資料,檢視的部分由框架去幫我們修改,符合開放封閉模式
- 使用原生js是直接通過操作
如何理解 MVVM ?
- MVC
Model
資料 →View
檢視 →Controller
控制器
- MVVM
MVVM
不算是一種創新- 但是其中的
ViewModel
是一種創新 ViewModel
是真正結合前端應用場景的實現
- 如何理解MVVM
MVVM - Model View ViewModel
,資料,檢視,檢視模型- 三者與
Vue
的對應:view
對應template
,vm
對應new Vue({…})
,model
對應data
- 三者的關係:
view
可以通過事件繫結的方式影響model
,model
可以通過資料繫結的形式影響到view
,viewModel
是把model
和view
連起來的聯結器
如何實現 MVVM - 以 Vue.js 為例
MVVM
框架的三大要素
- 響應式:
Vue
如何監聽到data
的每個屬性變化 - 模板引擎:
Vue
的模板如何被解析,指令如何處理 - 渲染:
Vue
的模板如何被渲染成html
,渲染過程是怎樣的
Vue 如何實現響應式
- 什麼是響應式
- 修改
data
屬性之後,Vue
立刻監聽到,立刻渲染頁面 data
屬性被代理到vm
上
- 修改
- Object.defineProperty
- 將物件屬性的值的設定和訪問 (get,set) 都變成函式,可以在當中加入我們自己的邏輯(進行監聽)
- 普通的
JavaScript
物件,做屬性修改,我們監聽不到,所以需要用到Object.defineProperty
- 既能get,又能set,才是雙向資料繫結
Vue 如何解析模板
- 模板是什麼
- 本質:模板就是字串
- 與html格式很像,但是模板中是有邏輯的,可以嵌入JS變數,如v-if, v-for等
- 檢視最終還是需要由模板生成
html
來顯示 - 模板必須先要轉換成JS程式碼
- 有邏輯(v-if, v-for),必須用JS才能實現(圖靈完備)
- 轉換為html渲染頁面,必須用JS才能實現
- 因此,模板要轉換成render函式
- render函式
- render函式包含了模板中所有的資訊,返回
vnode
,解決了模板中的邏輯(v-if, v-for)問題 - 如何找到最終生成的render函式
- 找到vue原始碼,搜尋
code.render
,將code列印出來,就是生成的render函式
- 找到vue原始碼,搜尋
- render函式包含了模板中所有的資訊,返回
- render函式與vdom
- 模板生成
html
:vm._c
vm._c
和snabbdom
中的h
函式的實現很像,都是傳入標籤,屬性,子元素作為引數Vue.js
的vdom
實現借鑑了snabbdom
updateComponent
中實現了vdom
的patch
- 頁面首次渲染執行
updateComponent
data
中每次修改屬性,都會執行updateComponent
- 模板生成
Vue.js 執行機制
- 第一步:解析模板成
render
函式- 因為在打包的時候就已經生成了render函式,所以編譯是第一步;響應式監聽是在程式碼執行的時候才開始監聽。
- 模板中的所有資訊都被render函式包含
- 模板中用到的data中的屬性,都變成了js變數
- 模板中的 v-model v-for v-on都變成了js邏輯
- render函式返回vnode
- 第二步:響應式開始監聽
- 通過Object.definedProperty監聽到物件屬性的get和set
- 將data的屬性代理到vm上
- 第三步:首次渲染,顯示頁面,且繫結依賴
- 初次渲染,執行
updateComponent
,執行vm._render()
- 執行
render
函式,會訪問到data
中的值,訪問時會被響應式的get
方法監聽到 - 執行
updateComponent
,會走到vdom
的patch
方法 patch
將vnode
渲染成dom
,初次渲染完成- 疑問:為何要監聽
get
,而不是直接監聽set
?- 因為
data
中有很多屬性,有些被用到,有些可能不被用到 - 只有被用到的才會走
get
- 沒有走到
get
中的屬性,set
的時候我們也無需關心 - 避免不必要的重新渲染
- 因為
- 初次渲染,執行
- 第四步:
data
屬性變化,觸發re-render
- 修改屬性,被響應式的
set
監聽到 set
中執行updateComponent
updateComponent
重新執行vm._render()
- 生成的
vnode
和prevVnode
,通過patch
進行對比 - 渲染到
html
中
- 修改屬性,被響應式的
手寫一個 Vue.js
index.html
這是最終的測試程式碼,我們自己實現的 Vue 在 XVue.js
和 compile.js
兩個檔案中,加起來大概200行程式碼左右,主要包括功能如下:
- 資料響應式:頁面中能直接引用data中的變數 test,我們給data.test重新賦值時,頁面能隨test值改變
- 雙向資料繫結:v-model
- 模板解析,處理指令和事件繫結:v-text v-model @click
- 渲染頁面:將模板轉化為 html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<div id="app">
{{test}}
<div v-text="test"></div>
<p>
<input type="text" v-model="test" />
</p>
<p v-html="html"></p>
<p>
<button @click="onClick">按鈕</button>
</p>
</div>
<script src="./compile.js"></script>
<script src="./XVue.js"></script>
<script>
const o = new XVue({
el: '#app',
data: {
test: '123',
foo: { bar: 'bar' },
html: '<button>html test</button>'
},
methods: {
onClick() {
alert('按鈕點選了')
}
}
})
console.log(o.$data.test) //123
o.$data.test = 'hello, Xvue!'
console.log(o.$data.test) //hello, Xvue!
</script>
</body>
</html>
Mini Vue 的組成部分:
- 監聽器 observe :資料劫持,實現響應式;屬性代理
- 依賴管理器 Dep :負責將檢視中所有依賴收集管理,包括依賴新增和通知更新
- 監聽器 Watcher :具體更新的執行者
- 編譯器 Compile :掃描模板中所有依賴(指令、插值、繫結、事件等),建立更新函式和監聽器( Watcher )
XVue.js
class XVue {
constructor(options) {
this.$data = options.data;
this.observe(this.$data);
// 執行編譯
new Compile(options.el, this);
}
observe(value) {
if (!value || typeof value !== 'object') {
return;
}
Object.keys(value).forEach(key => {
this.defineReactive(value, key, value[key]);
// 為vue的data做屬性代理
this.proxyData(key);
});
}
defineReactive(obj, key, val) {
// 遞迴查詢巢狀屬性
this.observe(val);
// 建立Dep
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依賴
Dep.target && dep.addDep(Dep.target);
// console.log(dep.deps);
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
},
});
}
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
},
});
}
}
// 依賴管理器:負責將檢視中所有依賴收集管理,包括依賴新增和通知
class Dep {
constructor() {
// deps裡面存放的是Watcher的例項
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
// 通知所有watcher執行更新
notify() {
this.deps.forEach(dep => {
dep.update();
});
}
}
// Watcher: 具體的更新執行者
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 將來 new 一個監聽器時,將當前 Watcher 例項附加到 Dep.target
// 將來通過 Dep.target 就能拿到當時建立的 Watcher 例項
Dep.target = this;
// 讀取操作,主動觸發 get,當前 Watcher 例項被新增到依賴管理器中
this.vm[this.key];
// 清空操作,避免不必要的重複新增(再次觸發 get 就不需要再新增 watcher 了)
Dep.target = null;
}
update() {
// console.log('from Watcher update: 檢視更新啦!!!');
// 通知頁面做更新
this.cb.call(this.vm, this.vm[this.key]);
}
}
compile.js
// 掃描模板中所有依賴(指令、插值、繫結、事件等)建立更新函式和watcher
class Compile {
// el是宿主元素或其選擇器
// vm當前Vue例項
constructor(el, vm) {
this.$el = document.querySelector(el);
this.$vm = vm;
if (this.$el) {
// 將dom節點轉換為Fragment提高執行效率
this.$fragment = this.node2Fragment(this.$el);
// 執行編譯,編譯完成以後所有的依賴已經替換成真正的值
this.compile(this.$fragment);
// 將生成的結果追加至宿主元素
this.$el.appendChild(this.$fragment);
}
}
node2Fragment(el) {
// 建立一個新的Fragment
const fragment = document.createDocumentFragment();
let child;
// 將原生節點移動至fragment
while ((child = el.firstChild)) {
// appendChild 是移動操作,移動一個節點,child 就會少一個,最終結束迴圈
fragment.appendChild(child);
}
return fragment;
}
// 編譯指定片段
compile(el) {
let childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// 判斷node型別,做相應處理
if (this.isElementNode(node)) {
// 元素節點要識別v-xx或@xx
this.compileElement(node);
} else if (
this.isTextNode(node) &&
/\{\{(.*)\}\}/.test(node.textContent)
) {
// 文字節點,只關心{{msg}}格式
this.compileText(node, RegExp.$1); // RegExp.$1匹配{{}}之中的內容
}
// 遍歷可能存在的子節點
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
compileElement(node) {
// console.log('編譯元素節點');
// <div v-text="test" @click="onClick"></div>
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
const attrName = attr.name; // 獲取屬性名 v-text
const exp = attr.value; // 獲取屬性值 test
if (this.isDirective(attrName)) {
// 指令
const dir = attrName.substr(2); // text
this[dir] && this[dir](node, this.$vm, exp);
} else if (this.isEventDirective(attrName)) {
// 事件
const dir = attrName.substr(1); // click
this.eventHandler(node, this.$vm, exp, dir);
}
});
}
compileText(node, exp) {
// console.log('編譯文字節點');
this.text(node, this.$vm, exp);
}
isElementNode(node) {
return node.nodeType == 1; //元素節點
}
isTextNode(node) {
return node.nodeType == 3; //元素節點
}
isDirective(attr) {
return attr.indexOf('v-') == 0;
}
isEventDirective(dir) {
return dir.indexOf('@') == 0;
}
// 文字更新
text(node, vm, exp) {
this.update(node, vm, exp, 'text');
}
// 處理html
html(node, vm, exp) {
this.update(node, vm, exp, 'html');
}
// 雙向繫結
model(node, vm, exp) {
this.update(node, vm, exp, 'model');
let val = vm.exp;
// 雙綁還要處理檢視對模型的更新
node.addEventListener('input', e => {
vm[exp] = e.target.value; // 這裡相當於執行了 set
});
}
// 更新
// 能夠觸發這個 update 方法的時機有兩個:1-編譯器初始化檢視時觸發;2-Watcher更新檢視時觸發
update(node, vm, exp, dir) {
let updaterFn = this[dir + 'Updater'];
updaterFn && updaterFn(node, vm[exp]); // 立即執行更新;這裡的 vm[exp] 相當於執行了 get
new Watcher(vm, exp, function (value) {
// 每次建立 Watcher 例項,都會傳入一個回撥函式,使函式和 Watcher 例項之間形成一對一的掛鉤關係
// 將來資料發生變化時, Watcher 就能知道它更新的時候要執行哪個函式
updaterFn && updaterFn(node, value);
});
}
textUpdater(node, value) {
node.textContent = value;
}
htmlUpdater(node, value) {
node.innerHTML = value;
}
modelUpdater(node, value) {
node.value = value;
}
eventHandler(node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp];
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm), false);
}
}
}