前端那些事儿

06Vue 源码解析3

2020-07-12  本文已影响0人  LM林慕

Vue 源码解析3

模板编译

模板编译的主要目标是将模板(template)转为渲染函数(render)

template==>render

模板编译必要性

Vue2.0 需要用到 Vnode 描述视图以及各种交互,手写显然不切实际,因此用户只需编写类似 HTML 代码的 Vue 模板,通过编译器将模板转换为可返回 Vnode 的 render 函数。

体验模板编译

带编译器的版本中,可以使用 template 或 el 的方式生命模板,测试 demo

(function anonymous (
  ) {
    with (this) {
      return _c('div', { attrs: { "id": "demo" } }, [_c('h1', [_v("Vue模板编
    译")]),_v(" "),_c('p',[_v(_s(foo))]),_v(" "),_c('comp')],1)}
    })

输出结果大致如下:

(function anonymous () {
  with (this) {
    return _c('div', { attrs: { "id": "demo" } }, [
      _c('h1', [_v("Vue模板编译")]),
      _v(" "), _c('p', [_v(_s(foo))]),
      _v(" "), _c('comp')], 1)
  }
})

整体流程

compileToFunctions

若指定 template 或 el 选项,则会执行编译,platforms\web\entry-runtime-with-compiler.js

编译过程

编译分为三步:解析、优化和生成,src\compiler\index.js

测试 demo

模板编译过程

image.png

实现模板编译共有三个阶段:解析、优化和生成。

解析 - parse

解析器将模板解析为抽象语法树,基于 AST 可以做优化或者代码生成工作。

调试查看得到的 AST,/src/compiler/parser/index.js,结构如下:

image.png

解析器内部分了 HTML 解析器、文本解析器和过滤解析器,最主要是 HTML 解析器。

优化 - optimize

优化器的作用是在 AST 中找出静态子树并打上标记。静态子树是在 AST 中永远不变的节点,如纯文本节点。

标记静态子树的好处:

测试 demo

代码实现,src/compiler/optimizer.js - optimize

标记结束

image.png

代码生成 - generage

将 AST 转换成渲染函数中的内容,即代码字符串。

generate 方法生成渲染函数代码,src/compiler/codegen/index.js

生成的 code:

`_c('div',{attrs:{"id":"demo"}},[
_c('h1',[_v("Vue.js测试")]),
_c('p',[_v(_s(foo))])
])`

典型指令的实现:v-if、v-for

image.png

着重观察几个结构性指令的解析过程。

解析 v-if:parser/index.js

processIf 用于处理 v-if 解析:

function processIf (el) {
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}

解析结果:

image.png

代码生成,codegen/index.js

genIfConditions 等用于生产条件语句相关代码。

生成结果:

"with(this){return _c('div',{attrs:{"id":"demo"}},[
(foo) ? _c('h1',[_v(_s(foo))]) : _c('h1',[_v("no title")]),
_v(" "),_c('abc')],1)}"

解析 v-for:parser/index.js

processFor 用于处理 v-for 指令:

export function processFor (el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const res = parseFor(exp)
    if (res) {
      extend(el, res)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `Invalid v-for expression: ${exp}`,
        el.rawAttrsMap['v-for']
      )
    }
  }
}

解析结果:v-for="item in items"

for:"items"

alias:"item"

image.png

代码生成,src\compiler\codegen\index.js

genFor 用于生成响应代码。

生成结果:

"with(this){return _c('div',{attrs:{"id":"demo"}},[_m(0),_v(" "),(foo)?_c('p',
[_v(_s(foo))]):_e(),_v(" "),
_l((arr),function(s){return _c('b',{key:s},[_v(_s(s))])})
,_v(" "),_c('comp')],2)}"

v-if、v-for 这些指令只能在编译器阶段处理,如果我们要在 render 函数处理条件或循环只能使用 if 和 for。

Vue.component('comp', {
  props: ['foo'],
  render (h) { // 渲染内容跟foo的值挂钩,只能⽤if语句
    if (this.foo == 'foo') {
      return h('div', 'foo')
    }
    return h('div', 'bar')
  }
})
(function anonymous (
) {
  with (this) {
    return _c('div', { attrs: { "id": "demo" } }, [_m(0), _v(" "), (foo) ? _c('p',
      [_v(_s(foo))]) : _e(), _v(" "), _c('comp')], 1)
  }
})

组件化机制

组件声明:Vue.component()

initAssetRegisters(Vue) src/core/global-api/assets.js,组件注册使用 extend 方法将配置转换为构造函数并添加到 components 选项。

组件实例创建及挂载

观察生成的渲染函数:

"with(this){return _c('div',{attrs:{"id":"demo"}},[
  _c('h1', [_v("虚拟DOM")]), _v(" "),
  _c('p', [_v(_s(foo))]), _v(" "),
  _c('comp') // 对于组件的处理并⽆特殊之处
  ], 1)}"

整体流程

首先创建的是跟实例,首次 _render() 时,会得到整棵树的 Vnode 结构,其中自定义组件相关的主要有:

整体流程:

new Vue() => $mount() => vm._render(h) => createElement() => createComponent() => patch => createElm => createComponent()

_createElement - src\core\vdom\create-element.js

_createElement 实际执行 Vnode 创建的函数,由于传入 tag 是非保留标签,因此判定为自定义组件通过 createComponent 去创建。

createComponent - src/core/vdom/create-component.js

创建组件 Vnode,保存了上一步处理得到的组件构造函数,props,事件等

创建组件实例

根组件执行更新函数时,会递归创建子元素和子组件,入口 createElm。

createEle() core/vdom/patch.js line751

首次执行 _update() 时,patch() 会通过 createEle() 创建根元素,子元素创建研究从这里开始。

createComponent core/vdom/patch.js line144

自定义组件创建:

// 组件实例创建、挂载
if (isDef(i = i.hook) && isDef(i = i.init)) {
  i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
  // 元素引⽤指定vnode.elm,元素属性创建等
  initComponent(vnode, insertedVnodeQueue)
  // 插⼊到⽗元素
  insert(parentElm, vnode.elm, refElm)
  if (isTrue(isReactivated)) {
    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  }
  return true
}

总结

Vue源码学习使我们能够深入理解原理,解答很多开发中的疑惑,规避很多潜在的错误,写出更好的代码。学习大神的代码,能够学习编程思想,设计模式,训练基本功,提升内力。

上一篇下一篇

猜你喜欢

热点阅读