首頁 > 軟體

從零實現一個vue檔案解析器

2022-06-24 18:00:03

如何從 0 處理一個 vue 檔案並實現簡單的響應式?

在現在的前端工程化中,打包工具是不可或缺的,其中webpack無疑是佔據了主導地位,當然也有尤大搞的vite,但是論生態和使用人數,至少在目前webpack還是更勝一籌。

打包工具能幫助我們打包前端檔案,在webpack中,不同字尾的檔案通過不同loader來處理。

本文就討論下怎麼實現一個處理.vue檔案的loader,以及用loader處理完.vue檔案怎麼把內容渲染在瀏覽器上,並實現簡單的響應式。

原始碼地址 gezhicui/vue-webpack

webpack 部分

首先進行 webpack 打包,把.vue 檔案通過 vue-loader 處理。

實現一個簡易的vue-loader,通過一系列正則,最終一個.vue 檔案的內容會被包裝到一個物件中

比方說我現在的.vue 檔案寫了下面這些內容:

<template>
  <div>
    <h2>{{ count + 1 }}</h2>
    <button @click="plus(1)">+</button>
  </div>
</template>

<script>
export default {
  name: 'App',
  data () {
    return {
      count: 0
    }
  },
  methods: {
    plus (num) {
      this.count += num;
    }
  }
}
</script>

那麼經過 vue-loader 處理,就會變成一個物件:

{
  template:
   `<div>
     <h2>{{ count + 1 }}</h2>
      <button @click="plus(1)">+</button>
  </div>`,
  name: 'App',
  data() {
    return { count: 0 }
  },
  methods: {
    plus(num) { this.count += num; },
  }
}

那麼,在瀏覽器執行這個檔案的時候,我們就能通過createApp方法,把這個物件使用 createApp 進行處理,掛載到頁面上

createApp 實現部分

在 vue 的main.js檔案中,我們通常會把根元件傳遞給createApp作為入參,如:

import App from './App';
import { createApp } from '../modules/vue';
createApp(App).mount('#app');

那我們實現的重點就在於createAppvue 元件的處理,以及在createApp的返回內容(就是 vm)中新增mount方法,實現處理完的節點的掛載

接下來就一步步實現createApp,首先,我們先來定義一個 vm,一會兒所有的屬性都可以放在 vm 上,同時把vue-loader解析過的檔案物件中的內容給解構出來

function createApp(component) {
  const vm = {};
  const { template, methods, data } = component;
}

template 解析

在上面經過vur-loader處理後,template以字串形式被放到物件中,所以我們可以拿到 dom 元素字串,把他轉成 dom 元素

/* 
  template:
   `<div>
     <h2>{{ count + 1 }}</h2>
      <button @click="plus(1)">+</button>
  </div>`,
*/
vm.$node = createNode(template);

function createNode(template) {
  const _tempNode = document.createElement('div');
  _tempNode.innerHTML = template;
  return getFirstChildNode(_tempNode);
}

這樣,我們就拿到了 html 接下來就是對 js 的操作

data 響應式處理

vue 的核心就在於響應式,vue2 通過Object.defineProperty實現響應式,我們來實現個簡單的響應式處理

首先拿到data,為了建立多個元件時data不被互相影響,所以data是一個函數

vm.$data = data();

for (let key in vm.$data) {
  Object.defineProperty(vm, key, {
    get() {
      return vm.$data[key];
    },
    set(newValue) {
      vm.$data[key] = newValue;
      // update觸發節點更新,至於實現我放到後面再說
      update(vm, key);
    },
  });
}

這樣,我們就監聽了data中每個屬性的getset,實現了資料的響應式處理

初始化資料池

在上面的 template 解析中,我們已經拿到了template轉換過後的節點,但是有個問題,節點的內容沒有經過任何處理,如{{count + 1}}會原封不動的展示在瀏覽器中,我們希望的是最終展示的是 count 這個變數+1 的結果,所以我們需要對雙括號語法進行解析

我們先定義一個正規表示式,匹配{{}}中的內容,以及定義一個節點資料池

// 節點資料池
const exprPool = new Map();
// 正則獲取雙括號中內容
const regExpr = /{{(.+?)}}/;

然後,從我們剛剛定義的vm.$node中拿到所有節點,並檢視該節點是否有雙括號語法,如果有的話存入節點資料池中

const allNodes = $node.querySelectorAll('*');
allNodes.forEach((node) => {
  // 這裡獲取到的textContent是原原始的沒經過任何處理的節點內容,如{{count + 1}}
  const vExpression = node.textContent;
  /* exprMatched:{
      0: "{{ count + 1 }}"
      1: " count + 1 "
      groups: undefined
      index: 0
      input: "{{ count + 1 }}"
    }
    */
  const exprMatched = vExpression.match(regExpr);
  // 如果有雙括號語法
  if (exprMatched) {
    const poolInfo = checkExpressionHasData($data, exprMatched[1].trim());
    // 把節點存入節點資料池
    poolInfo && exprPool.set(node, poolInfo);
  }
});

