Vue原理之模板編譯

// 模板內容
<div id="app">
  <input type="text" v-model="message"> {{message}} <div>{{a.b}}</div> </div>

// vue腳本
let vm = new Vue({
  el: '#app',
  data: {
    message: '我是message',
    a: {
      b: '個人a.b' 
    }
  }
})
複製代碼

看到上面的代碼,使用過vue的同窗能知道頁面的渲染結果會以下圖所示:vue

那他是如何進行渲染的呢,咱們帶着問題來進入正題。node

首先新建Vue.js並建立一個名爲Vue的類:

class Vue {
  constructor(options) {
    // 掛載可用數據到實例上
    this.$el = options.el;
    this.$data = options.data;

    // 若是含有模板就去編譯
    if (this.$el) {
      // 用數據和元素進行編譯
      new Compile(this.$el, this);
    }
  }
}
複製代碼

以上代碼就是對new Vue時傳遞的參數el和data進行存儲,再利用Compile來對編譯模板。git

Compile類對模板進行處理:

新建一個compile.js的文件,並建立Compile類github

class Compile{
  constructor(el ,vm) {
    this.el = this.isElememtNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    if (this.el) {
      // 把須要操做的dom先放到內存中
      let fragment = this.node2fragment(this.el);
      // 編譯:提取元素節點的v-model和文本節點{{}}
      this.compile(fragment);
      // 把編譯完成的元素放到頁面中
      this.el.appendChild(fragment);
    }
  }
}
複製代碼

因爲Vue中的el是能夠傳遞選擇器和元素節點的,咱們這裏也對el作了相應的處理。數組

判斷用戶傳遞的el是不是元素節點,若是是元素節點使用,若是是選擇器,就獲取元素後進行使用。app

// isElememtNode
isElememtNode(node) {
  return node.nodeType === 1;
}
複製代碼

獲取跟元素節點後,利用node2fragment函數把dom元素放入內存中處理:

node2fragment(el) {
   // 建立文檔碎片
   let fragment = document.createDocumentFragment();
   let firstChild;
   while(firstChild = el.firstChild) {
     // 把dom元素移入到fragment
     fragment.appendChild(firstChild);
   }
   return fragment;
 }
複製代碼

這樣咱們就獲得了fragment,接下來的處理,咱們只須要對fragment進行處理便可。dom

拿到了文檔碎片fragment,咱們就能夠開始編寫Compile核心函數了

compile(fragment) {
  // 獲取fragment的全部子元素
  let childNodes = fragment.childNodes;
  Array.from(childNodes).forEach(node => {
  if (this.isElememtNode(node)) {
      // 編譯元素
      this.compileElement(node);
      // 遞歸執行
      this.compile(node);
    } else {
      this.compileText(node);
    }
  })
}
複製代碼

獲取全部子元素後,分別針對是元素節點和文本節點的狀況進行處理,須要指出的一點就是,元素節點內部可能還有子元素, 因此咱們以當前子節點爲參數遞歸執行compile。mvvm

咱們再分別來看一下compileElement和compileText兩個方法函數

compileText

// 編譯文本節點
compileText(node) {
  let expr = node.textContent;
  // 匹配開頭是{{結尾是}}而且中間不存在}的值
  let reg = /\{\{([^}]+)\}\}/g;
  if (reg.test(expr)) {
    CompileUtil['text'](node, this.vm, expr);
  }
}

複製代碼

其中用到的正則:ui

/\{\{([^}]+)\}\}/g;
複製代碼

若是對這個正則不理解,咱們能夠配合圖來理解一下

他實現的功能就是匹配開頭是 {{ 結尾是 }} 而且中間不存在 } 的字符串模板。

獲得字符串模板以後咱們就能夠vm實例中取到對應的值,具體的處理,咱們分離到CompileUtil中來實現。

compileElement

若是是元素節點,咱們須要考慮的就是其存在指令的狀況(本篇文章只講述v-model的狀況)

咱們分爲三步來實現該功能

  1. 獲取元素節點的屬性集合
  2. 判斷屬性是否爲指令(isDirective函數)
  3. 若是是指令,利用CompileUtil函數作對應處理。
