日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 前端技术 > vue >内容正文

vue

Vue源码分析

發(fā)布時(shí)間:2024/1/23 vue 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Vue源码分析 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

    • Vue源碼分析
      • vue源碼
      • Object.defineProperty
      • MVVM
        • MVVM 類的實(shí)現(xiàn)
        • 模板編譯 Compile 類的實(shí)現(xiàn)
          • 1、解析根節(jié)點(diǎn)內(nèi)的 Dom 結(jié)構(gòu)
          • 2、編譯文檔碎片中的結(jié)構(gòu)
          • 3、CompileUtil 對(duì)象中指令方法的實(shí)現(xiàn)
        • 觀察者 Watcher 類的實(shí)現(xiàn)
        • 發(fā)布訂閱 Dep 類的實(shí)現(xiàn)
        • 數(shù)據(jù)劫持 Observer 類的實(shí)現(xiàn)
        • 驗(yàn)證 MVVM
      • 虛擬dom
        • 什么是dom?
      • DIFF算法

Vue源碼分析

vue源碼

  • Object.defineProperty
  • 數(shù)據(jù)劫持Observer
  • 依賴Dep
  • Watcher
  • xxxMinxin(混合)
  • proxy(代理)
  • defineReactive(響應(yīng)式)

Object.defineProperty

  • 動(dòng)態(tài)給對(duì)象添加屬性
  • 實(shí)現(xiàn)屬性的訪問(wèn)攔截
    訪問(wèn)屬性 obj.name 一定會(huì)調(diào)用get方法
    寫(xiě)入屬性 obj.name=xxx 一定會(huì)調(diào)用set方法
