9.1. 模板编译

Vue在挂载实例前,有相当多的工作是进行模板的编译,将template模板进行编译,解析成AST树,再转换成平台所需的代码,对于客户端而言,就是render函数,有了render函数才能进入实例挂载过程。而对于事件而言,我们经常使用v-on或者@在模板上绑定事件。因此对事件的第一步处理,就是在编译阶段对事件指令做收集处理。

从一个简单的用法分析编译阶段收集的信息:

  1. <div id="app">
  2. <div v-on:click.stop="doThis">点击</div>
  3. <span>{{count}}</span>
  4. </div>
  5. <script>
  6. var vm = new Vue({
  7. el: '#app',
  8. data() {
  9. return {
  10. count: 1
  11. }
  12. },
  13. methods: {
  14. doThis() {
  15. ++this.count
  16. }
  17. }
  18. })
  19. </script>

编译的入口在var ast = parse(template.trim(), options);中,具体可参考深入剖析Vue源码 - 实例挂载,编译流程这节,parse通过拆分模板字符串,将其解析为一个AST树,其中对于属性解析后的处理,在processAttr中,由于分支较多,我们只分析例子中的流程。

  1. function processAttrs (el) {
  2. var list = el.attrsList;
  3. var i, l, name, rawName, value, modifiers, syncGen, isDynamic;
  4. for (i = 0, l = list.length; i < l; i++) {
  5. name = rawName = list[i].name; // v-on:click
  6. value = list[i].value; // doThis
  7. if (dirRE.test(name)) { // 匹配v-或者@开头的指令
  8. el.hasBindings = true;
  9. modifiers = parseModifiers(name.replace(dirRE, ''));// parseModifiers('on:click')
  10. if (modifiers) {
  11. name = name.replace(modifierRE, '');
  12. }
  13. if (bindRE.test(name)) { // v-bind分支
  14. // ...留到v-bind指令时分析
  15. } else if (onRE.test(name)) { // v-on分支
  16. name = name.replace(onRE, ''); // 拿到真正的事件click
  17. isDynamic = dynamicArgRE.test(name);// 动态事件绑定
  18. if (isDynamic) {
  19. name = name.slice(1, -1);
  20. }
  21. addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic);
  22. } else { // normal directives
  23. // 其他指令相关逻辑
  24. } else {}
  25. }
  26. }

processAttrs的逻辑虽然较多,但是理解起来较为简单,通过正则匹配到事件指令相关内容,包括事件本身,事件回调以及事件修饰符。最终通过addHandler方法,为AST树添加事件相关的属性。而addHandler另一个重要功能是对事件修饰符进行特殊处理。

  1. // el是当前解析的AST树
  2. function addHandler (el,name,value,modifiers,important,warn,range,dynamic) {
  3. modifiers = modifiers || emptyObject;
  4. // passive 和 prevent不能同时使用,可以参照官方文档说明
  5. if (
  6. warn &&
  7. modifiers.prevent && modifiers.passive
  8. ) {
  9. warn(
  10. 'passive and prevent can\'t be used together. ' +
  11. 'Passive handler can\'t prevent default event.',
  12. range
  13. );
  14. }
  15. // 这部分的逻辑会对特殊的修饰符做字符串拼接的处理,以备后续的使用
  16. if (modifiers.right) {
  17. if (dynamic) {
  18. name = "(" + name + ")==='click'?'contextmenu':(" + name + ")";
  19. } else if (name === 'click') {
  20. name = 'contextmenu';
  21. delete modifiers.right;
  22. }
  23. } else if (modifiers.middle) {
  24. if (dynamic) {
  25. name = "(" + name + ")==='click'?'mouseup':(" + name + ")";
  26. } else if (name === 'click') {
  27. name = 'mouseup';
  28. }
  29. }
  30. if (modifiers.capture) {
  31. delete modifiers.capture;
  32. name = prependModifierMarker('!', name, dynamic);
  33. }
  34. if (modifiers.once) {
  35. delete modifiers.once;
  36. name = prependModifierMarker('~', name, dynamic);
  37. }
  38. /* istanbul ignore if */
  39. if (modifiers.passive) {
  40. delete modifiers.passive;
  41. name = prependModifierMarker('&', name, dynamic);
  42. }
  43. // events 用来记录绑定的事件
  44. var events;
  45. if (modifiers.native) {
  46. delete modifiers.native;
  47. events = el.nativeEvents || (el.nativeEvents = {});
  48. } else {
  49. events = el.events || (el.events = {});
  50. }
  51. var newHandler = rangeSetItem({ value: value.trim(), dynamic: dynamic }, range);
  52. if (modifiers !== emptyObject) {
  53. newHandler.modifiers = modifiers;
  54. }
  55. var handlers = events[name];
  56. /* istanbul ignore if */
  57. // 绑定的事件可以多个,回调也可以多个,最终会合并到数组中
  58. if (Array.isArray(handlers)) {
  59. important ? handlers.unshift(newHandler) : handlers.push(newHandler);
  60. } else if (handlers) {
  61. events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
  62. } else {
  63. events[name] = newHandler;
  64. }
  65. el.plain = false;
  66. }

最终转换的AST树如下:

9.1. 模板编译 - 图1