Vue實例簡單實現(xiàn)

簡單實現(xiàn)vue框架實例,實現(xiàn)的目的主要看下幾個知識點如何進行的:

  • Vue工作機制
  • Vue響應(yīng)式的原理
  • 依賴收集與追蹤
  • 編譯compile

以及一些相關(guān)操作,代碼如下

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>mvue-test-html</title>
</head>
<body>
  <div id="app">
    <p>{{name}}</p>
    <p m-text="name"></p>
    <p>{{age}}</p>

    <input type="text" m-model="name">
    <button @click="changeName">改變</button>
    <div m-html="html"></div>
  </div>

  <script src="./compile.js"></script>
  <script src="./mvue.js"></script>
  <script>
    const app = new MVue({
      el: '#app',
      data: {
        name: 'lily',
        age: 18,
        html: '<p>html-測試</p>'
      },
      created() {
        setTimeout(() => {
          this.age++
          this.name = '劉翔'
        }, 1000)
      },
      methods: {
        changeName() {
          console.log(8989898)
          this.name = 'lucy'
        }
      }
    })
  </script>

</body>
</html>

如上, 我們需要實現(xiàn)幾點:

  • 根組件初始化,el掛載
  • data實現(xiàn)數(shù)據(jù)雙向綁定,視圖層響應(yīng)更新, 如 this.name = '劉翔' 賦值后視圖層自動更新
  • created生命周期簡單實現(xiàn)
  • 指令v-text、表達式{{name}}、@click、v-model雙向數(shù)據(jù)綁定的實現(xiàn)
  • data、方法等掛載到this上,可以直接調(diào)用

這里分兩塊去處理這些東西,一部分是我們vue實例的處理,還一部分是編譯到html的處理。我這里寫了兩個文件,先實現(xiàn)vue實例,然后又寫了個compile的js文件。
MVue
這個里面首先包含一個vue實例,在constructor中我們做一些初始化的事情,然后執(zhí)行響應(yīng)式處理,將data中的值都做好攔截及監(jiān)聽處理,最后調(diào)用compile渲染出指定的el

observe 這個方法主要做響應(yīng)式處理,遍歷data中的所有鍵名一一調(diào)用defineReactive進行數(shù)據(jù)響應(yīng)式處理,然后代理到this實例上。

defineReactive 這個方法主要是給每個屬性的get、set定義攔截,做一些攔截處理。同時生成dep和key一一對應(yīng),對所有的依賴進行管理。

proxyData 顧名思義就是把data中的值代理到實例上面,方便this.name這樣去調(diào)用。

這里面還有一個Dep和Watcher兩個類,他們主要做依賴收集及管理,Dep里面會管理所有的watcher

// 定義KVue構(gòu)造函數(shù)
class MVue {
  constructor(options) {
    // 保存?zhèn)魅氲倪x項
    this.$options = options;

    // 傳入data
    this.$data = options.data;
    // 響應(yīng)式處理
    this.observe(this.$data);

    this.$methods = options.methods;


    new Compile(options.el, this)

    if (options.created) {
      options.created.call(this)
    }
  }

  // 響應(yīng)式處理
  observe(data) {
    if (!data || typeof data !== "object") {
      return;
    }

    // 遍歷data
    Object.keys(data).forEach(key => {
      // 響應(yīng)式處理
      this.defineReactive(data, key, data[key]);
      // 代理data中的屬性
      this.proxyData(key);
    });
  }

  defineReactive(data, key, val) {
    this.observe(val);

    // 定義一個Dep
    const dep = new Dep(); // 每個dep實例都與key值一一對應(yīng)
    // 給obj的每個key定義攔截
    Object.defineProperty(data, key, {
      get() {
        // 依賴收集
        Dep.target && dep.addDep(Dep.target);
        return val;
      },
      set(v) {
        if (v !== val) {
          val = v;
          dep.notify();
        }
      }
    });
  }

  // 講$data中的屬性代理到實例上
  proxyData(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key];
      },
      set(v) {
        this.$data[key] = v;
      }
    });
  }
}

// 創(chuàng)建dep:管理所有的watcher
class Dep {
  constructor() {
    // 存儲所有的依賴
    this.deps = [];
  }

