vue2-compile完结

简介: 前言上篇文章分析了编译的parse部分,今天接着分析剩下的部分。主要是分为optimizer和generate。

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战


前言


上篇文章分析了编译的parse部分,今天接着分析剩下的部分。主要是分为optimizergenerate


optimizer


我们先分析optimize,optimize的作用在于标记静态节点。什么意思呢?


在我们编写template模板的时候,可以使用指令如v-if,动态属性如:name,或者动态文本{{}}。我们知道当数据改变时会重新渲染,实际上是会重新执行整个template函数。


所以编译的时候做了一个优化,在optimizer会标记静态节点,也就是没有和数据有绑定的节点,在第一次渲染时就保存渲染结果,再次执行整个template时可以不用重复执行这一部分,而是使用上次渲染结果就行。


我们来看看源码部分

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)
}
复制代码


分为两步,第一步是标记静态节点,第二步是标记静态根节点。我们挑选第一步来分析。

function markStatic (node: ASTNode) {
  // 1
  node.static = isStatic(node)
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    // 2
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
  }
}
复制代码


function isStatic (node: ASTNode): boolean {
  // 如果是表达式就是非静态节点
  if (node.type === 2) { // expression
    return false
  }
  // 如果是纯文本则是静态节点
  if (node.type === 3) { // text
    return true
  }
  // 提取node数据进行判断
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}
复制代码

  1. 根据node数据判断是否静态节点且标记

  2. 遍历子节点标记静态节点,当子节点有非静态节点时说明父节点也是非静态节点,所以会更新父节点为非静态节点。


第二步则是标记静态节点的根节点,什么意思呢?加入有A->B两个节点,AB都是静态节点,A是B的父节点,则会标记A为staticRoot,这边就不做进一步分析了,实现过程大同小异。


generate


经过optimize后,得到的AST就可以用于生成渲染函数render了。我们来看看函数的实现generate


const code = generate(ast, options)
复制代码
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  // fix #11483, Root level <script> tags should not be rendered.
  const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
复制代码


generate其实就是将ast通过递归的方式生成函数的代码字符串,后面再通过new Function()的方式执行。



在生成代码的过程中会使用许多辅助函数,如_c等,他们是在渲染模板中定义的,在这边将其拼接到代码串中,在执行的时候就可以调用相应的函数。我们可以看看他们的定义,从相关的名字就可以看出个大概


instance/render-helpers/index.js

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}
复制代码


我们接着看看genElement的实现

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }
  // 1
  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 {
      // 2
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }
      // 3
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      // 4
      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)
    }
    // 5
    return code
  }
复制代码


我们在上面将其分为5步进行分析


  1. genStaticgenFor等。和我们之前在parse中分析的流程很想,就是根据节点中存在的不同指令属性等通过分类处理的方式实现不同处理。

  2. genData根据el属性进行数据准备,实际生成个节点对象,也就是我们在渲染中分析的data

  3. 通过genChildren遍历子节点实现递归genElement

  4. 将前面生成的节点数据data,和子节点children拼接生成本节点解析的字符串,实际将作为_c的参数。_c其实就是render函数的第一个参数createElement,我们之前在渲染中有分析过。

  5. 返回前面拼接好的code


上面的过程我们挑选几个关键实现来分析,如genStaticgenForgenData


genStatic

function genStatic (el: ASTElement, state: CodegenState): string {
  el.staticProcessed = true
  const 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' : ''
  })`
}
复制代码


genStatic实际就是利用我们上一步optimize得到的静态根,通过其标识静态节点,在静态节点中将generate独立出来,这样就可以在后面单独缓存并使用了。


genFor


我们再看个指令的例子

export function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  // ...
  el.forProcessed = true // avoid recursion
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}
复制代码


可以看到,遇到指令的时候就会拼接_l及以指令表达式及变量为参数的函数,我们这边不去深究在渲染时候此段代码的实际运行,我们只需要知道对于不同指令的处理,其实就是拼接不同的函数或者代码就行。


我们在上面可以看到重复调用genElement,因为我们在genElement那边会将处理过的指令或者属性标记,所以不会重复调用同一逻辑导致暴栈,而是会走genElement的后面逻辑。


genData


genData就是通过节点属性来生成节点数据,就是我们后面渲染中的data数据。我们知道其是个对象,所以我们这边也要拼接个对象字符串才对。


函数的代码比较长,这边就不贴了。实际大多都是if的判断逻辑,我们列举其中几个就好。

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  // 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)},`
  }
  // ...
  if (el.dynamicAttrs) {
    data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
  }
  return data
}
复制代码


实例


我们来看看编译前面的例子吧

<div>
  <div v-if="isShow" @click="doSomething" :class="activeClass">text{{name}}{{value}}text</div>
  <div v-for="item in 10"></div>
  <div name="1"><p>33</p></div>
</div>
复制代码
{
  render: `with(this){return _c('div',[(isShow)?_c('div',{cla…ction(item){return _c('div')}),_v(" "),_m(0)],2)}`,
  staticRenderFns: [`with(this){return _c('div',{attrs:{\"name\":\"1\"}},[_c('p',[_v(\"33\")])])}`]
}
复制代码


总结


本篇文章分析了编译的后两步optimizegenerate,大概就是梳理了下其实现逻辑及原理,很多细节如指令,属性等的实现我们没有去深入分析,这部分可能等后面结合具体渲染逻辑分析比较好,我们在此就了解其主要流程步骤及实现原理就行。



相关文章
|
2天前
|
移动开发 JavaScript 前端开发
vue源码如何学习?
【4月更文挑战第20天】Vue.js源码学习概要:首先,需深入了解Vue基础知识、JavaScript(ES6+)及Node.js+npm。从GitHub克隆Vue源码仓库,安装依赖并构建。学习路径始于`entry-runtime-with-compiler.js`,然后深入`core/observer`、`vdom`、`renderer`和`instance`模块,理解响应式系统、虚拟DOM、渲染及实例创建。此外,研究`src/compiler`以了解模板编译。学习过程需耐心阅读、注解代码,结合相关资源辅助理解。
24 0
|
2天前
|
JavaScript
|
9月前
|
JavaScript
vue入门之编译项目
vue入门之编译项目
131 0
|
2天前
|
JavaScript
【Error】解决 Vue2.x 与最新 Ant Design vue 版本不符的问题
【Error】解决 Vue2.x 与最新 Ant Design vue 版本不符的问题
|
10月前
|
JavaScript IDE API
vue3的setup的使用和原理解析
vue3的setup的使用和原理解析
248 0
|
11月前
|
设计模式 JavaScript 前端开发
Vue开篇-vue指令(第一天)
Vue开篇-vue指令(第一天)
37 0
|
缓存 资源调度 JavaScript
Vue实战【Vue项目开发时常见的几个错误】
Vue实战【Vue项目开发时常见的几个错误】
153 1
|
JavaScript 前端开发 算法
vue3 源码学习,实现一个 mini-vue(十四):构建 compile 编译器(上)
vue3 源码学习,实现一个 mini-vue(十四):构建 compile 编译器(上)
vue3 源码学习,实现一个 mini-vue(十四):构建 compile 编译器(上)
|
API
VUE3知识点总结 之 setup(一)
VUE3知识点总结 之 setup(一)
158 0
VUE3知识点总结 之 setup(一)
|
JavaScript
怎么写一个vue插件
怎么写一个vue插件
80 0