只需六步,寫(xiě)一個(gè)屬于自己的 vue!

Vue 是國(guó)內(nèi)目前最火的前端框架,它功能強(qiáng)大而又上手簡(jiǎn)單,基本成為前端工程師們的標(biāo)配,但很多同學(xué)都只是停留在如何使用上,知其然不知所以然,對(duì)它內(nèi)部的實(shí)現(xiàn)原理一知半解,今天就帶領(lǐng)大家動(dòng)手寫(xiě)一個(gè)類Vue的迷你庫(kù),進(jìn)一步加深對(duì)Vue的理解,在前端進(jìn)階的道路上如虎添翼!

內(nèi)容摘要

  • MVVM 簡(jiǎn)介和流程分析
  • 核心入口-MyVue類的實(shí)現(xiàn)
  • 觀察者 - Watcher 類的實(shí)現(xiàn)
  • 發(fā)布訂閱 - Dep 類的實(shí)現(xiàn)
  • 數(shù)據(jù)劫持 - Observer 類的實(shí)現(xiàn)
  • 大功告成,運(yùn)行 MyVue

MVVM 簡(jiǎn)介和流程分析

作為前端最火的框架之一,Vue是MVVM設(shè)計(jì)模式實(shí)現(xiàn)的典型代表,什么是MVVM呢?MVVM是Model-View-ViewModel的簡(jiǎn)寫(xiě),M - 數(shù)據(jù)模型(Model),V - 視圖層(View),VM - 視圖模型(ViewModel),它本質(zhì)上就是MVC 的改進(jìn)版。

MVVM 就是將其中的View 的狀態(tài)和行為抽象化,讓我們將視圖 UI 和業(yè)務(wù)邏輯分開(kāi)。

mvvm 實(shí)現(xiàn)原理的可以用下圖簡(jiǎn)略表示·

mvvm.png

mvvm 實(shí)現(xiàn)流程分析

process.png

本案例我們通過(guò)實(shí)現(xiàn) vue 中的插值表達(dá)式解析和指令 v-model 功能來(lái)探究vue的基本運(yùn)行原理。

1. 核心入口-MyVue類的實(shí)現(xiàn)

實(shí)現(xiàn)數(shù)據(jù)代理

先了解些必備知識(shí),Object.defineProperty(obj, prop, descriptor)該方法可以定義或修改對(duì)象的屬性描述符

MyVue 的創(chuàng)建

class MyVue {
    constructor(option) {
        this.$el = document.querySelector(option.el);
        this.$data = option.data;

        if (this.$el) {
            // 1, 代理數(shù)據(jù)
            this.proxyData();

            // 2, 數(shù)據(jù)劫持
            // new Observer(this.$data);

            // 3, 編譯數(shù)據(jù)
            // new Compile(this);
        }
    }
    // 代理數(shù)據(jù),用于監(jiān)聽(tīng)對(duì) data 數(shù)據(jù)的訪問(wèn)和修改
    proxyData() {
        for (const key in this.$data) {
            Object.defineProperty(this, key, {
                enumerable: true, // 設(shè)為false后,該屬性無(wú)法被刪除。
                configurable: false, // 設(shè)為true后,該屬性可以被 for...in或Object.keys 枚舉到。
                get() {
                    return this.$data[key]
                },
                set(newVal) {
                    this.$data[key] = newVal
                }
            })
        }
    }
}

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

