Vue源碼解析:AST語法樹轉render函數

通過對 Vue2.0 源碼閱讀,想寫一寫自己的理解,能力有限故從尤大佬2016.4.11第一次提交開始讀,準備陸續寫:

其中包含自己的理解和源碼的分析,儘量通俗易懂!由於是2.0的最早提交,所以和最新版本有很多差異、bug,後續將陸續補充,敬請諒解!包含中文註釋的Vue源碼已上傳...

開始

今天要說的代碼全在codegen文件夾中,在說實現原理前,還是先看個簡單的例子!

<div class="container">
    <span>{{msg}}</span>
    <button :class="{active: isActive}"  @click="handle">change msg</button>
</div>

上述類名爲container的元素節點包含5個子節點(其中3個是換行文本節點),轉化成的AST語法樹:
clipboard.png

AST語法樹轉的render函數長這樣:

function _render() {
  with (this) { 
    return __h__(
      'div', 
      {staticClass: "container"}, 
      [
        " ",
        __h__('span', {}, [String((msg))]),
        " ",
        __h__('button', {class: {active: isActive},on:{"click":handle}}, ["change msg"]),
        " "
      ]
    )
  };
}

可以的看出,render函數做的事情很簡單,就是把語法樹每個節點的指令進行解析。

看下render函數,它是由with函數包裹(爲了改變作用域),要用的時候直接_render.call(vm);另外就是__h__函數,這個後面會說到,這個函數用於元素節點的解析,接收3個參數:元素節點標籤名,節點數據,子節點數據。這個函數最後返回的就是虛擬dom了,不過今天先不深究,先說如何生成這樣的render函數,主要是v-ifv-forv-bindv-on等指令的解析。

源碼解析

這邊解析的是從AST樹轉換成render函數部分的源碼,由於vue2.0第一次提交的源碼這部分不全,故做了部分更新,代碼全在codegen文件夾中。

入口

整個AST語法樹轉render函數的起點是index.js文件中的generate()函數:

export function generate (ast) {
  const code = genElement(ast);
  return new Function (`with (this) { return ${code}}`);
}

明顯看到,generate()函數傳入參數爲AST語法樹,內部調用genElement()函數開始解析根節點(容器節點)。genElement()函數用於解析元素節點,它接收兩個參數:AST對象節點標識(v-for的key),最後返回形如__h__('div', {}, [])的字符串,看一下內部邏輯:

function genElement (el, key) {
  let exp;
  if (exp = getAndRemoveAttr(el, 'v-for')) { // 解析v-for指令
    return genFor(el, exp);
  } else if (exp = getAndRemoveAttr(el, 'v-if')) { // 解析v-if指令
    return genIf(el, exp, key);
  } else if (el.tag === 'template') { // 解析子組件
    return genChildren(el);
  } else {
    return `__h__('${el.tag}', ${genData(el, key) }, ${genChildren(el)})`;
  }
}

genElement()函數內部依次調用getAndRemoveAttr()函數判斷了v-forv-if標籤是否存在,若存在則刪除並返回表達式;隨後判斷節點名爲template就直接進入子節點解析;以上條件都不符合就返回__h__函數字符串,該字符串將使用到屬性解析和子節點解析。

function getAndRemoveAttr (el, attr) {
  let val;
  // 如果屬性存在,則從AST對象的attrs和attrsMap移除
  if (val = el.attrsMap[attr]) {
    el.attrsMap[attr] = null;
    for (let i = 0, l = el.attrs.length; i < l; i++) {
      if (el.attrs[i].name === attr) {
        el.attrs.splice(i, 1);
        break;
      }
    }
  }
  return val;
}

v-for 和 v-if 指令解析

讓我們先看看v-for的編譯:

