Study Notes
本博主會(huì)持續(xù)更新各種前端的技術(shù),如果各位道友喜歡惶我,可以關(guān)注接箫、收藏、點(diǎn)贊下本博主的文章茄猫。
深入響應(yīng)式原理
數(shù)據(jù)響應(yīng)式狈蚤、雙向綁定、數(shù)據(jù)驅(qū)動(dòng)
-
數(shù)據(jù)響應(yīng)式
數(shù)據(jù)模型僅僅是普通的 JavaScript 對(duì)象划纽,而當(dāng)我們修改數(shù)據(jù)時(shí)脆侮,視圖會(huì)進(jìn)行更新,避免了繁瑣的 DOM 操作勇劣,提高開發(fā)效率
-
雙向綁定
- 數(shù)據(jù)改變靖避,視圖改變潭枣;視圖改變,數(shù)據(jù)也隨之改變
- 我們可以使用 v-model 在表單元素上創(chuàng)建雙向數(shù)據(jù)綁定
-
數(shù)據(jù)驅(qū)動(dòng)是 Vue 最獨(dú)特的特性之一
開發(fā)過程中僅需要關(guān)注數(shù)據(jù)本身幻捏,不需要關(guān)心數(shù)據(jù)是如何渲染到視圖
Vue2.x 響應(yīng)式原理
當(dāng)你把一個(gè)普通的 JavaScript 對(duì)象傳入 Vue 實(shí)例作為 data 選項(xiàng)盆犁,Vue 將遍歷此對(duì)象所有的 property,并使用 Object.defineProperty 把這些 property 全部轉(zhuǎn)為 getter/setter篡九。Object.defineProperty 是 ES5 中一個(gè)無(wú)法 shim 的特性谐岁,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。
對(duì)象單屬性數(shù)據(jù)劫持
-
configurable
當(dāng)且僅當(dāng)該屬性的 configurable 鍵值為 true 時(shí)榛臼,該屬性的描述符才能夠被改變伊佃,同時(shí)該屬性也能從對(duì)應(yīng)的對(duì)象上被刪除。
-
enumerable
當(dāng)且僅當(dāng)該屬性的 enumerable 鍵值為 true 時(shí)沛善,該屬性才會(huì)出現(xiàn)在對(duì)象的枚舉屬性中航揉。
默認(rèn)為 false。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<script>
const el = document.createElement('p');
el.textContent = '在控制臺(tái)輸入vm.msg = 123';
document.body.append(el);
// 模擬Vue中的data選項(xiàng)
let data = { msg: 12 };
// 模擬vue實(shí)例
let vm = {};
// 數(shù)據(jù)劫持:當(dāng)訪問或者設(shè)置 vm 中的成員的時(shí)候金刁,做一些干預(yù)操作
Object.defineProperty(vm, 'msg', {
configurable: true,
enumerable: true,
// 當(dāng)獲取值的時(shí)候執(zhí)行
get: () => {
console.log('獲取');
return data.msg;
},
// 當(dāng)賦值的時(shí)候執(zhí)行
set: (val) => {
console.log('賦值msg ==> ', val);
if (val === data.msg) {
return;
}
data.msg = val;
// 數(shù)據(jù)更改帅涂,更新 DOM 的值
document.getElementsByTagName('p')[0].textContent = val;
},
});
</script>
</body>
</html>
對(duì)象多屬性數(shù)據(jù)劫持
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body>
<script>
const el = document.createElement('p');
el.textContent = '1231';
document.body.append(el);
let data = {
msg: '成功',
code: 1,
};
let vm = {};
Object.keys(data).forEach((key) => {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get: () => {
console.log(`獲取 ${key}==>${data[key]}`);
return data[key];
},
set: (val) => {
if (val === data[key]) {
return;
}
console.log(`賦值 ${key}==>${val}`);
data[key] = val;
document.getElementsByTagName('p')[0].textContent = val;
},
});
});
</script>
</body>
</html>
vue3.x 響應(yīng)式原理
- Proxy
- 直接監(jiān)聽對(duì)象,而非屬性尤蛮。
-
ES6
中新增媳友,IE 不支持,性能由瀏覽器優(yōu)化
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Title</title>
</head>
<body></body>
<script>
const el = document.createElement('p');
el.textContent = '在控制臺(tái)輸入vm.msg = 123';
document.body.append(el);
let data = {
msg: '成功',
code: 0,
};
const vm = new Proxy(data, {
get(target, key) {
console.log(`獲取${key}===>${target[key]}`);
return target[key];
},
set(target, key, value) {
if (target[key] === value) return;
console.log(`設(shè)置${key}===>${value}`);
target[key] = value;
document.getElementsByTagName('p')[0].textContent = value;
},
});
</script>
</html>
發(fā)布訂閱模式和觀察者模式
發(fā)布/訂閱模式
- 訂閱者
- 發(fā)布者
- 信號(hào)中心
我們假定产捞,存在一個(gè)"信號(hào)中心"庆锦,某個(gè)任務(wù)執(zhí)行完成,就向信號(hào)中心"發(fā)布"(publish)一個(gè)信號(hào)轧葛,其他任務(wù)可以向信號(hào)中心"訂閱"(subscribe)這個(gè)信號(hào),從而知道什么時(shí)候自己可以開始執(zhí)行艇搀。這就叫做"發(fā)布/訂閱模式"(publish-subscribe pattern)
Vue 的自定義事件
let vm = new Vue();
vm.$on('dataChange', () => {
console.log('dataChange');
});
vm.$on('dataChange', () => {
console.log('dataChange1');
});
vm.$emit('dataChange');
兄弟組件通信過程
// eventBus.js
// 事件中心
export let eventHub = new Vue()
// ComponentA.vue
import {eventHub} from './eventBus'
// 發(fā)布者
addTodo: function () {
// 發(fā)布消息(事件)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ''
}
// ComponentB.vue
import {eventHub} from './eventBus'
// 訂閱者
created: function () {
// 訂閱消息(事件)
eventHub.$on('add-todo', this.addTodo)
}
模擬 Vue 自定義事件的實(shí)現(xiàn)
/**
* @author Wuner
* @date 2020/7/29 21:58
* @description
*/
class EventEmitter {
constructor() {
this.events = {};
}
// 訂閱通知
$on(eventType, handle) {
this.events[eventType] = this.events[eventType] || [];
this.events[eventType].push(handle);
}
// 發(fā)布通知
$emit(eventType) {
if (this.events[eventType]) {
this.events[eventType].forEach((handle) => handle());
}
}
}
// 測(cè)試
let ev = new EventEmitter();
ev.$on('click', () => {
console.log('click1');
});
ev.$on('click', () => {
console.log('click2');
});
ev.$on('next', () => {
console.log('next');
});
ev.$emit('click');
ev.$emit('next');
觀察者模式
- 觀察者(訂閱者) -- Watcher
- update():當(dāng)事件發(fā)生時(shí)尿扯,具體要做的事情
- 目標(biāo)(發(fā)布者) -- Dep
- subs 數(shù)組:存儲(chǔ)所有的觀察者
- addSub():添加觀察者
- notify():當(dāng)事件發(fā)生,調(diào)用所有觀察者的 update() 方法
- 沒有事件中心
/**
* @author Wuner
* @date 2020/7/29 22:45
* @description
*/
// 目標(biāo)(發(fā)布者)
// Dependency
class Dep {
constructor() {
// 存儲(chǔ)所有的觀察者
this.subs = [];
}
// 添加觀察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub);
}
}
// 通知所有觀察者
notify() {
this.subs.forEach((sub) => sub.update());
}
}
// 觀察者(訂閱者)
class Watcher {
update() {
console.log('update');
}
}
// 測(cè)試
const watcher = new Watcher();
const dep = new Dep();
dep.addSub(watcher);
dep.notify();
總結(jié)
- 觀察者模式是由具體目標(biāo)調(diào)度焰雕,比如當(dāng)事件觸發(fā)衷笋,Dep 就會(huì)去調(diào)用觀察者的方法,所以觀察者模式的訂閱者與發(fā)布者之間是存在依賴的矩屁。
- 發(fā)布/訂閱模式由統(tǒng)一調(diào)度中心調(diào)用辟宗,因此發(fā)布者和訂閱者不需要知道對(duì)方的存在。
Vue 響應(yīng)式原理模擬
整體分析
-
Vue
把 data 中的成員注入到 Vue 實(shí)例吝秕,并且把 data 中的成員轉(zhuǎn)成 getter/setter
-
Observer(數(shù)據(jù)劫持)
能夠?qū)?shù)據(jù)對(duì)象的所有屬性進(jìn)行監(jiān)聽泊脐,如有變動(dòng)可拿到最新值并通知 Dep
-
Compiler(解析指令)
解析每個(gè)元素中的指令/插值表達(dá)式,并替換成相應(yīng)的數(shù)據(jù)
-
Dep(訂閱者)
添加觀察者(watcher)烁峭,當(dāng)數(shù)據(jù)變化通知所有觀察者
-
Watcher(觀察者)
數(shù)據(jù)變化更新視圖
Vue
- 負(fù)責(zé)接收初始化的參數(shù)(選項(xiàng))
- 負(fù)責(zé)把 data 中的屬性注入到 Vue 實(shí)例容客,轉(zhuǎn)換成 getter/setter
- 負(fù)責(zé)調(diào)用 observer 監(jiān)聽 data 中所有屬性的變化
- 負(fù)責(zé)調(diào)用 compiler 解析指令/插值表達(dá)式
/**
* @author Wuner
* @date 2020/7/29 23:34
* @description
*/
class Vue {
constructor(options) {
// 初始化參數(shù)(選項(xiàng))
this.$options = options || {};
this.$data = this.$options.data || {};
const el = this.$options.el;
// 判斷el是否是字符串秕铛,如果是的話,則通過querySelector找到dom節(jié)點(diǎn)缩挑,否則直接賦值dom
this.$el = typeof el === 'string' ? document.querySelector(el) : el;
// 負(fù)責(zé)把data中的屬性但两,注入到vue實(shí)例,并轉(zhuǎn)換為getter和setter
this._proxyData(this.$data);
// 調(diào)用 observer 監(jiān)聽 data 中所有屬性的變化
new Observer(this.$data);
// 編譯
new Compiler(this);
}
_proxyData(data) {
// 遍歷 data 的所有屬性
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(val) {
if (val === data[key]) {
return;
}
data[key] = val;
},
});
});
}
}
Observer
- 負(fù)責(zé)把 data 選項(xiàng)中的屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
- data 中的某個(gè)屬性也是對(duì)象供置,把該屬性轉(zhuǎn)換成響應(yīng)式數(shù)據(jù)
- 數(shù)據(jù)變化發(fā)送通知
/**
* @author Wuner
* @date 2020/7/30 0:12
* @description
*/
// 負(fù)責(zé)數(shù)據(jù)劫持
// 把 $data 中的成員轉(zhuǎn)換成 getter/setter
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
// 判斷數(shù)據(jù)是否是對(duì)象谨湘,如果是對(duì)象,則遍歷對(duì)象的所有屬性芥丧,設(shè)置為 getter/setter
if (data && typeof data === 'object') {
// 遍歷 data 的所有成員
Object.keys(data).forEach((key) =>
this.defineReactive(data, key, data[key]),
);
}
}
// 定義響應(yīng)式成員
defineReactive(data, key, val) {
let dep = new Dep();
// 如果val是對(duì)象紧阔,,繼續(xù)設(shè)置它下面的成員為響應(yīng)式數(shù)據(jù)
this.walk(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: () => {
// 收集依賴
Dep.target && dep.addSub(Dep.target);
// 這里val不能通過data[key]獲取娄柳,否則會(huì)陷入自調(diào)用死循環(huán)
return val;
},
set: (newVal) => {
// 這里val不能通過data[key]獲取寓辱,否則會(huì)陷入自調(diào)用死循環(huán)
if (newVal === val) return;
val = newVal;
// 如果newVal被賦值為對(duì)象,則繼續(xù)設(shè)置它下面的成員為響應(yīng)式數(shù)據(jù)
this.walk(newVal);
// 發(fā)送通知
dep.notify();
},
});
}
}
Compiler
- 負(fù)責(zé)編譯模板赤拒,解析指令/插值表達(dá)式
- 負(fù)責(zé)頁(yè)面的首次渲染
- 當(dāng)數(shù)據(jù)變化后重新渲染視圖
nodeType
常量 | 值 | 描述 |
---|---|---|
Node.ELEMENT_NODE | 1 | 一個(gè) 元素 節(jié)點(diǎn)秫筏,例如 <p> 和 <div>。 |
Node.TEXT_NODE | 3 | Element 或者 Attr 中實(shí)際的 文字 |
Node.CDATA_SECTION_NODE | 4 | 一個(gè) CDATASection挎挖,例如 <!CDATA[[ … ]]>这敬。 |
Node.PROCESSING_INSTRUCTION_NODE | 7 | 一個(gè)用于 XML 文檔的 ProcessingInstruction ,例如 <?xml-stylesheet ... ?> 聲明蕉朵。 |
Node.COMMENT_NODE | 8 | 一個(gè) Comment 節(jié)點(diǎn)崔涂。 |
Node.DOCUMENT_NODE | 9 | 一個(gè) Document 節(jié)點(diǎn)。 |
Node.DOCUMENT_TYPE_NODE | 10 | 描述文檔類型的 DocumentType 節(jié)點(diǎn)始衅。例如 <!DOCTYPE html> 就是用于 HTML5 的冷蚂。 |
Node.DOCUMENT_FRAGMENT_NODE | 11 | 一個(gè) DocumentFragment 節(jié)點(diǎn) |
已棄用的節(jié)點(diǎn)類型常量
常量 | 值 | 描述 |
---|---|---|
Node.ATTRIBUTE_NODE | 2 | 元素 的耦合屬性 。在 DOM4 規(guī)范里 Node 接口將不再實(shí)現(xiàn)這個(gè)元素屬性汛闸。 |
Node.ENTITY_REFERENCE_NODE | 5 | 一個(gè) XML 實(shí)體引用節(jié)點(diǎn)蝙茶。 在 DOM4 規(guī)范里被移除。 |
Node.ENTITY_NODE | 6 | 一個(gè) XML <!ENTITY ...> 節(jié)點(diǎn)诸老。 在 DOM4 規(guī)范中被移除隆夯。 |
Node.NOTATION_NODE | 12 | 一個(gè) XML <!NOTATION ...> 節(jié)點(diǎn)。 在 DOM4 規(guī)范里被移除. |
/**
* @author Wuner
* @date 2020/7/30 2:52
* @description
*/
// 負(fù)責(zé)解析指令/插值表達(dá)式
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.compiler(this.el);
}
// 編譯模板别伏,處理文本節(jié)點(diǎn)和元素節(jié)點(diǎn)
compiler(el) {
// el.childNodes是一個(gè)偽數(shù)組
const childNodes = Array.from(el.childNodes);
childNodes.forEach((node) => {
// console.dir(node);
if (this.isTextNode(node)) {
// 處理文本節(jié)點(diǎn)
this.compilerText(node);
} else if (this.isElementNode(node)) {
// 處理元素節(jié)點(diǎn)
this.compilerElement(node);
}
// 判斷當(dāng)前節(jié)點(diǎn)是否存在子節(jié)點(diǎn)蹄衷,并且子節(jié)點(diǎn)個(gè)數(shù)大于0,需遞歸調(diào)用compile
if (node.childNodes && node.childNodes.length) {
this.compiler(node);
}
});
}
// 編譯元素節(jié)點(diǎn)厘肮,處理指令
compilerElement(node) {
// console.dir(node);
// attributes是一個(gè)偽數(shù)組
// 遍歷元素節(jié)點(diǎn)中的所有屬性愧口,找到指令
Array.from(node.attributes).forEach((attr) => {
// 獲取元素屬性的名稱
// 判斷當(dāng)前的屬性名稱是否是指令
if (this.isDirective(attr.name)) {
this.updater(node, attr);
}
});
}
// 負(fù)責(zé)更新 DOM
// 創(chuàng)建 Watcher
updater(node, attr) {
// attrName 的形式 v-text v-model
// 截取屬性的名稱,獲取 text model
const attrName = attr.name.substr(2);
// 處理不同的指令
const fn = this[attrName + 'Updater'];
// 因?yàn)樵?textUpdater等中要使用 this
fn && fn.call(this, node, attr.value);
}
// 處理 v-text 指令
textUpdater(node, key) {
node.textContent = this.vm[key];
// 每一個(gè)指令中創(chuàng)建一個(gè) watcher轴脐,觀察數(shù)據(jù)的變化
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
// 處理 v-model 指令
modelUpdater(node, key) {
node.value = this.vm[key];
// 監(jiān)聽視圖的變化
node.addEventListener('input', () => (this.vm[key] = node.value));
// 每一個(gè)指令中創(chuàng)建一個(gè) watcher调卑,觀察數(shù)據(jù)的變化
new Watcher(this.vm, key, (newValue) => {
node.value = newValue;
});
}
// 編譯文本節(jié)點(diǎn)
compilerText(node) {
// console.dir(node);
let reg = /\{\{(.+?)\}\}/;
// 獲取文本節(jié)點(diǎn)的內(nèi)容
let textContent = node.textContent;
if (reg.test(textContent)) {
// 插值表達(dá)式中的值就是我們要的屬性名稱
let key = RegExp.$1.trim();
// 把插值表達(dá)式替換成具體的值
node.textContent = this.vm.$data[key];
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue;
});
}
}
// 判斷是否屬性是指令
isDirective(attrName) {
return attrName.startsWith('v-');
}
// 判斷是否是文本節(jié)點(diǎn)
isTextNode(node) {
return node.nodeType === 3;
}
// 判斷是否是元素節(jié)點(diǎn)
isElementNode(node) {
return node.nodeType === 1;
}
}
Dep(Dependency)
- 收集依賴抡砂,添加觀察者(watcher)
- 通知所有觀察者
/**
* @author Wuner
* @date 2020/7/30 22:00
* @description
*/
class Dep {
constructor() {
// 存儲(chǔ)觀察者的數(shù)組
this.subs = [];
}
// 添加觀察者
addSub(sub) {
// 判斷是否是觀察者
sub && sub.update && this.subs.push(sub);
}
// 通知所有觀察者
notify() {
this.subs.forEach((sub) => sub.update());
}
}
Watcher
- 當(dāng)數(shù)據(jù)變化觸發(fā)依賴, dep 通知所有的 Watcher 實(shí)例更新視圖
- 自身實(shí)例化的時(shí)候往 dep 對(duì)象中添加自己
/**
* @author Wuner
* @date 2020/7/30 22:17
* @description
*/
class Watcher {
constructor(vm, key, callback) {
this.vm = vm;
this.key = key;
// 當(dāng)數(shù)據(jù)變化的時(shí)候恬涧,調(diào)用 callback 更新視圖
this.callback = callback;
// 在 Dep 的靜態(tài)屬性上記錄當(dāng)前 watcher 對(duì)象注益,當(dāng)訪問數(shù)據(jù)的時(shí)候把 watcher 添加到 dep 的 subs 中
Dep.target = this;
// 這里通過vm取值時(shí),會(huì)調(diào)用到observer中的defineReactive中的get方法
this.oldValue = vm[key];
// 賦值后溯捆,將緩存清空丑搔,防止污染
Dep.target = null;
}
update() {
this.oldValue !== this.vm[this.key] && this.callback(this.vm[this.key]);
}
}
總結(jié)
問題
-
給屬性重新賦值成對(duì)象,是否是響應(yīng)式的提揍?
是響應(yīng)式
-
給 Vue 實(shí)例新增一個(gè)成員是否是響應(yīng)式的啤月?
不是響應(yīng)式的。原因
整體流程
- Vue
- 記錄傳入的選項(xiàng)劳跃,設(shè)置 el
- 把 data 的成員注入到 Vue 實(shí)例
- 負(fù)責(zé)調(diào)用 Observer 實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式處理(數(shù)據(jù)劫持)
- 負(fù)責(zé)調(diào)用 Compiler 編譯指令/插值表達(dá)式等
- Observer
- 數(shù)據(jù)劫持
- 負(fù)責(zé)把 data 中的成員轉(zhuǎn)換成 getter/setter
- 負(fù)責(zé)把多層屬性轉(zhuǎn)換成 getter/setter
- 如果給屬性賦值為新對(duì)象谎仲,把新對(duì)象的成員設(shè)置為 getter/setter
- 添加 Dep 和 Watcher 的依賴關(guān)系
- 數(shù)據(jù)變化發(fā)送通知
- Compiler
- 負(fù)責(zé)編譯模板,解析指令/插值表達(dá)式
- 負(fù)責(zé)頁(yè)面的首次渲染過程
- 當(dāng)數(shù)據(jù)變化后重新渲染
- Dep
- 收集依賴刨仑,添加訂閱者(watcher)
- 通知所有訂閱者
- Watcher
- 自身實(shí)例化的時(shí)候往 dep 對(duì)象中添加自己
- 當(dāng)數(shù)據(jù)變化 dep 通知所有的 Watcher 實(shí)例更新視圖