// 編譯元素節點
compileElement(node) {
  let attrs = node.attributes; // 獲取當前節點的屬性
  Array.from(attrs).forEach(attr => {
    let attrName = attr.name;
    // 若是是指令進行數據處理
    if (this.isDirective(attrName)) {
      let expr = attr.value;
      let [,type] = attrName.split('-');
      CompileUtil[type](node, this.vm, expr)
    }
  })
}

// 若是是v-開頭,咱們就認爲他是指令
isDirective(name) {
  return name.startsWith('v-');
}
複製代碼

以上compileText和compileElement兩個方法中,具體的處理方式都使用到了CompileUtil這個輔助類,咱們能夠來看一下其代碼實現。

CompileUtil

咱們先來看對於text的處理。

通過以上的處理,咱們會拿到相似於{{XXX}}的字符串,有了這個字符串,咱們還須要下面幾步:

  1. 獲得{{xxx}}中的xxx
  2. 尋找vm.$data中xxx對應的值
  3. 獲得對應值後,更新對應節點的文本內容

上面須要處理的一個難點是:咱們的須要的值多是對象中的對象,相似於{{a.b.c}},解決方案爲:先把字符串分隔成數組,再使用reduce每次都取到下一個key,最後利用key取到對應對象的值。

// 編譯所需的輔助方法
CompileUtil = {
  getVal(vm, expr) { // 獲取實例上對應的數據
    expr = expr.split('.');
    return expr.reduce((prev, next) => {
      return prev[next];
    }, vm.$data);
  },
  
  getTextVal(expr, vm) {
    return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      // expr: {{XXX}}
      // arguments[1]是XXX
      return this.getVal(vm, arguments[1]);
    });
  },

  text(node, vm, expr) { // 文本處理
    let updateFn = this.updater['textUpdater'];
    let value = this.getTextVal(expr, vm);
    updateFn && updateFn(node, value);
  },

  updater: {
    textUpdater(node, value) {
      node.textContent = value;
    }
  }
}
複製代碼

處理完了text,再來看如何處理指令

在上面的compileElement方法中,咱們判斷了節點屬性是不是指令,若是是指令咱們就拿到具體的指令,例如v-model咱們就拿到model,到這裏,咱們還須要如下幾步:

  1. 獲取到指令所對應的key,例如v-model=「message"中的message
  2. 更新節點的value值爲vm.data對應數據的值,例如vm.data.message
  3. 設置節點的value值爲對應的值

爲了實現以上需求,咱們給CompileUtil新增model方法

model(node, vm ,expr) { // v-model處理
  let updateFn = this.updater['modelUpdater'];
  updateFn && updateFn(node, this.getVal(vm, expr));
},
複製代碼

對應的modelUpdater:

modelUpdater(node, value) {
  node.value = value;
}
複製代碼

完整的CompileUtil代碼以下

// 編譯所需的輔助方法
CompileUtil = {
  getVal(vm, expr) { // 獲取實例上對應的數據
    expr = expr.split('.');
    return expr.reduce((prev, next) => {
      return prev[next];
    }, vm.$data);
  },
  
  getTextVal(expr, vm) {
    return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
      return this.getVal(vm, arguments[1]);
    });
  },

  text(node, vm, expr) { // 文本處理
    let updateFn = this.updater['textUpdater'];
    let value = this.getTextVal(expr, vm);
    updateFn && updateFn(node, value);
  },

  model(node, vm ,expr) { // v-model處理
    let updateFn = this.updater['modelUpdater'];
    updateFn && updateFn(node, this.getVal(vm, expr));
  },

  updater: {
    textUpdater(node, value) {
      node.textContent = value;
    },
    modelUpdater(node, value) {
      node.value = value;
    }
  }
}
複製代碼

到這裏,文本節點和v-model指令的編譯都已經完成。

最後一步,就是把文檔碎片fragment放回到根節點中去

this.el.appendChild(fragment);
複製代碼

到這裏,一個基礎的編譯環節就宣告完成,打開頁面就能獲得期待的渲染結果了👏👏👏

斗膽發文,歡迎吐槽和指正。

附上完整代碼示例,期待與您共同進步:github.com/Ljhhhhhh/mv…

相關文章
相關標籤/搜索