function genFor (el, exp) {
  const inMatch = exp.match(/([a-zA-Z_][\w]*)\s+(?:in|of)\s+(.*)/);
  if (!inMatch) {
    throw new Error('Invalid v-for expression: '+ exp);
  }
  const alias = inMatch[1].trim();
  exp = inMatch[2].trim();
  let key = getAndRemoveAttr(el, 'track-by'); // 後面用 :key 代替了 track-by

  if (!key) {
    key ='undefined';
  } else if (key !== '$index') {
    key = alias + '["' + key + '"]';
  }

  return `(${exp}) && (${exp}).map(function (${alias}, $index) {return ${genElement(el, key)}})`;
}

該函數先進行正則匹配,如"item in items",將解析出別名(item)和表達式(items),再去看看當前節點是否含:key,如果有那就作爲genElement()函數的參數解析子節點。舉個🌰,對於模版<div v-for="item in items" track-by="id"></div>,將解析成:

`(items) && (items).map(function (item, $index) {return ${genElement(el, item["id"])}})`

你會發現v-for解析完,通過mao循環對該節點繼續解析,但此時該節點已經沒有的v-for:key屬性了。繼續看看v-if的解析:

function genIf (el, exp, key) {
  return `(${exp}) ? ${genElement(el, key)} : null`;
}

v-if的解析就很粗暴,直接通過條件運算符去決定繼續解析該節點,還是直接返回 null

屬性解析

這裏說的屬性解析,包括了v-bind指令、v-on指令和v-model指令的解析,以及普通屬性的解析。這些解析都在genData()函數中:

function genData (el, key) {
  if (!el.attrs.length && !key) {
    return '{}';
  }
  let data = '{';
  let attrs = `attrs:{`;
  let props = `props:{`;
  let events = {};
  let hasAttrs = false;
  let hasProps = false;
  let hasEvents = false;

  ...

  if (hasAttrs) {
    data += attrs.slice(0, -1) + '},';
  }
  if (hasProps) {
    data += props.slice(0, -1) + '},';
  }
  if (hasEvents) {
    data += genEvents(events); // 事件解析
  }
  return data.replace(/,$/, '') + '}';
}

看一下genData()函數整體,先是判斷有沒有屬性,然後定義了多個變量:data是輸出結果;attrs用於存儲節點屬性;props用於存儲節點某些特殊屬性;event用於存儲事件;hasxxx是當前節點是否含xxx的標識。隨後會進行屬性的遍歷計算,最後通過對hasxxx的判斷來對data進行拼接輸出。重點是中間屬性的遍歷、各種指令/屬性的處理,先看看特殊的keyclass

if (key) {
  data += `key:${key},`;
}

const classBinding = getAndRemoveAttr(el, ':class') || getAndRemoveAttr(el, 'v-bind:class');
if (classBinding) {
  data += `class: ${classBinding},`;
}
const staticClass = getAndRemoveAttr(el, 'class');
if (staticClass) {
  data += `staticClass: "${staticClass}",`;
}

這邊也是調用getAndRemoveAttr()獲取class屬性,並以動態和靜態進行存儲,比較簡單。再來看看其他屬性的處理:

for (let i = 0, l = el.attrs.length; i < l; i++) {
  let attr = el.attrs[i];
  let name = attr.name;
  let value = attr.value;

  if (/^v-|^@|^:/.test(name)) { 
    const modifiers = parseModifiers(name);  // 事件修飾符(.stop/.prevent/.self)
    name = removeModifiers(name);

    if (/^:|^v-bind:/.test(name)) {  // v-bind
      name = name.replace(/^:|^v-bind:/, '');
      if (name === 'style') {
        data += `style: ${value},`;
      } else if (/^(value|selected|checked|muted)$/.test(name)) {
        hasProps = true;
        props += `"${name}": (${value}),`;
      } else {
        hasAttrs = true;
        attrs += `"${name}": (${value}),`;
      }
    } else if (/^@|^v-on:/.test(name)) { // v-on
      hasEvents = true;
      name = name.replace(/^@|^v-on:/, '');
      addHandler(events, name, value, modifiers);
    } else if (name === 'v-model') { // v-model
      hasProps = hasEvents = true;
      props += genModel(el, events, value) + ',';
    } 
  } else { 
    hasAttrs = true;
    attrs += `"${name}": (${JSON.stringify(attr.value)}),`;
  }
}

