模板编译

76 阅读5分钟

问题和知识点集合

1.  问题:为什么要一个数据一个dep

这会造成,这个组件里面的所有的dep收集的都是同一个渲染watcher,既然都是一个watcher,那创建那么多dep干甚?

答:平时用户会写很多watcher,computed,当数据变化之后,调用的是用户写的回调函数,只有每个数据都对应一个dep,才能精准收集每个数据对应的回调函数。如此细粒度的dep是为了用户watcher准备的,不是给渲染watcher用的。

2.  再问依赖收集什么时候被收集到,和编译环节有关系吗

答:和编译环节没有关系,编译环节生成render函数。是在执行render函数生成虚拟dom阶段,会访问到具体的值。拿到了这些值,才知道vnode长什么样。

注意:和diff算法一点关系都没有,因为render函数执行的时候,根本还没有进入到diff环节

updateComponent = function () {
   vm._update(vm._render(), hydrating);
};

例子:

html

    <div id="demo">
      <div>{{currentBranch}}</div>
    </div>

render函数

(function anonymous() {
  with(this){
    return _c(
      'div',
      {
        attrs:{
          "id":"demo"
        }
      },
      [
        _c(
          'div',
          [
            _v(
              _s(currentBranch)
              )
          ]
        )
      ]
    )
  }
})

在执行_s(currentBranch)语句过程中,在这个_s函数执行之前,先会访问所传参数currentBranch,此时就触发了get函数。不是一定要函数里面用到这个参数,才会访问,只要传了,就等于访问了一次this.currentBranch。

构建组件实例 =》

new Watcher =》

第一次patch =》

批量创建_update(_render()) =》

_s(currentBranch) =》 // 在即将执行之前,会先访问参数,然后再执行函数

访问值触发get =》

dep开始做依赖收集

3.  为什么vue中的render函数用with语句

1.  参考尤雨溪自己的回答

总结:用with可以执行作用域,使得生成的render函数的代码量大量减少

2.  with语句用法

var a = new Function(`
with(this){
    console.log(location.href)
  }
`)
a()
//file:///D:/MyDocument/%E6%A1%8C%E9%9D%A2/project/%E9%BB%84%E6%AF%85vue%E6%BA%90%E7%A0%81/vue-dev/examples/commits/index.html

剩余问题点

1.  slot原理,重要

2.  keep-alive原理,重要

模板是如何变成渲染函数render的,三个步骤

1.  解析:模板转化为对象AST(抽象语法树)

AST:用对象的形式描述我们将要生成的js代码

2.  优化:标记静态节点,diff时候直接跳过,节约性能

3.  生成:代码生成,转化ast为代码的字符串。

将来怎么转化为真正函数呢?new Function('代码字符串'),new一个Function,里面传入字符串就行了

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1. 解析:模板转化为对象AST
  const ast = parse(template.trim(), options)
  // 2. 优化:标记静态节点
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 代码生成
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

例子

html

    <div id="demo">
      <div>{{currentBranch}}</div>
    </div>

render函数

根元素一个div,有属性id,子元素共一个,div,子元素有一个孙子元素,一个文本节点。

执行这个render函数,则执行里面的with语句,则有执行with里面的return里面的函数,这些是具体的生成虚拟dom的函数,最终return出去的,是一份完整的虚拟dom,就是一份jjs对象。

这个返回的render函数是已经new Function过之后的,已经是一个真正的函数,不是字符串,字符串是with语句的那段。

(function anonymous() {
  with(this){
    return _c(
      'div',
      {
        attrs:{
          "id":"demo"
        }
      },
      [
        _c(
          'div',
          [
            _v(
              _s(currentBranch)
              )
          ]
        )
      ]
    )
  }
})

1.  解析,parse

文件地址:src\compiler\parser\index.js

compileToFunctions =》

createCompiler =》

createCompilerCreator =》 解析,优化,代码生成都在这个函数

parse =》

parseHTML:得到模板字符串,

用正则解析:

解析HTML的方法类似于一个栈的方式,对应开始标签和结束标签,不断地入栈出栈。

只要遇到开始标签,就执行start函数,遇到结束就是end,遇到文本就chars,遇到注释就是comment。整个过程是一个递归过程。

每次遇到一个新的开始标签,就新创建一个ast对象

  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    start (tag, attrs, unary) {
        //
    },
    end () {
        // 
    },
    chars (textstring) {
        //
    },
    comment (textstring) {
        //
    }
  })
  return root
}

关键指令解析(重点看)

文件地址:src\compiler\parser\index.js

      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        processFor(element)
        processIf(element)
        processOnce(element)
        // element-scope stuff
        processElement(element, options)
      }

2.  优化,optimize

主要就是标记静态节点,标记静态根节点。被标记静态节点的vnode会带有static:true,staticRoot:true这两个属性

export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  markStatic(root)
  // second pass: mark static roots.
  markStaticRoots(root, false)
}

3.  生成代码,generate

重点看几个重要语句都生成啥样的render函数,v-if,v-for,v-once等

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

真正生成代码字符串的函数:genElement,以下是几个重要的生成代码的点。

1.  v-if

通过代码可以看出,最终生成了一个三元表达式的字符串。

核心代码

  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

模板

<div v-if="test === 1">333</div>

最终生成的代码字符串,是一个三元表达式

(test === 1)?_c('div',[_v("333")]):_e()
2.  v-for

从一下源码和生成的代码可以看出,最终生成了_l函数,在render执行的时候,就会在with语句里面执行this._l最终生成vnode。在这个_l函数里面,还藏了很多别的函数,比如生成div就要有_c函数,主要是看for循环里面要做什么

核心源码

  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'

模板

