vue mvvm數(shù)據(jù)響應(yīng)實現(xiàn)
為什么實現(xiàn)數(shù)據(jù)響應(yīng)式
當(dāng)前vue、react等框架流行。無論是vue、還是react框架大家最初的設(shè)計思路都是類似的。都是以數(shù)據(jù)驅(qū)動視圖,數(shù)據(jù)優(yōu)先。希望能夠通過框架減少開發(fā)人員直接操作節(jié)點,讓開發(fā)人員能夠把更多的精力放在業(yè)務(wù)上而不是過多的放在操作節(jié)點上。另一方面,框架會通過虛擬dom及diff算法提高頁面性能。這其中需要數(shù)據(jù)優(yōu)先最根本的思路就是實現(xiàn)數(shù)據(jù)響應(yīng)式。so,本次來看下如何基于原生實現(xiàn)數(shù)據(jù)響應(yīng)式。
vue中的數(shù)據(jù)響應(yīng)
vue中會根據(jù)數(shù)據(jù)將數(shù)據(jù)通過大胡子語法及指令渲染到視圖上,這里我們以大胡子語法為例。如下:
<div id="app">
{{message}}
</div>
let vm = new Vue({
el:"#app",
data:{
message:"測試數(shù)據(jù)"
}
})
setTimeout(()=>{
vm.message = "修改的數(shù)據(jù)";
},1000)
如上代碼,很簡單 。vue做了兩件事情。一、把message數(shù)據(jù)初次渲染到視圖。二、當(dāng)message數(shù)據(jù)改變的時候視圖上渲染的message數(shù)據(jù)同時也會做出響應(yīng)。以最簡單的案例。帶著問題來看,通過原生js如何實現(xiàn)??這里為了簡化操作便于理解,這里就不去使用虛擬dom。直接操作dom結(jié)構(gòu)。
實現(xiàn)數(shù)據(jù)初次渲染
根據(jù)vue調(diào)用方式。定義Vue類來實現(xiàn)各種功能。將初次渲染過程定義成編譯compile函數(shù)渲染視圖。通過傳入的配置以及操作dom來實現(xiàn)渲染。大概思路是通過正則查找html 里 #app 作用域內(nèi)的表達(dá)式,然后查找數(shù)據(jù)做對應(yīng)的替換即可。具體實現(xiàn)如下:
class Vue {
constructor(options) {
this.opts = options;
this.compile();
}
compile() {
let ele = document.querySelector(this.opts.el);
// 獲取所有子節(jié)點
let childNodes = ele.childNodes;
childNodes.forEach(node => {
if (node.nodeType === 3) {
// 找到所有的文本節(jié)點
let nodeContent = node.textContent;
// 匹配“{{}}”
let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
if (reg.test(nodeContent)) {
let $1 = RegExp.$1;
// 查找數(shù)據(jù)替換 “{{}}”
node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
}
}
})
}
}
如上完成了初次渲染,將message數(shù)據(jù)渲染到了視圖上。但是會返現(xiàn)并沒對深層次的dom結(jié)構(gòu)做處理也就是如下情況:
<div id="app">
1{{ message }}2
<div>
hello , {{ message }}
</div>
</div>

渲染結(jié)果如上
發(fā)現(xiàn)結(jié)果并沒有達(dá)到預(yù)期。so,需要改下代碼,讓節(jié)點可以深層次查找就可以了。代碼如下:
compile() {
let ele = document.querySelector(this.opts.el);
this.compileNodes(ele);
}
compileNodes(ele) {
// 獲取所有子節(jié)點
let childNodes = ele.childNodes;
childNodes.forEach(node => {
if (node.nodeType === 3) {
// 找到所有的文本節(jié)點
let nodeContent = node.textContent;
// 匹配“{{}}”
let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
if (reg.test(nodeContent)) {
let $1 = RegExp.$1;
// 查找數(shù)據(jù)替換 “{{}}”
node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
}
} else if (node.nodeType === 1) {
if (node.childNodes.length > 0) {
this.compileNodes(node);
}
}
})
}
上述代碼通過遞歸查找節(jié)點 實現(xiàn)深層次節(jié)點的渲染工作。如此,就實現(xiàn)了視圖的初次渲染。
數(shù)據(jù)劫持
回過頭來看下上面說的第二個問題:當(dāng)message數(shù)據(jù)改變的時候視圖上渲染的message數(shù)據(jù)同時也會做出響應(yīng)。如何實現(xiàn)數(shù)據(jù)響應(yīng)式?簡而言之就是數(shù)據(jù)變動影響視圖變動?再將問題拆分下 1. 如何知道數(shù)據(jù)變動了? 2.如何根據(jù)數(shù)據(jù)變動來更改視圖?
- 如何知道數(shù)據(jù)變動了? 這里就需要用到數(shù)據(jù)攔截了,或者叫數(shù)據(jù)觀察。把會變動的data數(shù)據(jù)觀察起來。當(dāng)他變動的時候我們可以做后續(xù)的渲染事情。如何攔截數(shù)據(jù)呢 ?vue2里采取的是definePrototype。
let obj = {
myname:"張三"
}
Object.defineProperty(obj,'myname',{
configurable:true,
enumerable:true,
get(){
console.log("get.")
return "張三";
},
set(newValue){
console.log("set")
console.log(newValue);
}
})
console.log(obj);
上述代碼會發(fā)現(xiàn),通過defineProperty劫持的對象屬性下都會有g(shù)et及set方法。那么當(dāng)我們獲取或者設(shè)置數(shù)據(jù)的時候就能出發(fā)對應(yīng)的get及set 。這樣就能攔截數(shù)據(jù)做后續(xù)操作。