通過for循環對節點屬性進行遍歷,先用/^v-|^@|^:/正則判斷當前屬性是否爲指令,若不是就直接添加到attrs中,若是就需要繼續進行解析了。進入if後首先來到了事件修飾符的處理,主要用到了parseModifiers()removeModifiers()兩個函數,主要就是拿到事件修飾符並刪除,如v-on:click.prevent.self,將返回['prevent', 'self'],簡單看一下:

function parseModifiers (name) {
  const match = name.match(/\.[^\.]+/g);
  if (match) {
    return match.map(m => m.slice(1));
  }
}

function removeModifiers (name) {
  return name.replace(/\.[^\.]+/g, '');
}

然後進入v-bind的處理,依次處理了:style、特殊屬性、其他屬性...這邊特殊屬性用正則/^(value|selected|checked|muted)$/去匹配,之所以特殊我的理解是:含有該屬性的元素會在頁面加載時給自身默認狀態,如想默認選擇複選框,給它加上checked="checked"就行了,但是後續不能用setAttribute()修改,而是通過checkboxObject.checked=true|false更改狀態。

v-bind解析完了,進入v-on的解析,主要是用到了addHandler()函數,這部分在event.js中。

function addHandler (events, name, value, modifiers) {
    const captureIndex = modifiers && modifiers.indexOf('capture');
    if (captureIndex > -1) {
        modifiers.splice(captureIndex, 1);
        name = '!' + name;
    }
    const newHandler = { value, modifiers };
    const handlers = events[name];
    if (isArray(handlers)) {
        handlers.push(newHandler);
    } else if (handlers) {
        events[name] = [handlers, newHandler];
    } else {
        events[name] = newHandler;
    }
}

該函數先對capture事件修飾符(事件捕獲模式)進行了判斷,若有就給name前加個!標識;然後就去events裏面找是否已經有name事件了,找到一種情況追加進去,所以events可能長這樣:{click: change, mouseleave: [fn1, fn2]}

最後來說說v-model指令,實現原理就是v-bindv-on的結合,例如你想對輸入框進行雙向綁定,你也可以寫成

<input :value="val" @input="fn">

{
  data: {
     val: ''
  },
  methods: {
    fn (e) {
      this.val = e.target.value;
    }
  }
}

所以對雙向綁定的處理,就是對不同的元素節點採用不同的事件綁定而已,如對於select標籤用onchange監聽,對文本輸入框用oninput監聽...這部分的代碼全在model.js文件中,看一下genModel()函數吧:

function genModel (el, events, value) {
  if (el.tag === 'select') {
    if (el.attrsMap.multiple != null) { // 同時選擇多個選項
      return genMultiSelect(events, value, el)
    } else {
      return genSelect(events, value)
    }
  } else {
    switch (el.attrsMap.type) {
      case 'checkbox':
        return genCheckboxModel(events, value)
      case 'radio':
        return genRadioModel(events, value, el)
      default:
        return genDefaultModel(events, value)
    }
  }
}

依次找了select標籤和input標籤,這邊還考慮到了下拉標籤的多選情況,然後找對應函數去解析,這邊就拿文本框的處理函數genDefaultModel()來舉例:

function genDefaultModel (events, value) {
  addHandler(events, 'input', `${value}=$event.target.value`);
  return `value:(${value})`;
}

該函數先調用之前提到的addHandler()函數添加時間,再返回value屬性追加到props中。其他下拉框、單選框等的處理函數也是類似...

最後還有對事件的處理,我們前面只是把事件都存儲到events對象中,需要處理後添加到data返回值中,主要用到的函數是genEvents()

const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/
const modifierCode = {
  stop: '$event.stopPropagation();',
  prevent: '$event.preventDefault();',
  self: 'if($event.target !== $event.currentTarget)return;'
}