<div v-for="(item, index) in arr" :key="item.key">{{item.value}}</div>
      arr: [
        {
          key: 11,
          value: '11'
        },
        {
          key: 22,
          value: '22'
        }
      ],

生成的代码字符串

_l(
(arr),
function(item,index){return _c('div',{key:item.key},[_v(_s(item.value))])}
)
3.  v-once

由例子可以看出,由于只渲染一次,在没有别的指令情况下,会执行生成静态元素的函数genStatic。

核心代码

// v-once
    function genOnce (el, state) {
      el.onceProcessed = true;
      if (el.if && !el.ifProcessed) {
        return genIf(el, state)
      } else if (el.staticInFor) {
        var key = '';
        var parent = el.parent;
        while (parent) {
          if (parent.for) {
            key = parent.key;
            break
          }
          parent = parent.parent;
        }
        if (!key) {
          state.warn(
            "v-once can only be used inside v-for that is keyed. ",
            el.rawAttrsMap['v-once']
          );
          return genElement(el, state)
        }
        return ("_o(" + (genElement(el, state)) + "," + (state.onceId++) + "," + key + ")")
      } else {
        return genStatic(el, state)
      }
    }

模板

<div v-once>{{num1}}</div>

生成代码字符串

_m(0)
4.  静态节点

静态节点调用的函数是genStatic,必须是两层嵌套起步才会被判定为静态节点,这个做应该是为了平衡内存和性能之间的取舍。

核心代码

function genStatic (el, state) {
      el.staticProcessed = true;
      // Some elements (templates) need to behave differently inside of a v-pre
      // node.  All pre nodes are static roots, so we can use this as a location to
      // wrap a state change and reset it upon exiting the pre node.
      var originalPreState = state.pre;
      if (el.pre) {
        state.pre = el.pre;
      }
      state.staticRenderFns.push(("with(this){return " + (genElement(el, state)) + "}"));
      state.pre = originalPreState;
      return ("_m(" + (state.staticRenderFns.length - 1) + (el.staticInFor ? ',true' : '') + ")")
    }

模板

  <div>
    <div>645465645</div>
  </div>

生成代码字符串

_m(1)

事件的处理

在编译生成代码字符串阶段,事件是被当做el.属性处理,在genData函数内,此函数处理了很多属性,如原生事件,v-model事件,其他各种属性等

export function genData (elASTElementstateCodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // module data generation functions
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:{${genProps(el.attrs)}},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:{${genProps(el.props)}},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/'') + '}'
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

1.  原生事件

原生事件最终编译得到的代码字符串如下

<div @click=a>{{num1}}</div>
{on:{"click":a}}

在mount阶段,执行render函数后,变成vnode的结果,在data里面有on对象

在patch阶段,vnode中的on对象,最终转化为真正的事件挂载到相应的dom上

步骤:

vm._update(vm._render(), hydrating); =》

patch =》

createElm =》

invodeCreateHooks =》

updateDOMListeners

invodeCreateHooks是在组件创建的过程中,创建元素的过程中执行的,如果当前元素有属性就会执行。不仅仅是包括有事件。

        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }

updateDOMListeners函数最终把事件挂载到dom上

function add (event, fn) {
  target.$on(event, fn);
}

// 添加原生事件
function add (
  eventstring,
  handler: Function,
  once: boolean,
  capture: boolean,
  passive: boolean
) {
  handler = withMacroTask(handler)
  if (once) handler = createOnceHandler(handler, event, capture)
  target.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

2.  自定义事件

步骤

patch =》

createComponent(创建组件) =》

hook.init(属性处理)=》

createComponentInstanceForVnode(创建组件实例) =》

init =》

initEvent =》

updateComponentListeners

事件的派发和监听者都是组件实例,自定义组件中一定伴随着原生事件的监听和处理

3.  双向绑定事件

编译阶段,编译后的结果:

1)有一个on监听事件,默认监听的是input事件,可以修改。

2)有一个domProps事件,这是添加属性事件,会给dom元素的value属性,值是inputValue的值。

<input type="text" v-model="inputValue">
    {
      directives:[{name:"model",rawName:"v-model",value:(inputValue),expression:"inputValue"}],
      attrs:{"type":"text"},
      domProps:{"value":(inputValue)},
      on:{"input":function($event){if($event.target.composing)return;inputValue=$event.target.value}}
    }

3)为什么还要有一个指令directives,是干什么用的。

在编译阶段用的,src\platforms\web\compiler\directives\model.js

编译阶段,针对不同的元素,如input,selectcheckbox,radio,textarea会生成不同的代码,这个指令就是在这个时候用到的

生成vnode后的结果

最后转化在真实dom身上,就是有一个原生input事件,还有inputValue值被依赖收集了,变成了响应式。

4.  组件双向绑定事件v-model

编译后代码字符串

patch过程中的处理,在createComponent函数内

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

初始化阶段,对节点赋值以及事件监听

对节点赋值:src\platforms\web\runtime\modules\dom-props.js

事件监听:src\platforms\web\runtime\modules\events.js

额外的model指令:src\platforms\web\compiler\directives\model.js

5.  事件,彩蛋(@hook:name)

在Vue当中,hooks可以作为一种event,在Vue源码当中,称之为hookEvent。

<el-table @hook:created="handleTableCreated" />

应用场景举例:有一个来自第三方的复杂表格组件,表格进行数据更新的时候渲染时间需要一秒,由于渲染时间较长,为了更好的用户体验,我们希望在表格进行更新的时候显示一个loading动画,修改源码这个方法不优雅,于是可以用hookEvent。

原理:

在源码中,如果有hookEvent,则会额外派发一个事件出去,事件名称写死是hook:开头的。

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook] // 这是一个数组
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}