Vue源码解析:AST语法树转render函数

开始

今天要说的代码全在codegen文件夹中,在说实现原理前,仍是先看个简单的例子!html

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

上述类名为container的元素节点包含5个子节点(其中3个是换行文本节点),转化成的AST语法树:
vue

AST语法树转的render函数长这样:node

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

能够的看出,render函数作的事情很简单,就是把语法树每一个节点的指令进行解析。git

看下render函数,它是由with函数包裹(为了改变做用域),要用的时候直接_render.call(vm);另外就是__h__函数,这个后面会说到,这个函数用于元素节点的解析,接收3个参数:元素节点标签名,节点数据,子节点数据。这个函数最后返回的就是虚拟dom了,不过今天先不深究,先说如何生成这样的render函数,主要是v-ifv-forv-bindv-on等指令的解析。github

源码解析

这边解析的是从AST树转换成render函数部分的源码,因为vue2.0第一次提交的源码这部分不全,故作了部分更新,代码全在codegen文件夹中。express

入口

整个AST语法树转render函数的起点是index.js文件中的generate()函数:segmentfault

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__函数字符串,该字符串将使用到属性解析和子节点解析。dom

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的编译:ide

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) &amp;&amp; (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 &amp;&amp; !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 &lt; 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 =&gt; 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 &amp;&amp; modifiers.indexOf('capture');
    if (captureIndex &gt; -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;
}
}
}

<p>因此对双向绑定的处理,就是对不一样的元素节点采用不一样的事件绑定而已,如对于select标签用onchange监听,对文本输入框用oninput监听...这部分的代码全在<code>model.js</code>文件中,看一下<code>genModel()</code>函数吧:</p>

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)
}
}
}

<p>依次找了select标签和input标签,这边还考虑到了<a href="http://www.w3school.com.cn/tags/att_select_multiple.asp" rel="nofollow noreferrer">下拉标签的多选</a>状况,而后找对应函数去解析,这边就拿文本框的处理函数<code>genDefaultModel()</code>来举例:</p>

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

<p>该函数先调用以前提到的<code>addHandler()</code>函数添加时间,再返回<code>value</code>属性追加到props中。其余下拉框、单选框等的处理函数也是相似...</p>
<p>最后还有对事件的处理,咱们前面只是把事件都存储到<code>events</code>对象中,须要处理后添加到<code>data</code>返回值中,主要用到的函数是<code>genEvents()</code>:</p>

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 + '}';
}
}

<p><code>simplePathRE</code>正则用于看属性值是不是简单函数名,<code>fn</code>是简单函数名而<code>fn('x')</code>不是;<code>modifierCode</code>对象用于存储事件修饰符对应的js代码;<code>genEvents()</code>函数对<code>events</code>对象进行遍历,调用<code>genHandler()</code>函数逐个解析;<code>genHandler()</code>函数内部是对不一样的参数进行不一样的处理,作的比较好的是:</p>
<ul>
<li>对是不是简单函数的处理,例如<code>@click="fn"</code>会返回<code>click: fn</code>,<code>@click="fn('11')"</code>会返回<code>click: function($event){fn('11')}</code>,这将大大便利了后续dom事件的绑定;</li>
<li>对是否含事件修饰符的处理,例如<code>@click.stop="fn"</code>,将返回<code>click: function($event){$event.stopPropagation();fn()}</code>。</li>
</ul>
<p>到这里,全部属性都解析完毕了!返回的结果形如<code>{key: ...,class: ...,staticClass: ...,attrs: {...},props: {...},on: {...}}</code>。</p>
<h2>子节点解析</h2>
<p>子节点的解析主要是用到了<code>genChildren()</code>函数:</p>

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(',') + ']';
}

<p>经过<code>map</code>方法对子节点数组进行循环,依次判断节点标签是否存在,再分别解析元素节点和文本节点,最后将结果拼接成数组形式的字符串。元素节点的解析函数<code>genElement()</code>上面说过了,接下来讲说文本节点的解析函数<code>genText()</code>:</p>

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

<p>判断一波是否有文本,有就继续调用<code>parseText()</code>函数:</p>

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函数,但代码量感受挺大的,这边还有不少地方等待完善,等后续继续补充...

好困啊,晚安了

来源:https://segmentfault.com/a/1190000017544298

相关文章
相关标签/搜索