function genEvents (events) {
    let res = 'on:{';
    for (let name in events) {
        res += `"${name}":${genHandler(events[name])},`;
    }
    return res.slice(0, -1) + '}';
}

function genHandler (handler) {
  if (!handler) {
    return `function(){}`;
  } else if (isArray(handler)) {
    // handler爲數組則循環調用
    return `[${handler.map(genHandler).join(',')}]`;
  } else if (!handler.modifiers || !handler.modifiers.length) {
    
    return simplePathRE.test(handler.value)
      ? handler.value
      : `function($event){${handler.value}}`;
  } else {
    let code = 'function($event){';
    for (let i = 0; i < handler.modifiers.length; i++) {
      let modifier = handler.modifiers[i];
      code += modifierCode[modifier];
    }
    let handlerCode = simplePathRE.test(handler.value)
      ? handler.value + '()'
      : handler.value;
    return code + handlerCode + '}';
  }
}

simplePathRE正則用於看屬性值是否是簡單函數名,fn是簡單函數名而fn('x')不是;modifierCode對象用於存儲事件修飾符對應的js代碼;genEvents()函數對events對象進行遍歷,調用genHandler()函數逐個解析;genHandler()函數內部是對不同的參數進行不同的處理,做的比較好的是:

  • 對是否是簡單函數的處理,例如@click="fn"會返回click: fn@click="fn('11')"會返回click: function($event){fn('11')},這將大大便利了後續dom事件的綁定;
  • 對是否含事件修飾符的處理,例如@click.stop="fn",將返回click: function($event){$event.stopPropagation();fn()}

到這裏,所有屬性都解析完畢了!返回的結果形如{key: ...,class: ...,staticClass: ...,attrs: {...},props: {...},on: {...}}

子節點解析

子節點的解析主要是用到了genChildren()函數:

function genChildren (el) {
  if (!el.children.length) {
    return 'undefined';
  }
  return '[' + el.children.map(node => {
    if (node.tag) {
      return genElement(node);
    } else {
      return genText(node);
    }
  }).join(',') + ']';
}

通過map方法對子節點數組進行循環,依次判斷節點標籤是否存在,再分別解析元素節點和文本節點,最後將結果拼接成數組形式的字符串。元素節點的解析函數genElement()上面說過了,接下來說說文本節點的解析函數genText()

function genText (text) {
  if (text === ' ') {
    return '" "';
  } else {
    const exp = parseText(text);
    if (exp) {
      return 'String(' + exp + ')';
    } else {
      return JSON.stringify(text);
    }
  }
}

判斷一波是否有文本,有就繼續調用parseText()函數:

const tagRE = /\{\{((?:.|\\n)+?)\}\}/g;
export function parseText (text) {
  if (!tagRE.test(text)) {
    return null;
  }
  var tokens = [];
  var lastIndex = tagRE.lastIndex = 0;
  var match, index, value;
  while (match = tagRE.exec(text)) { // 循環解析 {{}}
    index = match.index;
    // 把 '{{' 之前的文本推入
    if (index > lastIndex) {
      tokens.push(JSON.stringify(text.slice(lastIndex, index)));
    }
    // 把{{}}中間數據取出推入
    value = match[1];
    tokens.push('(' + match[1].trim() + ')');
    lastIndex = index + match[0].length;
  }
  if (lastIndex < text.length) {
    tokens.push(JSON.stringify(text.slice(lastIndex)));
  }
  return tokens.join('+');
}

該函數通過循環調用tagRE正則匹配文本,依次匹配出 {{}},並推入數組,最後將數組轉爲字符串。例如文本hi,{{name}}!,將返回'hi'+(name)+'!'

總結

到這也終於算是說完了,雖然這部分做的事情比較簡單,主要就是指令解析,將AST樹解析爲render函數,但代碼量感覺挺大的,這邊還有很多地方等待完善,等後續繼續補充...

好睏啊,晚安了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章