演示效果

custom-vue.gif
一、問題:在 new Vue() 的時(shí)候發(fā)生了什么?vue雙向綁定是如何實(shí)現(xiàn)的?
回顧在vue中的用法:
new Vue({
el: '#app',
data: {
nickname: '雙流兒',
age: 18
}
})
二、分析
- 在vue內(nèi)部其實(shí)是使用的發(fā)布訂閱模式,其中observe方法設(shè)置需要觀察(監(jiān)聽)的數(shù)據(jù),compile方法遍歷dom節(jié)點(diǎn)(解析指令),拿到指令綁定的key,再根據(jù)key設(shè)置需要觀察的數(shù)據(jù)和訂閱管理器
- 在執(zhí)行new操作的時(shí)候傳入了el-需要掛載到的dom id,data-綁定的數(shù)據(jù)。
今天我們來實(shí)現(xiàn)一個(gè)v-model、v-text(包括{{ xxx }}) - dom結(jié)構(gòu)
<div id="app">
<h1>昵稱</h1>
<div v-text="nickname"></div>
<input type="text" v-model="nickname">
<br>
<h1>年齡</h1>
<div>{{ age }}</div>
<input type="text" v-model="age">
</div>
- vue2實(shí)例
new Vue({
el: '#app',
data: {
nickname: '雙流兒',
age: 18
}
});
三、定義一個(gè)Vue類
class Vue {
constructor({ el, data }) {
// 獲取dom
this.$el = document.querySelector(el);
// 監(jiān)聽(觀察)的數(shù)據(jù)
this.$data = data || {};
// 訂閱每個(gè)key(訂閱管理器)
this.$directives = {};
this.observe(this.$data);
this.compile(this.$el);
}
}
四、監(jiān)聽器observe
// 設(shè)置監(jiān)聽數(shù)據(jù)
observe(data) {
const _this = this;
for (const key in data) {
// 當(dāng)前每項(xiàng)的value
let value = data[key];
if (typeof value === 'object') this.observe(value);
Object.defineProperty(data, key, {
enumerable: true, // 設(shè)置屬性可枚舉
configurable: true, // 設(shè)置屬性可刪除
get() {
return value;
},
set(newValue) {
// 新的值與原來的值相等就不用執(zhí)行以下(更新)操作
if (newValue === value) return;
value = newValue;
// 監(jiān)聽到值改變后更新對應(yīng)指令的數(shù)據(jù)
_this.$directives[key].forEach(fn => {
fn.update();
});
}
});
}
}
五、解析器compile
// 設(shè)置指令(設(shè)置每個(gè)訂閱者)
setDirective(node, key, attr) {
const watcher = new Watcher({ node, key, attr, data: this.$data });
if (this.$directives[key]) this.$directives[key].push(watcher);
else this.$directives[key] = [watcher];
}
// 解析器-遍歷拿到dom上的指令(這里其實(shí)是把指令當(dāng)做自定義屬性來處理)
compile(dom) {
const _this = this;
const reg = /\{\{(.*)\}\}/; // 來匹配{{ xxx }}中的xxx
const ndoes = dom.childNodes; // 節(jié)點(diǎn)集
// ndoes是類數(shù)組對象不能使用es迭代器,需要轉(zhuǎn)成數(shù)據(jù)
Array.from(ndoes).forEach(node => {
// 如果node還有子項(xiàng),執(zhí)行遞歸
if (node.childNodes.length) _this.compile(node);
// 在本例中使用nodeType來判斷是什么類型,如nodeType為3時(shí)表示node的子節(jié)點(diǎn)有且僅有一個(gè)文本類型,也就是{{ xxx }}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
const key = RegExp.$1.trim(); // $1獲取reg匹配到的第一個(gè)值
// 聲明 {{ xxx }} 為text類型
_this.setDirective(node, key, 'nodeValue');
}
}
if (node.nodeType === 1) {
// v-text
if (node.hasAttribute('v-text')) {
const key = node.getAttribute('v-text'); // key就是實(shí)例化Vue是傳入的nickname/age
node.removeAttribute('v-text'); // 移除node上的自定義屬性
_this.setDirective(node, key, 'textContent');
}
// v-model 且node必須是input標(biāo)簽
if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
const key = node.getAttribute('v-model'); // key就是實(shí)例化Vue是傳入的nickname/age
node.removeAttribute('v-model'); // 移除node上的自定義屬性
_this.setDirective(node, key, 'value');
// 設(shè)置input事件監(jiān)聽
node.addEventListener('input', e => {
_this.$data[key] = e.target.value;
});
}
}
});
}
六、觀察者Watcher
class Watcher {
constructor({ node, key, attr, data }) {
this.node = node; // 指令對應(yīng)的DOM節(jié)點(diǎn)
this.key = key; // data的key
this.attr = attr; // 綁定的html原生屬性,本例v-text對應(yīng)textContent
this.data = data; // 監(jiān)聽的數(shù)據(jù)
this.update(); // 初始化更新數(shù)據(jù)
}
// 更新
update() {
this.node[this.attr] = this.data[this.key];
}
}
以上使用 Object.defineProperty 來實(shí)現(xiàn)數(shù)據(jù)劫持,那么怎么使用ES6的Proxy代理數(shù)據(jù)呢?
我們只需要修改 observe 方法
七、使用Proxy實(shí)現(xiàn)監(jiān)聽器
observe(data) {
const _this = this;
this.$data = new Proxy(data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
const status = Reflect.set(target, key, value);
if (status) {
// 當(dāng)status為true時(shí),表示數(shù)據(jù)已經(jīng)改變
_this.$directives[key].forEach(fn => {
fn.update();
});
}
return status;
}
});
}
八、Object.defineProperty vs Proxy
從上可以看出,在使用Object.defineProperty時(shí),需要遞歸遍歷data中的每個(gè)屬性,Proxy不需要,所以Proxy性能會(huì)優(yōu)于Object.defineProperty,這就是說vue3初始化比vue2性能更好的原因之一。
九、在vue3中實(shí)現(xiàn)數(shù)據(jù)雙向綁定
思路同上,這里是把Vue作為一個(gè)對象
class Watcher {
constructor({ node, key, attr, data }) {
this.node = node; // 指令對應(yīng)的DOM節(jié)點(diǎn)
this.key = key; // data的key
this.attr = attr; // 綁定的html原生屬性,本例v-text對應(yīng)textContent
this.data = data; // 監(jiān)聽的數(shù)據(jù)
this.update(); // 初始化更新數(shù)據(jù)
}
// 更新
update() {
this.node[this.attr] = this.data[this.key];
}
}
const Vue = {
$data: {},
$directives: {},
createApp({ data }) {
const _this = this;
this.$data = new Proxy(typeof data === 'function' ? data(): data, {
get(target, key) {
return target[key];
},
set(target, key, value) {
const status = Reflect.set(target, key, value);
if (status) {
// 當(dāng)status
_this.$directives[key].forEach(fn => {
fn.update();
});
}
return status;
}
});
return this;
},
mount(el) {
this.$el = document.querySelector(el);
this.compile(this.$el);
},
// 設(shè)置指令(設(shè)置每個(gè)訂閱者)
setDirective(node, key, attr) {
const watcher = new Watcher({ node, key, attr, data: this.$data });
if (this.$directives[key]) this.$directives[key].push(watcher);
else this.$directives[key] = [watcher];
},
compile(dom) {
const _this = this;
const reg = /\{\{(.*)\}\}/; // 來匹配{{ xxx }}中的xxx
const ndoes = dom.childNodes; // 節(jié)點(diǎn)集
// ndoes是類數(shù)組對象不能使用es迭代器,需要轉(zhuǎn)成數(shù)據(jù)
Array.from(ndoes).forEach(node => {
// 如果node還有子項(xiàng),執(zhí)行遞歸
if (node.childNodes.length) _this.compile(node);
// 在本例中使用nodeType來判斷是什么類型,如nodeType為3時(shí)表示node的子節(jié)點(diǎn)有且僅有一個(gè)文本類型,也就是{{ xxx }}
if (node.nodeType === 3) {
if (reg.test(node.nodeValue)) {
const key = RegExp.$1.trim(); // $1獲取reg匹配到的第一個(gè)值
// 聲明 {{ xxx }} 為text類型
_this.setDirective(node, key, 'nodeValue');
}
}
if (node.nodeType === 1) {
// v-text
if (node.hasAttribute('v-text')) {
const key = node.getAttribute('v-text'); // key就是實(shí)例化Vue是傳入的nickname/age
node.removeAttribute('v-text'); // 移除node上的自定義屬性
_this.setDirective(node, key, 'textContent');
}
// v-model 且node必須是input標(biāo)簽
if (node.hasAttribute('v-model') && node.tagName === 'INPUT') {
const key = node.getAttribute('v-model'); // key就是實(shí)例化Vue是傳入的nickname/age
node.removeAttribute('v-model'); // 移除node上的自定義屬性
_this.setDirective(node, key, 'value');
// 設(shè)置input事件監(jiān)聽
node.addEventListener('input', e => {
_this.$data[key] = e.target.value;
});
}
}
});
}
};
const obj = {
data() {
return {
nickname: '雙流兒',
age: 18
}
}
}
Vue.createApp(obj).mount('#app');
- vue3實(shí)例
const obj = {
data() {
return {
nickname: '雙流兒',
age: 18
}
}
}
Vue.createApp(obj).mount('#app');
總結(jié)
不管哪種思路都需要:
- 觀察者observe
- 解析器compile
- 監(jiān)聽器Watcher