<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Vue源碼分析demo</title><!--為了更好地適配移動(dòng)設(shè)備,使用css的媒體查詢功能--><meta name="viewport" content="width=device-width,initial-scale=1" /> </head> <body><input type="text" id="input" /><div id="app"></div><script>var obj={//name:"jack"}var name="jack2";//動(dòng)態(tài)地給obj添加屬性name//第一個(gè)參數(shù)--對(duì)象 第二個(gè)參數(shù)--屬性名 第三個(gè)--get setObject.defineProperty(obj,'name',{get:function(){console.log("讀取屬性");return name;}set:function(v){console.log("寫(xiě)入屬性");name = v;document.querySelecter("#app").innerHTML = v;}})//類似雙向數(shù)據(jù)綁定document.querySelecter("#input").onkeyup = function(e){ obj.name = e.target.value;//document.querySelecter("#app").innerHTML = e.target.value; //寫(xiě)入屬性}document.querySelecter("#app").innerHTML = obj.name; //讀取屬性</script> </body> </html>

MVVM

MVVM 設(shè)計(jì)模式,是由 MVC(最早來(lái)源于后端)、MVP 等設(shè)計(jì)模式進(jìn)化而來(lái),M - 數(shù)據(jù)模型(Model),VM - 視圖模型(ViewModel),V - 視圖層(View)。

在 MVC 模式中,除了 Model 和 View 層以外,其他所有的邏輯都在 Controller 中,Controller 負(fù)責(zé)顯示頁(yè)面、響應(yīng)用戶操作、網(wǎng)絡(luò)請(qǐng)求及與 Model 的交互,隨著業(yè)務(wù)的增加和產(chǎn)品的迭代,Controller 中的處理邏輯越來(lái)越多、越來(lái)越復(fù)雜,難以維護(hù)。為了更好的管理代碼,為了更方便的擴(kuò)展業(yè)務(wù),必須要為 Controller “瘦身”,需要更清晰的將用戶界面(UI)開(kāi)發(fā)從應(yīng)用程序的業(yè)務(wù)邏輯與行為中分離,MVVM 為此而生。

new Vue({el: })

Vue是什么? — 函數(shù)

  • vue是不是一個(gè)mvvm框架?
  • model—數(shù)據(jù)模型 數(shù)據(jù) data
  • view—視圖–展示 {{xxx}}
  • viewmodel—數(shù)據(jù)驅(qū)動(dòng)

proxy

  • obj.name
  • vm–vue實(shí)例
  • data–定義的屬性
  • 把data中的屬性定義在vm實(shí)例上 vm._data.xxx

頁(yè)面渲染過(guò)程

  • 取data里的數(shù)據(jù)定義在vm實(shí)例上
  • 遍歷data屬性—對(duì)每一個(gè)屬性進(jìn)行監(jiān)聽(tīng)–Observer
  • 在Observer里使用defineReactive把屬性定義成響應(yīng)式 {{obj.name}}
    讀取屬性–進(jìn)入defineReactive定義的攔截器中
    更新屬性–進(jìn)入defineReactive定義的攔截器中
    defineReactive每定義一個(gè)屬性都有一個(gè)Dep 是不是閉包?
    name屬性里面也有一個(gè)Dep
  • 什么是Dep?
    用來(lái)保存Watcher(渲染W(wǎng)atcher),通知Watcher更新頁(yè)面
  • 當(dāng)data初始化完畢后
    name–dep,頁(yè)面渲染時(shí){{obj.name}}會(huì)執(zhí)行渲染W(wǎng)atcher,Dep.target是什么?
    執(zhí)行渲染W(wǎng)atcher—執(zhí)行g(shù)et()—putTarget(this)—Dep.target==渲染W(wǎng)atcher
    進(jìn)入defineReactive攔截器—添加依賴關(guān)系 屬性—dep—Watcher
  • 響應(yīng)式如何實(shí)現(xiàn)?
    修改obj.name=xxx,會(huì)進(jìn)入defineReactive的set方法—dep.notify()—通知所有Watcher更新–虛擬dom–diff–渲染頁(yè)面

在 Vue 的 MVVM 設(shè)計(jì)中,我們主要針對(duì) Compile(模板編譯)、Observer(數(shù)據(jù)劫持)、Watcher(數(shù)據(jù)監(jiān)聽(tīng))和 Dep(發(fā)布訂閱)幾個(gè)部分來(lái)實(shí)現(xiàn),核心邏輯流程可參照下圖:

MVVM 類的實(shí)現(xiàn)

在 Vue 中,對(duì)外只暴露了一個(gè)名為 Vue 的構(gòu)造函數(shù),在使用的時(shí)候 new 一個(gè) Vue 實(shí)例,然后傳入了一個(gè) options 參數(shù),類型為一個(gè)對(duì)象,包括當(dāng)前 Vue 實(shí)例的作用域 el、模板綁定的數(shù)據(jù) data 等等。

我們模擬這種 MVVM 模式的時(shí)候也構(gòu)建一個(gè)類,名字就叫 MVVM,在使用時(shí)同 Vue 框架類似,需要通過(guò) new 指令創(chuàng)建 MVVM 的實(shí)例并傳入 options。
文件:MVVM.js

class MVVM {constructor(options) {// 先把 el 和 data 掛在 MVVM 實(shí)例上this.$el = options.el;this.$data = options.data;if (this.$el) {// $data數(shù)據(jù)劫持new Observer(this.$data);// 將數(shù)據(jù)代理到實(shí)例上 vm.message = "hello"this.proxyData(this.$data);// 用數(shù)據(jù)和元素進(jìn)行編譯new Compile(this.$el, this);}}proxyData(data) { // 代理數(shù)據(jù)的方法Object.keys(data).forEach(key => {Object.defineProperty(this, key, {get() {return data[key];},set(newVal) {data[key] = newVal;}});});} }

通過(guò)上面代碼,我們可以看出,在我們 new 一個(gè) MVVM 的時(shí)候,在參數(shù) options 中傳入了一個(gè) Dom 的根元素節(jié)點(diǎn)和數(shù)據(jù) data 并掛在了當(dāng)前的 MVVM 實(shí)例上。

當(dāng)存在根節(jié)點(diǎn)的時(shí)候,通過(guò) Observer類 對(duì) data 數(shù)據(jù)進(jìn)行了劫持,并通過(guò) MVVM 實(shí)例的方法 proxyData 把 data 中的數(shù)據(jù)掛在當(dāng)前 MVVM 實(shí)例上,同樣對(duì)數(shù)據(jù)進(jìn)行了劫持,是因?yàn)槲覀冊(cè)讷@取和修改數(shù)據(jù)的時(shí)候可以直接通過(guò) this 或 this.$data,在 Vue 中實(shí)現(xiàn)數(shù)據(jù)劫持的核心方法是 Object.defineProperty,我們也使用這個(gè)方式通過(guò)添加 getter 和 setter 來(lái)實(shí)現(xiàn)數(shù)據(jù)劫持。

最后使用 Compile 類對(duì)模板和綁定的數(shù)據(jù)進(jìn)行了解析和編譯,并渲染在根節(jié)點(diǎn)上,之所以數(shù)據(jù)劫持和模板解析都使用類的方式實(shí)現(xiàn),是因?yàn)榇a方便維護(hù)和擴(kuò)展,其實(shí)不難看出,MVVM 類其實(shí)作為了 Compile 類和 Observer 類的一個(gè)橋梁。

模板編譯 Compile 類的實(shí)現(xiàn)

Compile 類在創(chuàng)建實(shí)例的時(shí)候需要傳入兩個(gè)參數(shù),第一個(gè)參數(shù)是當(dāng)前 MVVM 實(shí)例作用的根節(jié)點(diǎn),第二個(gè)參數(shù)就是 MVVM 實(shí)例,之所以傳入 MVVM 的實(shí)例是為了更方便的獲取 MVVM 實(shí)例上的屬性。

在 Compile 類中,我們會(huì)盡量的把一些公共的邏輯抽取出來(lái)進(jìn)行最大限度的復(fù)用,避免冗余代碼,提高維護(hù)性和擴(kuò)展性,我們把 Compile 類抽取出的實(shí)例方法主要分為兩大類,輔助方法和核心方法,在代碼中用注釋標(biāo)明。

1、解析根節(jié)點(diǎn)內(nèi)的 Dom 結(jié)構(gòu)

文件:Compile.js

class Compile {constructor(el, vm) {this.el = this.isElementNode(el) ? el : document.querySelector(el);this.vm = vm;// 如過(guò)傳入的根元素存在,才開(kāi)始編譯if (this.el) {// 1、把這些真實(shí)的 Dom 移動(dòng)到內(nèi)存中,即 fragment(文檔碎片)let fragment = this.node2fragment(this.el);}}/* 輔助方法 */// 判斷是否是元素節(jié)點(diǎn)isElementNode(node) {return node.nodeType === 1;}/* 核心方法 */// 將根節(jié)點(diǎn)轉(zhuǎn)移至文檔碎片node2fragment(el) {// 創(chuàng)建文檔碎片let fragment = document.createDocumentFragment();// 第一個(gè)子節(jié)點(diǎn)let firstChild;// 循環(huán)取出根節(jié)點(diǎn)中的節(jié)點(diǎn)并放入文檔碎片中while (firstChild = el.firstChild) {fragment.appendChild(firstChild);}return fragment;} }

上面編譯模板的過(guò)程中,前提條件是必須存在根元素節(jié)點(diǎn),傳入的根元素節(jié)點(diǎn)允許是一個(gè)真實(shí)的 Dom 元素,也可以是一個(gè)選擇器,所以我們創(chuàng)建了輔助方法 isElementNode 來(lái)幫我們判斷傳入的元素是否是 Dom,如果是就直接使用,是選擇器就獲取這個(gè) Dom,最終將這個(gè)根節(jié)點(diǎn)存入 this.el 屬性中。

解析模板的過(guò)程中為了性能,我們應(yīng)取出根節(jié)點(diǎn)內(nèi)的子節(jié)點(diǎn)存放在文檔碎片中(內(nèi)存),需要注意的是將一個(gè) Dom 節(jié)點(diǎn)內(nèi)的子節(jié)點(diǎn)存入文檔碎片的過(guò)程中,會(huì)在原來(lái)的 Dom 容器中刪除這個(gè)節(jié)點(diǎn),所以在遍歷根節(jié)點(diǎn)的子節(jié)點(diǎn)時(shí),永遠(yuǎn)是將第一個(gè)節(jié)點(diǎn)取出存入文檔碎片,直到節(jié)點(diǎn)不存在為止。

2、編譯文檔碎片中的結(jié)構(gòu)

在 Vue 中的模板編譯的主要就是兩部分,也是瀏覽器無(wú)法解析的部分,元素節(jié)點(diǎn)中的指令和文本節(jié)點(diǎn)中的 Mustache 語(yǔ)法(雙大括號(hào))。
文件:Compile.js —— 完善

class Compile {constructor(el, vm) {//domthis.el = this.isElementNode(el) ? el : document.querySelector(el);this.vm = vm;// 如過(guò)傳入的根元素存在,才開(kāi)始編譯if (this.el) {// 1、把這些真實(shí)的 Dom 移動(dòng)到內(nèi)存中,即 fragment(文檔碎片)let fragment = this.node2fragment(this.el);// ********** 以下為新增代碼 **********// 2、將模板中的指令中的變量和 {{}} 中的變量替換成真實(shí)的數(shù)據(jù)this.compile(fragment);// 3、把編譯好的 fragment 再塞回頁(yè)面中this.el.appendChild(fragment);// ********** 以上為新增代碼 **********}}/* 輔助方法 */// 判斷是否是元素節(jié)點(diǎn)isElementNode(_node) {return _node.nodeType === 1;}// ********** 以下為新增代碼 **********// 判斷屬性是否為指令isDirective(name) {return name.includes("v-");}// ********** 以上為新增代碼 **********/* 核心方法 */// 將根節(jié)點(diǎn)轉(zhuǎn)移至文檔碎片node2fragment(el) {// 創(chuàng)建文檔碎片let fragment = document.createDocumentFragment();// 第一個(gè)子節(jié)點(diǎn)let firstChild;// 循環(huán)取出根節(jié)點(diǎn)中的節(jié)點(diǎn)并放入文檔碎片中while (firstChild = el.firstChild) {fragment.appendChild(firstChild);}return fragment;}// ********** 以下為新增代碼 **********// 解析文檔碎片compile(fragment) {// 當(dāng)前父節(jié)點(diǎn)節(jié)點(diǎn)的子節(jié)點(diǎn),包含文本節(jié)點(diǎn),類數(shù)組對(duì)象let childNodes = fragment.childNodes;// 轉(zhuǎn)換成數(shù)組并循環(huán)判斷每一個(gè)節(jié)點(diǎn)的類型Array.from(childNodes).forEach(node => {if (this.isElementNode(node)) { // 是元素節(jié)點(diǎn)// 遞歸編譯子節(jié)點(diǎn)this.compile(node);// 編譯元素節(jié)點(diǎn)的方法this.compileElement(node);} else { // 是文本節(jié)點(diǎn)// 編譯文本節(jié)點(diǎn)的方法this.compileText(node);}});}// 編譯元素compileElement(node) {// 取出當(dāng)前節(jié)點(diǎn)的屬性,類數(shù)組let attrs = node.attributes;Array.from(attrs).forEach(attr => {// 獲取屬性名,判斷屬性是否為指令,即含 v-let attrName = attr.name;if (this.isDirective(attrName)) {// 如果是指令,取到該屬性值得變量在 data 中對(duì)應(yīng)得值,替換到節(jié)點(diǎn)中let exp = attr.value;// 取出方法名let [, type] = attrName.split("-");// 調(diào)用指令對(duì)應(yīng)得方法CompileUtil[type](node, this.vm, exp);}});}// 編譯文本compileText(node) {// 獲取文本節(jié)點(diǎn)的內(nèi)容let exp = node.textContent;// 創(chuàng)建匹配 {{}} 的正則表達(dá)式//let reg = /\{\{([^}+])\}\}/g;//“.”表示任意字符?!?#43;”表示前面表達(dá)式一次乃至多次。“?”表示匹配模式是非貪婪的。let reg = /\{\{(.+?)\}\}/g;// 如果存在 {{}} 則使用 text 指令的方法if (reg.test(exp)) {CompileUtil["text"](node, this.vm, exp);}}// ********** 以上為新增代碼 ********** }

上面代碼新增內(nèi)容得主要邏輯就是做了兩件事:

  • 調(diào)用 compile 方法對(duì) fragment 文檔碎片進(jìn)行編譯,即替換內(nèi)部指令和 Mustache 語(yǔ)法中變量對(duì)應(yīng)的值;
    將編譯好的 fragment 文檔碎片塞回根節(jié)點(diǎn)。
  • 在第一個(gè)步驟當(dāng)中邏輯是比較繁瑣的,首先在 compile 方法中獲取所有的子節(jié)點(diǎn),循環(huán)進(jìn)行編譯,如果是元素節(jié)點(diǎn)需要遞歸 compile,傳入當(dāng)前元素節(jié)點(diǎn)。在這個(gè)過(guò)程當(dāng)中抽取出了兩個(gè)方法,compileElement 和 compileText 用來(lái)對(duì)元素節(jié)點(diǎn)的屬性和文本節(jié)點(diǎn)進(jìn)行處理。

compileElement 中的核心邏輯就是處理指令,取出元素節(jié)點(diǎn)所有的屬性判斷是否是指令,是指令則調(diào)用指令對(duì)應(yīng)的方法。compileText 中的核心邏輯就是取出文本的內(nèi)容通過(guò)正則表達(dá)式匹配出被 Mustache 語(yǔ)法的 “{{ }}” 包裹的內(nèi)容,并調(diào)用處理文本的 text 方法。

文本節(jié)點(diǎn)的內(nèi)容有可能存在 “{{ }} {{ }} {{ }}”,正則匹配默認(rèn)是貪婪的,為了防止第一個(gè) “{” 和最后一個(gè) “}” 進(jìn)行匹配,所以在正則表達(dá)式中應(yīng)使用非貪婪匹配。

在調(diào)用指令的方法時(shí)都是調(diào)用的 CompileUtil 下對(duì)應(yīng)的方法,我們之所以單獨(dú)把這些指令對(duì)應(yīng)的方法抽離出來(lái)存儲(chǔ)在 CompileUtil 對(duì)象下的目的是為了解耦,因?yàn)楹竺嫫渌念愡€要使用。

3、CompileUtil 對(duì)象中指令方法的實(shí)現(xiàn)

CompileUtil 中存儲(chǔ)著所有的指令方法及指令對(duì)應(yīng)的更新方法,由于 Vue 的指令很多,我們這里只實(shí)現(xiàn)比較典型的 v-model 和 “{{ }}” 對(duì)應(yīng)的方法,考慮到后續(xù)更新的情況,我們統(tǒng)一把設(shè)置值到 Dom 中的邏輯抽取出對(duì)應(yīng)上面兩種情況的方法,存放到 CompileUtil 的 updater 對(duì)象中。
文件:CompileUtil.js

CompileUtil = {};// 更新Dom節(jié)點(diǎn)方法 CompileUtil.updater = {// 文本更新textUpdater(node, value) {node.textContent = value;},// 輸入框更新modelUpdater(node, value) {node.value = value;} };// 獲取 data 值的方法 CompileUtil.getVal = function (vm, exp) {// 將匹配的值用 . 分割開(kāi),如 vm.data.a.bexp = exp.split(".");// 歸并取值return exp.reduce((prev, next) => {return prev[next];}, vm.$data); };// 獲取文本 {{}} 中變量在 data 對(duì)應(yīng)的值 CompileUtil.getTextVal = function (vm, exp) {// 使用正則匹配出 {{ }} 間的變量名,再調(diào)用 getVal 獲取值return exp.replace(/\{\{([^}]+)\}\}/g, (...args) => {return this.getVal(vm, args[1]);}); };// 設(shè)置 data 值的方法 CompileUtil.setVal = function (vm, exp, newVal) {exp = exp.split(".");return exp.reduce((prev, next, currentIndex) => {// 如果當(dāng)前歸并的為數(shù)組的最后一項(xiàng),則將新值設(shè)置到該屬性if(currentIndex === exp.length - 1) {return prev[next] = newVal}// 繼續(xù)歸并return prev[next];}, vm.$data); }// 處理 v-model 指令的方法 CompileUtil.model = function (node, vm, exp) {// 獲取賦值的方法let updateFn = this.updater["modelUpdater"];// 獲取 data 中對(duì)應(yīng)的變量的值let value = this.getVal(vm, exp);// 添加觀察者,作用與 text 方法相同new Watcher(vm, exp, newValue => {updateFn && updateFn(node, newValue);});// v-model 雙向數(shù)據(jù)綁定,對(duì) input 添加事件監(jiān)聽(tīng)node.addEventListener('input', e => {// 獲取輸入的新值let newValue = e.target.value;// 更新到節(jié)點(diǎn)this.setVal(vm, exp, newValue);});// 第一次設(shè)置值updateFn && updateFn(node, value); };// 處理文本節(jié)點(diǎn) {{}} 的方法 CompileUtil.text = function (node, vm, exp) {// 獲取賦值的方法let updateFn = this.updater["textUpdater"];// 獲取 data 中對(duì)應(yīng)的變量的值let value = this.getTextVal(vm, exp);// 通過(guò)正則替換,將取到數(shù)據(jù)中的值替換掉 {{ }}exp.replace(/\{\{(.+?)\}\}/g, (...args) => {// 解析時(shí)遇到了模板中需要替換為數(shù)據(jù)值的變量時(shí),應(yīng)該添加一個(gè)觀察者// 當(dāng)變量重新賦值時(shí),調(diào)用更新值節(jié)點(diǎn)到 Dom 的方法new Watcher(vm, args[1], newValue => {// 如果數(shù)據(jù)發(fā)生變化,重新獲取新值updateFn && updateFn(node, newValue);});});// 第一次設(shè)置值updateFn && updateFn(node, value); };

這部分的整個(gè)思路就是在 Compile 編譯模板后處理 v-model 和 “{{ }}” 時(shí),其實(shí)都是用 data 中的數(shù)據(jù)替換掉 fragment 文檔碎片中對(duì)應(yīng)的節(jié)點(diǎn)中的變量。因此會(huì)經(jīng)常性的獲取 data 中的值,在更新節(jié)點(diǎn)時(shí)又會(huì)重新設(shè)置 data 中的值,所以我們抽離出了三個(gè)方法 getVal、getTextVal 和 setVal 掛在了 CompileUtil 對(duì)象下。

獲取和設(shè)置 data 的值兩個(gè)方法 getVal 和 setVal 思路相似,由于獲取的變量層級(jí)不定,可能是 data.a,也可能是 data.obj.a.b,所以都是使用歸并的思路,借用 reduce 方法實(shí)現(xiàn)的,區(qū)別在于 setVal 方法在歸并過(guò)程中需要判斷是不是歸并到最后一級(jí),如果是則設(shè)置新值,而 getTextVal 就是在 getVal 外包了一層處理 “{{ }}” 的邏輯。

在這些準(zhǔn)備工作就緒以后就可以實(shí)現(xiàn)我們的主邏輯,即對(duì) Compile 類中解析的文本節(jié)點(diǎn)和元素節(jié)點(diǎn)指令中的變量用 data 值進(jìn)行替換,還記得前面說(shuō)針對(duì) v-model 和 “{{ }}” 進(jìn)行處理,因此設(shè)計(jì)了 model 和 text 兩個(gè)核心方法。

model和text兩個(gè)方法邏輯相似,都獲取了各自的 updater 中的方法,對(duì)值進(jìn)行設(shè)置,并且在設(shè)置的同時(shí)為了后續(xù) data 中的數(shù)據(jù)修改,視圖的更新,創(chuàng)建了 Watcher 的實(shí)例,并在內(nèi)部用新值重新更新節(jié)點(diǎn),不同的是 Vue 的 v-model 指令在表單中實(shí)現(xiàn)了雙向數(shù)據(jù)綁定,只要表單元素的 value 值發(fā)生變化,就需要將新值更新到 data 中,并響應(yīng)到頁(yè)面上。

所以我們的實(shí)現(xiàn)方式是給這個(gè)綁定了 v-model 的表單元素監(jiān)聽(tīng)了 input 事件,并在事件中實(shí)時(shí)的將新的 value 值更新到 data 中,至于 data 中的改變后響應(yīng)到頁(yè)面中需要另外三個(gè)類 Watcher、Observer 和 Dep 共同實(shí)現(xiàn),我們下面就來(lái)實(shí)現(xiàn) Watcher 類。

觀察者 Watcher 類的實(shí)現(xiàn)

在 CompileUtil 對(duì)象的方法中創(chuàng)建 Watcher 實(shí)例的時(shí)候傳入了三個(gè)參數(shù),即 MVVM 的實(shí)例、模板綁定數(shù)據(jù)的變量名 exp 和一個(gè) callback,這個(gè) callback 內(nèi)部邏輯是為了更新數(shù)據(jù)到 Dom,所以我們的 Watcher 類內(nèi)部要做的事情就清晰了,獲取更改前的值存儲(chǔ)起來(lái),并創(chuàng)建一個(gè) update 實(shí)例方法,在值被更改時(shí)去執(zhí)行實(shí)例的 callback 以達(dá)到視圖的更新。
文件:Watcher.js

class Watcher {constructor(vm, exp, callback) {this.vm = vm;this.exp = exp;this.callback = callback;// 更改前的值this.value = this.get();}get() {// 將當(dāng)前的 watcher 添加到 Dep 類的靜態(tài)屬性上Dep.target = this;// 獲取值觸發(fā)數(shù)據(jù)劫持let value = CompileUtil.getVal(this.vm, this.exp);// 清空 Dep 上的 Watcher,防止重復(fù)添加Dep.target = null;return value;}update() {// 獲取新值let newValue = CompileUtil.getVal(this.vm, this.exp);// 獲取舊值let oldValue = this.value;// 如果新值和舊值不相等,就執(zhí)行 callback 對(duì) dom 進(jìn)行更新if(newValue !== oldValue) {this.callback(newValue);}} }

看到上面代碼一定有兩個(gè)疑問(wèn):

  • 使用 get 方法獲取舊值得時(shí)候?yàn)槭裁匆獙?dāng)前的實(shí)例掛在 Dep 上,在獲取值后為什么又清空了;
  • update 方法內(nèi)部執(zhí)行了 callback 函數(shù),但是 update 在什么時(shí)候執(zhí)行。
    這就是后面兩個(gè)類 Dep 和 observer 要做的事情,我們首先來(lái)介紹 Dep,再介紹 Observer 最后把他們之間的關(guān)系整個(gè)串聯(lián)起來(lái)。

發(fā)布訂閱 Dep 類的實(shí)現(xiàn)

其實(shí)發(fā)布訂閱說(shuō)白了就是把要執(zhí)行的函數(shù)統(tǒng)一存儲(chǔ)在一個(gè)數(shù)組中管理,當(dāng)達(dá)到某個(gè)執(zhí)行條件時(shí),循環(huán)這個(gè)數(shù)組并執(zhí)行每一個(gè)成員。
文件:Dep.js

class Dep {constructor() {this.subs = [];}// 添加訂閱addSub(watcher) {this.subs.push(watcher);}// 通知notify() {this.subs.forEach(watcher => watcher.update());} }

在 Dep 類中只有一個(gè)屬性,就是一個(gè)名為 subs 的數(shù)組,用來(lái)管理每一個(gè) watcher,即 Watcher 類的實(shí)例,而 addSub 就是用來(lái)將 watcher 添加到 subs 數(shù)組中的,我們看到 notify 方法就解決了上面的一個(gè)疑問(wèn),Watcher 類的 update 方法是怎么執(zhí)行的,就是這樣循環(huán)執(zhí)行的。
接下來(lái)我們整合一下盲點(diǎn):

  • Dep 實(shí)例在哪里創(chuàng)建聲明,又是在哪里將 watcher 添加進(jìn) subs 數(shù)組的;
  • Dep 的 notify 方法應(yīng)該在哪里調(diào)用;
  • Watcher 內(nèi)容中,使用 get 方法獲取舊值得時(shí)候?yàn)槭裁匆獙?dāng)前的實(shí)例掛在 Dep 上,在獲取值后為什么又清空了。
    這些問(wèn)題在最后一個(gè)類 Observer 實(shí)現(xiàn)的時(shí)候都將清晰,下面我們重點(diǎn)來(lái)看最后一部分核心邏輯。

數(shù)據(jù)劫持 Observer 類的實(shí)現(xiàn)

還記得實(shí)現(xiàn) MVVM 類的時(shí)候就創(chuàng)建了這個(gè)類的實(shí)例,當(dāng)時(shí)傳入的參數(shù)是 MVVM 實(shí)例的 data 屬性,在 MVVM 中把數(shù)據(jù)通過(guò) Object.defineProperty 掛到了實(shí)例上,并添加了 getter 和 setter,其實(shí) Observer 類主要目的就是給 data 內(nèi)的所有層級(jí)的數(shù)據(jù)都進(jìn)行這樣的操作。
文件:Observer.js

class Observer {constructor (data) {this.observe(data);}// 添加數(shù)據(jù)監(jiān)聽(tīng)observe(data) {if(!data || typeof data !== 'object') {return;}Object.keys(data).forEach(key => {// 劫持(實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式)this.defineReactive(data, key, data[key]);this.observe(data[key]); // 深度劫持});}// 數(shù)據(jù)響應(yīng)式defineReactive (object, key, value) {let _this = this;// 每個(gè)變化的數(shù)據(jù)都會(huì)對(duì)應(yīng)一個(gè)數(shù)組,這個(gè)數(shù)組是存放所有更新的操作let dep = new Dep();// 獲取某個(gè)值被監(jiān)聽(tīng)到Object.defineProperty(object, key, {enumerable: true,configurable: true,get () { // 當(dāng)取值時(shí)調(diào)用的方法Dep.target && dep.addSub(Dep.target);return value;},set (newValue) { // 當(dāng)給 data 屬性中設(shè)置的值適合,更改獲取的屬性的值if(newValue !== value) {_this.observe(newValue); // 重新賦值如果是對(duì)象進(jìn)行深度劫持value = newValue;dep.notify(); // 通知所有人數(shù)據(jù)更新了}}});} }

在上面的代碼中,observe 的目的是遍歷對(duì)象,在內(nèi)部對(duì)數(shù)據(jù)進(jìn)行劫持,即添加 getter 和 setter,我們把劫持的邏輯單獨(dú)抽取成 defineReactive 方法,需要注意的是 observe 方法在執(zhí)行最初就對(duì)當(dāng)前的數(shù)據(jù)進(jìn)行了數(shù)據(jù)類型驗(yàn)證,然后再循環(huán)對(duì)象每一個(gè)屬性進(jìn)行劫持,目的是給同為 Object 類型的子屬性遞歸調(diào)用 observe 進(jìn)行深度劫持。

在 defineReactive 方法中,創(chuàng)建了 Dep 的實(shí)例,并對(duì) data 的數(shù)據(jù)使用 get 和 set 進(jìn)行劫持,還記得在模板編譯的過(guò)程中,遇到模板中綁定的變量,就會(huì)解析,并創(chuàng)建 watcher,會(huì)在 Watcher 類的內(nèi)部獲取舊值,即當(dāng)前的值,這樣就觸發(fā)了 get,在 get 中就可以將這個(gè) watcher 添加到 Dep 的 subs 數(shù)組中進(jìn)行統(tǒng)一管理,因?yàn)樵诖a中獲取 data 中的值操作比較多,會(huì)經(jīng)常觸發(fā) get,我們又要保證 watcher 不會(huì)被重復(fù)添加,所以在 Watcher 類中,獲取舊值并保存后,立即將 Dep.target 賦值為 null,并且在觸發(fā) get 時(shí)對(duì) Dep.target 進(jìn)行了短路操作,存在才調(diào)用 Dep 的 addSub 進(jìn)行添加。

而 data 中的值被更改時(shí),會(huì)觸發(fā) set,在 set 中做了性能優(yōu)化,即判斷重新賦的值與舊值是否相等,如果相等就不重新渲染頁(yè)面,不等的情況有兩種,如果原來(lái)這個(gè)被改變的值是基本數(shù)據(jù)類型沒(méi)什么影響,如果是引用類型,我們需要對(duì)這個(gè)引用類型內(nèi)部的數(shù)據(jù)進(jìn)行劫持,因此遞歸調(diào)用了 observe,最后調(diào)用 Dep 的 notify 方法進(jìn)行通知,執(zhí)行 notify 就會(huì)執(zhí)行 subs 中所有被管理的 watcher 的 update,就會(huì)執(zhí)行創(chuàng)建 watcher 時(shí)的傳入的 callback,就會(huì)更新頁(yè)面。

在 MVVM 類將 data 的屬性掛在 MVVM 實(shí)例上并劫持與通過(guò) Observer 類對(duì) data 的劫持還有一層聯(lián)系,因?yàn)檎麄€(gè)發(fā)布訂閱的邏輯都是在 data 的 get 和 set 上,只要觸發(fā)了 MVVM 中的 get 和 set 內(nèi)部會(huì)自動(dòng)返回或設(shè)置 data 對(duì)應(yīng)的值,就會(huì)觸發(fā) data 的 get 和 set,就會(huì)執(zhí)行發(fā)布訂閱的邏輯。

通過(guò)上面長(zhǎng)篇大論的敘述后,這個(gè) MVVM 模式用到的幾個(gè)類的關(guān)系應(yīng)該完全敘述清晰了,雖然比較抽象,但是細(xì)心琢磨還是會(huì)明白之間的關(guān)系和邏輯,下面我們就來(lái)對(duì)我們自己實(shí)現(xiàn)的這個(gè) MVVM 進(jìn)行驗(yàn)證。

驗(yàn)證 MVVM

按照 Vue 的方式根據(jù)自己的 MVVM 實(shí)現(xiàn)的內(nèi)容簡(jiǎn)單的寫(xiě)了一個(gè)模板如下:

<!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>MVVM</title> </head> <body><div id="app"><!-- 雙向數(shù)據(jù)綁定 靠的是表單 --><input type="text" v-model="message"><div>{{message}}</div><ul><li>{{message}}</li></ul>{{message}}</div><!-- 引入依賴的 js 文件 --><script src="./js/Watcher.js"></script><script src="./js/Observer.js"></script><script src="./js/Compile.js"></script><script src="./js/CompileUtil.js"></script><script src="./js/Dep.js"></script><script src="./js/MVVM.js"></script><script>let vm = new MVVM({el: '#app',data: {message: 'hello world!'}});</script> </body> </html>

打開(kāi) Chrom 瀏覽器的控制臺(tái),在上面通過(guò)下面操作來(lái)驗(yàn)證:

  • 輸入 vm.message = “hello” 看頁(yè)面是否更新;
  • 輸入 vm.$data.message = “hello” 看頁(yè)面是否更新;
  • 改變文本輸入框內(nèi)的值,看頁(yè)面的其他元素是否更新。

通過(guò)上面的測(cè)試,相信應(yīng)該理解了 MVVM 模式對(duì)于前端開(kāi)發(fā)重大的意義,實(shí)現(xiàn)了雙向數(shù)據(jù)綁定,實(shí)時(shí)保證 View 層與 Model 層的數(shù)據(jù)同步,并可以讓我們?cè)陂_(kāi)發(fā)時(shí)基于數(shù)據(jù)編程,而最少的操作 Dom,這樣大大提高了頁(yè)面渲染的性能,也可以使我們把更多的精力用于業(yè)務(wù)邏輯的開(kāi)發(fā)上。

參考文章:https://www.jianshu.com/p/b87e6b689a3a感謝作者的分享!

源碼鏈接:https://gitee.com/monkeyhlj/vue-learning/tree/master/20210812-%E6%89%8B%E5%86%99%E4%B8%80%E4%B8%AAMVVM

虛擬dom

什么是dom?

  • <div>xxx</div>
    操作dom工具,jq,原生js
    頻繁操作dom性能很差 – 數(shù)據(jù)量大時(shí)
    改善:先記著dom更新,定時(shí)批量修改,通過(guò)算法判斷哪些需要修改
  • 虛擬dom (js對(duì)象)
var node={tag:"div:,//text:"xxx",children:[{tag:"span",//text:"yyy",children:[{tag:'#text',text:"yyy"}]}] }<div><span>yyy</span></div> <!DOCTYPE html> <html lang="en"> <head><meta charset="UTF-8"><title>Vue源碼分析demo</title><!--為了更好地適配移動(dòng)設(shè)備,使用css的媒體查詢功能--><meta name="viewport" content="width=device-width,initial-scale=1" /> </head> <body><div id="app"></div><script>function VNode(tag,child,text){this.tag=tag;this.child=child;this.text=text;}//將虛擬dom轉(zhuǎn)真實(shí)domVNode.prototype.render = function(){if(this.tag === '#text'){ //文本return document.createTextNode(this.text);}//元素let el = document.createElement(this.tag); //<div><span></span></div>this.child.forEach(vnode => {el.appendChild(vnode.render()) //添加生成的dom 遞歸})return el;}//創(chuàng)建虛擬domfunction Element(tag,child,text){if (typeof child === 'string'){text = child;child = [];}return new VNode(tag,child,text);}//根據(jù)虛擬dom生成真實(shí)domlet vnodes = Element('div',[Element('#text','aaa')]);let res = vnodes.render();console.log(res);document.querySelector("#app").innerHTML = res;</script> </body> </html>

DIFF算法

  • 將虛擬dom映射到真實(shí)dom
  • 同級(jí)比較
  • 有2對(duì)指針,舊首,舊尾,新首,新尾(見(jiàn)下下圖)
    1、從左到右比較
    將舊首的元素與新首的元素進(jìn)行比較,若相同則向右移動(dòng)一位
    若新元素有多的則直接添加到舊元素上
    2、從右到左比較
    3、從舊首與新尾節(jié)點(diǎn)比較
    4、從新首與舊尾節(jié)點(diǎn)比較
    5、從舊首—舊尾區(qū)間查找有沒(méi)有相同的節(jié)點(diǎn),有則復(fù)用,無(wú)則創(chuàng)建


創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來(lái)咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)

總結(jié)

以上是生活随笔為你收集整理的Vue源码分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。