還有沒有其他方式達(dá)到數(shù)據(jù)劫持的效果呢?ES6中出現(xiàn)了Proxy 代理對象同樣也可以達(dá)到類似劫持?jǐn)?shù)據(jù)的功能。如下代碼:
let obj = {
myname:"張三"
}
let newObj = new Proxy(obj,{
get(target,key){
console.log("get...")
return "張三"
},
set(target,name,newValue){
console.log("set...");
return Reflect.set(target,name,newValue);
}
})
兩種方式都可以實現(xiàn)數(shù)據(jù)劫持。proxy功能更加強(qiáng)大,很多方法是defineProperty所不具備的。且proxy直接攔截的是對象而defineProperty攔截的是對象屬性。so,可以利用上述方式將data數(shù)據(jù)做劫持,代碼如下:
observe(data){
let keys = Object.keys(data);
keys.forEach(key=>{
let value = data[key];
Object.defineProperty(data,key,{
configurable:true,
enumerable:true,
get(){
return value;
},
set(newValue){
value = newValue;
}
});
})
}
觀察者模式實現(xiàn)數(shù)據(jù)響應(yīng)
有了劫持?jǐn)?shù)據(jù)方式后,接下來需要實現(xiàn)的就是當(dāng)修改數(shù)據(jù)的時候?qū)⑿聰?shù)據(jù)渲染到視圖。如何辦到呢?會發(fā)現(xiàn),需要在data設(shè)置的時候觸發(fā)視圖的compile編譯。二者之間互相影響,此時可以想到利用觀察者模式,通過觀察者模式讓二者產(chǎn)生關(guān)聯(lián),如下:

圖略小,代碼也貼上吧。
class Vue extends EventTarget {
constructor(options) {
super();
this.opts = options;
this.observe(this.opts.data);
this.compile();
}
observe(data){
let keys = Object.keys(data);
let _this = this;
keys.forEach(key=>{
let value = data[key];
Object.defineProperty(data,key,{
configurable:true,
enumerable:true,
get(){
return value;
},
set(newValue){
_this.dispatchEvent(new CustomEvent(key,{
detail:newValue
}));
value = newValue;
}
});
})
}
compile() {
let ele = document.querySelector(this.opts.el);
this.compileNodes(ele);
}
compileNodes(ele) {
// 獲取所有子節(jié)點
let childNodes = ele.childNodes;
childNodes.forEach(node => {
if (node.nodeType === 3) {
// 找到所有的文本節(jié)點
let nodeContent = node.textContent;
// 匹配“{{}}”
let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
if (reg.test(nodeContent)) {
let $1 = RegExp.$1;
// 查找數(shù)據(jù)替換 “{{}}”
node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
this.addEventListener($1,e=>{
let oldValue = this.opts.data[$1];
let newValue = e.detail;
let reg = new RegExp(oldValue);
node.textContent = node.textContent.replace(reg,newValue);
})
}
} else if (node.nodeType === 1) {
if (node.childNodes.length > 0) {
this.compileNodes(node);
}
}
})
}
}
如上,成功的通過觀察者模式實現(xiàn)了數(shù)據(jù)的響應(yīng)。但是會發(fā)現(xiàn)data與compile之間需要通過鍵名來進(jìn)行關(guān)聯(lián)。如果data數(shù)據(jù)結(jié)構(gòu)嵌套關(guān)系復(fù)雜后面會比較難處理。有沒有一種方式讓二者松解耦呢?這時候可以用發(fā)布訂閱模式來進(jìn)行改造。
發(fā)布訂閱模式改造響應(yīng)式