function checkExpressionHasData(data, expression) {
  for (let key in data) {
    if (expression.includes(key) && expression !== key) {
      // count + 1,返回{key:count,expression:count+1}
      return {
        key,
        expression,
      };
    } else if (expression === key) {
      // count,返回{key:count,expression:count}
      return {
        key,
        expression: key,
      };
    } else {
      return null;
    }
  }
}

初始化事件池

處理完雙括號語法,我們還需要處理@click這樣的事件語法,首先,我們建立一個事件池,再定義兩個正則分別匹配函數

const eventPool = new Map();
// 匹配函數名
const regStringFn = /(.+?)((.+?))/;
// 匹配函數引數
const regString = /'(.+?)'/;

同樣的,我們也需要遍歷所有節點

const allNodes = $node.querySelectorAll('*');
allNodes.forEach((node) => {
  const vClickVal = node.getAttribute(`@click`);
  if (vClickVal) {
    /* 
      比如@click='plus(1)',解析完成的fnInfo就是
      fnInfo:{
        args: [1]
        methodName: "plus"
      }
      */
    const fnInfo = checkFunctionHasArgs(vClickVal);
    const handler = fnInfo
      ? //有參函數傳入args
        methods[fnInfo.methodName].bind(vm, ...fnInfo.args)
      : //無參函數直接繫結
        methods[vClickVal].bind(vm);

    //存入事件池,節點為key,事件為value
    eventPool.set(node, {
      type: vClick,
      handler,
    });
    //刪除dom上的attr,不然瀏覽器檢視原始碼就會顯示自定義事件  這樣不好
    node.removeAttribute(`@${vClick}`);
  }
});

function checkFunctionHasArgs(str) {
  const matched = str.match(regStringFn);

  if (matched) {
    const argArr = matched[2].split(',');
    const args = checkIsString(matched[2])
      ? argArr // ['1']
      : argArr.map((item) => Number(item));

    return {
      methodName: matched[1],
      args,
    };
  }
}
function checkIsString(str) {
  return str.match(regString);
}

這樣,我們有擁有了節點資料池和事件池,接下來我們就要拿節點資料池和事件池做操作了

繫結事件處理

有了事件池,我們就要把事件池中的事件繫結到 dom 元素上去,讓事件能夠觸發。這步其實是很容易的,因為我們把 vue 事件加入事件池中時,key 是 dom 元素value 是事件處理常式,只要把他們兩個互相繫結就行

function (vm) {
  //node:key  info:value
  for (let [node, info] of eventPool) {
        // type:事件型別  handler:事件處理常式
    let { type, handler } = info;
    //在vue中,是用this.function 去存取方法,所以方法要被繫結到vm上
    vm[handler.name] = handler;
    //給節點繫結事件處理常式
    node.addEventListener(type, vm[handler.name], false);
  }
}

render 頁面

執行完上面的內容,我們就到了最後一步 render 頁面了,我們只要把節點資料池中的節點內容渲染到瀏覽器

function render(vm) {
  exprPool.forEach((info, node) => {
    _render(vm, node, info);
  });
}
function _render(vm, node, info) {
  //info:{key: 'count',expression 'count + 1'}
  const { expression } = info;
  //expression是一個字串,為了執行字串,所以我們需要new Function
  const r = new Function(
    'vm',
    'node',
    `
    with (vm) {
      node.textContent = ${expression};
    }
  `
  );

  r(vm, node);
}

在這裡,我們先解決兩個問題

  • with 是幹啥用的?
  • 為什麼_render 要抽離出來?

首先先來介紹下 with

with 的作用是用來改變識別符號的查詢優先順序,優先從 with 指定物件的屬性中查詢。e.g:

var a = 1;
var obj = {
  a: 2,
};
with (obj) {
  console.log(a); //2
}

那為什麼_render 要單獨抽成一個函數? 因為在前面的 data 響應式處理 中,set被觸發時,我們需要拿到新的資料值去update頁面元素,這時候就也會用到render函數,那就簡單實現下上面提到的updata

export function update(vm, key) {
  //在節點資料池中查詢哪個節點的key==當前改變的key,找到則重新render
  exprPool.forEach((info, node) => {
    if (info.key === key) {
      _render(vm, node, info);
    }
  });
}

到此為止,就能實現一個完整的不通過任何第三方外掛解析 vue 檔案,並實現簡單的響應式處理了!!

到此這篇關於實現一個vue檔案解析器的文章就介紹到這了,更多相關vue檔案解析器內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com