  addDep(dep) {
    this.deps.push(dep);
  }

  // 通知更新
  notify() {
    this.deps.forEach(dep => dep.update());
  }
}

// 創(chuàng)建watcher: 保存data中的數(shù)值和頁面中的掛鉤關(guān)系
class Watcher {
  constructor(vm, key, cb) {
    // 創(chuàng)建實例時立刻將該實例指向Dep.target便于依賴收集
    this.vm = vm;
    this.cb = cb;
    this.key = key;

    // 觸發(fā)依賴收集
    Dep.target = this;
    this.vm[this.key]; // 觸發(fā)依賴收集
    Dep.target = null;
  }

  // 更新
  update() {
    console.log(this.key + "更新了");
    this.cb.call(this.vm, this.vm[this.key])
  }
}

compile 主要做一些指令等一系列操作的處理,包括實例中的el元素經(jīng)過處理后掛載到dom上等操作

這里主要使用了正則去匹配相應(yīng)的表達式、指令等,然后做出相關(guān)操作處理。具體看代碼操作即可。

// 遍歷dom,解析指令和插值表達式
class Compile {
  // el 待編譯的模板, vm-MVue實例
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);

    // 把模版中的內(nèi)容移到片段操作
    this.$fragment = this.node2Fragment(this.$el);
    // 執(zhí)行編譯
    this.compile(this.$fragment)
    // 放回至el中
    this.$el.appendChild(this.$fragment)

  }

  node2Fragment(el) {
    // 創(chuàng)建片段
    const fragment = document.createDocumentFragment();

    let child;
    while(child = el.firstChild) {
      fragment.appendChild(child)
    }
    return fragment;
  }

  compile(el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (node.nodeType == 1) {
        // 元素
        // console.log('編譯元素' + node.nodeName)
        this.compileEle(node)
      } else if (this.isInter(node)) {
        // 只關(guān)心{{XXX}}
        // console.log('編譯插值文本' + node.textContent)
        this.compileText(node)
      }
      // 遞歸子節(jié)點
      if (node.children && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  isInter(node) {
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }

  compileEle(node) {
    const nodeAttr = node.attributes;
    // 匹配 m-xxx
    Array.from(nodeAttr).forEach(attr => {
      // 規(guī)定 m-xxx="yyyy"
      const attrName = attr.name;
      const exp = attr.value;
      if (attrName.indexOf('m-') == 0) {
        // 指令
        const dir = attrName.substring(2);
        // 執(zhí)行
        this[dir] && this[dir](node, this.$vm, exp)
      } else if (attrName.indexOf('@') == 0) {
        // 事件
        const method = attrName.substring(1)
        this.addEvent(node, this.$vm, exp, method)
      }
    })
  }

  // 文本替換
  compileText(node) {
    // console.log(RegExp.$1);
    // console.log(this.$vm[RegExp.$1])
    // 表達式
    const exp = RegExp.$1
    this.update(node, this.$vm, exp, 'text')
  }

  update(node, vm, exp, type) {
    const updater = this[type + 'Updater']
    updater && updater(node, vm[exp])
    new Watcher(vm, exp, function(val) {
      updater && updater(node, val)
    })
  }

  textUpdater(node, val) {
    node.textContent = val
  }

  htmlUpdater(node, val) {
    node.innerHTML = val
  }

  modelUpdater(node, val) {
    node.value = val
  }

  text(node, vm, exp) {
    this.update(node, vm, exp, 'text')
  }

  html(node, vm, exp) {
    this.update(node, vm, exp, 'html')
  }

  model(node, vm, exp) {
    this.update(node, vm, exp, 'model')
    node.addEventListener('input', (e) => {
      vm[exp] = e.target.value
    })
  }

  addEvent(node, vm, exp, method) {
    const fn = vm.$options.methods && vm.$options.methods[exp]
    node.addEventListener(method, fn.bind(vm))
  }
}

image.png

這只是一個非常簡單的vue模仿,距離框架真正處理差了十萬八千里,不過里面的一些思路還是比較溫和的,僅供vue框架源碼初探,后面會具體分析vue的源碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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