還是略小,也還是貼上代碼:
class Vue {
constructor(options) {
this.opts = options;
this.observe(this.opts.data);
this.compile();
}
observe(data){
let keys = Object.keys(data);
let _this = this;
keys.forEach(key=>{
let value = data[key];
let dep = new Dep();
Object.defineProperty(data,key,{
configurable:true,
enumerable:true,
get(){
if(Dep.target){
dep.addSub(Dep.target);
}
return value;
},
set(newValue){
dep.notify(newValue);
value = newValue;
}
});
})
}
compile() {
let ele = document.querySelector(this.opts.el);
this.compileNodes(ele);
}
compileNodes(ele) {
// 獲取所有子節(jié)點
let childNodes = ele.childNodes;
childNodes.forEach(node => {
if (node.nodeType === 3) {
// 找到所有的文本節(jié)點
let nodeContent = node.textContent;
// 匹配“{{}}”
let reg = /\{\{\s*([^\{\}\s]+)\s*\}\}/g;
if (reg.test(nodeContent)) {
let $1 = RegExp.$1;
// 查找數(shù)據(jù)替換 “{{}}”
node.textContent = node.textContent.replace(reg, this.opts.data[$1]);
new Watcher(this.opts.data,$1,(newValue)=>{
let oldValue = this.opts.data[$1];
let reg = new RegExp(oldValue);
node.textContent = node.textContent.replace(reg,newValue);
})
}
} else if (node.nodeType === 1) {
if (node.childNodes.length > 0) {
this.compileNodes(node);
}
}
})
}
}
class Dep{
constructor(){
this.subs = [];
}
addSub(sub){
this.subs.push(sub);
}
notify(newValue){
this.subs.forEach(sub=>{
sub.update(newValue);
})
}
}
class Watcher{
constructor(data,key,cb){
Dep.target = this;
data[key];
this.cb = cb;
Dep.target = null;
}
update(newValue){
this.cb(newValue);
}
}
如上代碼思路是 針對每個數(shù)據(jù)會生成一個dep(依賴收集器)在數(shù)據(jù)get的時候收集watcher,將watcher 添加到dep里保存。數(shù)據(jù)一旦有改變觸發(fā)notify發(fā)布消息從而影響compile編譯更新視圖。這個流程也可以參看下圖:

如上就完成了視圖響應(yīng)。通過上述代碼,我們可以看出實現(xiàn)數(shù)據(jù)響應(yīng)兩個核心點1.數(shù)據(jù)劫持。2.觀察者和發(fā)布訂閱。在這我們可以思考一個問題,2個設(shè)計模式都是可以實現(xiàn)的但是有什么區(qū)別呢?
觀察者與發(fā)布訂閱
這里需要從概念來看
- 觀察者模式:定義一個對象與其他對象之間的一種依賴關(guān)系,當(dāng)對象發(fā)生某種變化的時候,依賴它的其它對象都會得到更新,一對多的關(guān)系。
- 發(fā)布訂閱模式:是一種消息范式,消息的發(fā)送者(稱為發(fā)布者)不會將消息直接發(fā)送給特定的接收者(稱為訂閱者)。而是將發(fā)布的消息分為不同的類別,無需了解哪些訂閱者(如果有的話)可能存在。同樣的,訂閱者可以表達(dá)對一個或多個類別的興趣,只接收感興趣的消息,無需了解哪些發(fā)布者(如果有的話)存在。

兩者之間關(guān)系,發(fā)布訂閱是三者之間關(guān)系。發(fā)布訂閱會多了一個關(guān)系器來組織主題和觀察者之間的關(guān)系。這樣做的好處就是松解耦??瓷厦骓憫?yīng)式例子可以看出觀察者需要通過事件名稱來進(jìn)行關(guān)聯(lián)。發(fā)布訂閱定義dep管理器之后data和compile徹底解耦,讓二者松散解耦。在處理多層數(shù)據(jù)結(jié)構(gòu)上發(fā)布訂閱會更清晰。松解耦能夠應(yīng)對更多變化,把模塊之間依賴降到最低。發(fā)布訂閱廣義上是觀察者模式。
好了 暫時先over 。 如果覺得有收獲的話可以點個贊,贈人玫瑰,手有余香?。。。?/p>
以上就是vue mvvm數(shù)據(jù)響應(yīng)實現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于vue mvvm數(shù)據(jù)響應(yīng)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue3.3?+?TS4構(gòu)建實現(xiàn)ElementPlus功能的組件庫示例
Vue.js?是目前最盛行的前端框架之一,而?TypeScript?則是一種靜態(tài)類型言語,它能夠讓開發(fā)人員在編寫代碼時愈加平安和高效,本文將引見如何運(yùn)用?Vue.js?3.3?和?TypeScript?4?構(gòu)建一個自主打造媲美?ElementPlus?的組件庫2023-10-10
vue-print-nb解決vue打印問題,并且隱藏頁眉頁腳方式
這篇文章主要介紹了vue-print-nb解決vue打印問題,并且隱藏頁眉頁腳方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05
vue 地區(qū)選擇器v-distpicker的常用功能
這篇文章主要介紹了vue 地區(qū)選擇器v-distpicker的常用功能,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-07-07
Vue數(shù)據(jù)變了但頁面沒有變的幾種情況及解決方法
如果,你發(fā)現(xiàn)自己需要在Vue中做一次強(qiáng)制更新,99.99%的情況,是你在某個地方做錯了事,本文給大家就介紹了Vue數(shù)據(jù)變了,但頁面沒有變的幾種情況及解決方法,并通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-08-08
webpack+vue-cli項目中引入外部非模塊格式j(luò)s的方法
今天小編就為大家分享一篇webpack+vue-cli項目中引入外部非模塊格式j(luò)s的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09