class Compile {
  constructor(vm) {
    // 要編譯的容器
    this.el = vm.$el

    // 掛載實(shí)例對(duì)象,方便其他實(shí)例方法訪問(wèn)
    this.vm = vm

    // 通過(guò)文檔片段來(lái)編譯模板
    // 1,createDocumentFragment()方法,是用來(lái)創(chuàng)建一個(gè)虛擬的節(jié)點(diǎn)對(duì)象
    // 2,DocumentFragment(以下簡(jiǎn)稱DF)節(jié)點(diǎn)不屬于文檔樹(shù),它有如下特點(diǎn):
    //  2-1 當(dāng)把該節(jié)點(diǎn)插入文檔樹(shù)時(shí),插入的不是該節(jié)點(diǎn)自身,而是它所有的子孫節(jié)點(diǎn)
    //  2-2 當(dāng)添加多個(gè)dom元素時(shí),如果先將這些元素添加到DF中,再統(tǒng)一將DF添加到頁(yè)面,會(huì)減少
    // 頁(yè)面的重排和重繪,進(jìn)而提升頁(yè)面渲染性能。
    //  2-3 使用 appendChild 方法將dom樹(shù)中的節(jié)點(diǎn)添加到 DF 中時(shí),會(huì)刪除原來(lái)的節(jié)點(diǎn)

    // 1,獲取文檔片段
    const fragment = this.nodeToFragment(this.el)

    // 2,編譯模板
    this.compile(fragment) 

    // 3,將編譯好的子元素重新追加到模板容器中
    this.el.appendChild(fragment)
    // console.log(fragment, fragment.nodeType, this.el.nodeType)
  }

  // dom元素轉(zhuǎn)為文檔片段
  nodeToFragment(element) {
    // 1,創(chuàng)建文檔片段
    const f = document.createDocumentFragment()

    // 2, 遷移子元素
    while(element.firstChild) {
      f.appendChild(element.firstChild)
    }
    
    // 3,返回文檔片段 
    return f
  }

  // 編譯方法
  compile(fragment) {
    // 1,獲取所有的子節(jié)點(diǎn)
    const childNodes = fragment.childNodes;

    // 2,遍歷子節(jié)點(diǎn)數(shù)組
    childNodes.forEach(node => {
      // 分別處理元素節(jié)點(diǎn)(nodeType: 1)和文檔節(jié)點(diǎn)(nodeType:3)
      const ntype = node.nodeType
      
      if (ntype === 1) {
        // 如果是元素節(jié)點(diǎn),解析指令
        this.compileElement(node)
      } else if (ntype === 3) {
        // 如果是文檔節(jié)點(diǎn),解析雙花括號(hào)
        this.compileText(node)
      }

      // 如果存在子節(jié)點(diǎn)則遞歸調(diào)用 compile
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  // 編譯元素節(jié)點(diǎn)
  compileElement(node){
    // 獲取元素的所有屬性
    const attrs = node.attributes;
    
    Array.from(attrs).forEach(atr => {
      const {name, value} = atr

      if (name.startsWith('v-')) {
        // 對(duì)指令做處理
        // name == v-model
        const [, b] = name.split('-')
        if (b === 'model') {
          node.value = this.vm[value]
        }
      }
    })
  }
  // 編譯文檔節(jié)點(diǎn)
  compileText(node) {
    const con = node.textContent;
    const reg = /\{\{(.+?)\}\}/g;

    if (reg.test(con)) {
      const value = con.replace(reg, (...args) => {
        // console.log(args)
        return this.vm[args[1]]
      })

      // 更新文檔節(jié)點(diǎn)內(nèi)容
      node.textContent = value
    }
  }
}

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

class Watcher {
  // 當(dāng)觀察者對(duì)應(yīng)的數(shù)據(jù)發(fā)生變化時(shí),使其可以更新視圖
  constructor(vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;

    // 保存舊值
    this.oldVal = this.getOldVal()
  }

  getOldVal() {
    Dep.target = this
    const oldVal = this.vm[this.key]
    Dep.target = null

    return oldVal
  }

  // 更新視圖
  update() {
    this.cb()
  }
}
// 接下來(lái)處理:1,誰(shuí)來(lái)通知觀察者去更新視圖;2,在什么時(shí)機(jī)更新視圖

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

// 收集依賴
class Dep {
  constructor() {
    // 初始化觀察者列表
    this.subs = []
  }

  // 收集觀察者
  addSub(watcher) {
    this.subs.push(watcher)
  }

  // 通知觀察者去更新視圖
  notify() {
    this.subs.forEach(w => w.update())
  }
}

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

class Observer {
  constructor(data) {
    this.observer(data)
  }

  observer(data) {
    // 實(shí)例化依賴收集器,專門收集所有的觀察者對(duì)象
    const dep = new Dep();
      
    for (const key in data) {
      let val = data[key]
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: false,
        get() {
          // 當(dāng)觀察者實(shí)例化的時(shí)候會(huì)訪問(wèn)對(duì)應(yīng)屬性,進(jìn)而觸發(fā)get函數(shù),然后添加訂閱者
          // 之后,數(shù)據(jù)的變化就會(huì)觸發(fā) set 函數(shù),而 set 函數(shù)觸發(fā)就會(huì)執(zhí)行 dep.notify()
          // 從而實(shí)現(xiàn),數(shù)據(jù)變化驅(qū)動(dòng)視圖更新。
          Dep.target && dep.addSub(Dep.target);
          return val
        },
        set(newVal) {
          val = newVal
          // 既然數(shù)據(jù)更新,視圖也應(yīng)該隨之更新
          dep.notify()
        }
      })
    }
  }
}

6. 大功告成,運(yùn)行 MyVue

對(duì)之前代碼進(jìn)行調(diào)整

  • 首先是在 MyVue 中
class MyVue {
    constructor(option) {
        this.$el = document.querySelector(option.el);
        this.$data = option.data;

        if (this.$el) {
            // 1, 代理數(shù)據(jù)
            this.proxyData();

            // 2, 數(shù)據(jù)劫持
-           // new Observer(this.$data);
+           new Observer(this.$data);

            // 3, 編譯數(shù)據(jù)
-           // new Compile(this);
+           new Compile(this);
        }
    }
}
  • 然后是在 Compile 中
class Compile {
  ...
  // 編譯元素節(jié)點(diǎn)
  compileElement(node){
    // 獲取元素的所有屬性
    const attrs = node.attributes;
    
    Array.from(attrs).forEach(atr => {
      const {name, value} = atr

      if (name.startsWith('v-')) {
        // 對(duì)指令做處理
        // name == v-model
        const [, b] = name.split('-')
        if (b === 'model') {
          // 將v-model 的綁定的數(shù)據(jù)解析到輸入框中
          node.value = this.vm[value]
                   
+         // 輸入框內(nèi)容變化,則修改數(shù)據(jù)(這一步是從視圖到數(shù)據(jù)的變化,需要手動(dòng)添加)
+         node.addEventListener('input', (e) => {
+          this.vm.$data[value] = e.target.value;
+         });

+         // 通過(guò)添加一個(gè)觀察者實(shí)例對(duì)象,當(dāng)數(shù)據(jù)發(fā)生任何變化則自動(dòng)更新視圖
+         new Watcher(this.vm, value, () => {
+           node.value = this.vm.$data[value];
+         });
        }
      }
    })
  }
  // 編譯文檔節(jié)點(diǎn)
  compileText(node) {
    const con = node.textContent;
    const reg = /\{\{(.+?)\}\}/g;

    if (reg.test(con)) {
      const value = con.replace(reg, (...args) => {
        // console.log(args)
+       // 通過(guò)添加一個(gè)觀察者實(shí)例對(duì)象,當(dāng)數(shù)據(jù)發(fā)生任何變化則自動(dòng)更新視圖
+       new Watcher(this.vm, args[1], () => {
+         node.textContent = con.replace(reg, (...args) => {
+           return this.vm[args[1]]
+         })
+       })
        return this.vm[args[1]]
      })

      // 更新文檔節(jié)點(diǎn)內(nèi)容
      node.textContent = value
    }
  }
}

引入前面創(chuàng)建的五個(gè)文件,就可以 new 一個(gè)自己的vue實(shí)例對(duì)象了,趕緊去試試吧!

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>MyVue</title>
</head>
<body>
  <div id="app">
    <!-- v-model 指令功能的實(shí)現(xiàn) -->
    <input type="text" v-model="msg">
    <div>
      <!-- 插值表達(dá)式 -->
      <p>{{msg}} --- {{info}}</p>
    </div>
  </div>
  <script src="js/Dep.js"></script>
  <script src="js/Observer.js"></script>
  <script src="js/Watcher.js"></script>
  <script src="js/Compile.js"></script>
  <script src="js/MyVue.js"></script>
  <script>
    var mv = new MyVue({
      el: "#app",
      data: {
        msg: "Hello",
        info: "World"
      }
    })
  </script>
</body>
</html>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容