vue html解释器,vue模板渲染--compile

模板渲染过程在实际使用vue的过程可能并不需要太深理解,但就vue来说,这些底层思想可以更好地让我们理解这个框架,以及了解为什么Vue的API要如此设计…

vue2+与vue1+的模板渲染过程完全不同,vue1使用的是DocumentFragment API,具体就不介绍了(可以直接跳到MDN去了解),而vue2开始则使用了Virtual DOM,基于Virtual DOM,vue2支持了服务端渲染SSR,以及JSX语法。介绍渲染流程之前,先说明两个数据结构:抽象语法树AST,以及VNode。

AST(抽象语法树)

AST 的全称是 Abstract Syntax Tree(抽象语法树),是源代码的抽象语法结构的树状表现形式,计算机学科中编译原理的概念。而vue就是将模板代码映射为AST数据结构,进行语法解析。这里采用了flow的语法,flow是一个JS静态类型检查工具。

在 Vue 中,ASTNode 分几种不同类型,关于 ASTNode 的定义在 flow/compile.js 里面,请看下图:

743166a8968c

看一下 Vue 2.0 源码中 AST 数据结构 的定义:

declare type ASTNode = ASTElement | ASTText | ASTExpression

declare type ASTElement = { // 有关元素的一些定义

type: 1;

tag: string;

attrsList: Array;

attrsMap: { [key: string]: string | null };

parent: ASTElement | void;

children: Array;

//......

}

declare type ASTExpression = {

type: 2;

expression: string;

text: string;

tokens: Array;

static?: boolean;

// 2.4 ssr optimization

ssrOptimizability?: number;

};

declare type ASTText = {

type: 3;

text: string;

static?: boolean;

isComment?: boolean;

// 2.4 ssr optimization

ssrOptimizability?: number;

};

我们用一个简单的例子来说明一下:

Latest Vue.js Commits

{{1 + 1}}

我们想一想这段代码会生成什么样的 AST 呢?

743166a8968c

VNODE数据结构

VNODE就是vue中的虚拟dom节点,VNODE 数据结构 如下:

constructor (

tag?: string,

data?: VNodeData,

children?: ?Array,

text?: string,

elm?: Node,

context?: Component,

componentOptions?: VNodeComponentOptions,

asyncFactory?: Function

) {

this.tag = tag

this.data = data

this.children = children

this.text = text

this.elm = elm

this.ns = undefined

this.context = context

this.fnContext = undefined

this.fnOptions = undefined

this.fnScopeId = undefined

this.key = data && data.key

this.componentOptions = componentOptions

this.componentInstance = undefined

this.parent = undefined

this.raw = false

this.isStatic = false

this.isRootInsert = true

this.isComment = false

this.isCloned = false

this.isOnce = false

this.asyncFactory = asyncFactory

this.asyncMeta = undefined

this.isAsyncPlaceholder = false

}

真实DOM存在什么问题,为什么要用虚拟DOM

我们为什么不直接使用原生 DOM 元素,而是使用真实 DOM 元素的简化版 VNode,最大的原因就是 document.createElement 这个方法创建的真实 DOM 元素会带来性能上的损失。我们来看一个 document.createElement 方法的例子

let div = document.createElement('div');

for(let k in div) {

console.log(k);

}

打开 console 运行一下上面的代码,会发现打印出来的属性多达 228 个,而这些属性有 90% 多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,关联着真实的dom,比如属性elm,只包括我们需要的属性,并新增了一些在 diff 过程中需要使用的属性,例如 isStatic。

模板渲染流程

先来一张图:

743166a8968c

首先从$mount开始,可以看到,mount其实就是拿到了html模板作为template,然后将这个template通过compileToFunctions方法编译成render函数:

if (template) {

/* istanbul ignore if */

if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

mark('compile')

}

const { render, staticRenderFns } = compileToFunctions(template, { //对获取到的template进行编译

shouldDecodeNewlines,

shouldDecodeNewlinesForHref,

delimiters: options.delimiters,

comments: options.comments

}, this)

options.render = render

options.staticRenderFns = staticRenderFns

/* istanbul ignore if */

if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

mark('compile end')

measure(`vue ${this._name} compile`, 'compile', 'compile end')

}

}

那么这个compileToFunctions做了什么呢?主要将 template 编译成 render 函数。首先读缓存,没有缓存就调用 compile 方法拿到 render 函数 的字符串形式,再通过 new Function 的方式生成 render 函数。

// 有缓存的话就直接在缓存里面拿

const key = options && options.delimiters

? String(options.delimiters) + template

: template

if (cache[key]) {

return cache[key]

}

const res = {}

const compiled = compile(template, options) // compile 后面会详细讲

res.render = makeFunction(compiled.render) //通过 new Function 的方式生成 render 函数并缓存

const l = compiled.staticRenderFns.length

res.staticRenderFns = new Array(l)

for (let i = 0; i < l; i++) {

res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])

}

......

}

return (cache[key] = res) // 记录至缓存中

现在我们具体看一下compile方法,上文中提到 compile 方法就是将 template 编译成 render 函数 的字符串形式。

export function compile (

template: string,

options: CompilerOptions

): CompiledResult {

const AST = parse(template.trim(), options) //1. parse

optimize(AST, options) //2.optimize

const code = generate(AST, options) //3.generate

return {

AST,

render: code.render,

staticRenderFns: code.staticRenderFns

}

}

这个函数主要有三个步骤组成:parse,optimize 和 generate,分别输出一个包含 AST,staticRenderFns 的对象和 render函数 的字符串。

parse 函数,主要功能是将 template字符串解析成 AST,采用了 jQuery 作者 John Resig 的 HTML Parser。前面定义了ASTElement的数据结构,parse 函数就是将template里的结构(指令,属性,标签等)转换为AST形式存进ASTElement中,最后解析生成AST。

optimize 函数(src/compiler/optimizer.js)主要功能就是标记静态节点,为后面 patch 过程中对比新旧 VNode 树形结构做优化。被标记为 static 的节点在后面的 diff 算法中会被直接忽略,不做详细的比较。

generate 函数(src/compiler/codegen/index.js)主要功能就是根据 AST 结构拼接生成 render 函数的字符串。

1. parse(解析器)

在 parse 函数中,我们先是定义了非常多的全局属性以及函数,然后调用了 parseHTML 这么一个函数,这也是 parse 最核心的函数,这个函数会不断的解析模板,填充 root,最后把 root(AST) 返回回去。

parseHTML

在这个函数中,最重要的是 while 循环中的代码,而在解析过程中发挥重要作用的有这么几个正则表达式。

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/

const ncname = '[a-zA-Z_][\\w\\-\\.]*'

const qnameCapture = `((?:${ncname}\\:)?${ncname})`

const startTagOpen = new RegExp(`^

const startTagClose = /^\s*(\/?)>/

const endTag = new RegExp(`^]*>`)

const doctype = /^^>]+>/i

const comment = /^

const conditionalComment = /^

Vue 通过上面几个正则表达式去匹配开始结束标签、标签名、属性等等。

关于 while 的详细注解我放在我仓库里了,有兴趣的可以去看看。

在 while 里,其实就是不断的去用 html.indexOf('

等于 0:这就代表这是注释、条件注释、doctype、开始标签、结束标签中的某一种

大于等于 0:这就说明是文本、表达式

小于 0:表示 html 标签解析完了,可能会剩下一些文本、表达式

parse 函数就是不断的重复这个工作,然后将 template 转换成 AST,在解析过程中,其实对于标签与标签之间的空格,Vue 也做了优化处理,有些元素之间的空格是没用的。

compile 其实要说要说非常多的篇幅,但是这里只能简单的理一下思路,具体代码还需要各位下去深扣。

2. optimize(优化器)

从代码中的注释我们可以看出,优化器的目的就是去找出 AST 中纯静态的子树:

1.把纯静态子树提升为常量,每次重新渲染的时候就不需要创建新的节点了

2.在 patch 的时候就可以跳过它们

optimize 的代码量没有 parse 那么多,我们来看看:

export function optimize (root: ?ASTElement, options: CompilerOptions) {

// 判断 root 是否存在

if (!root) return

// 判断是否是静态的属性

// 'type,tag,attrsList,attrsMap,plain,parent,children,attrs'

isStaticKey = genStaticKeysCached(options.staticKeys || '')

// 判断是否是平台保留的标签,html 或者 svg 的

isPlatformReservedTag = options.isReservedTag || no

// 第一遍遍历: 给所有静态节点打上是否是静态节点的标记

markStatic(root)

// 第二遍遍历:标记所有静态根节点

markStaticRoots(root, false)

}

下面两段代码我都剪切了一部分,因为有点多,这里就不贴太多代码了。

第一遍遍历

function markStatic (node: ASTNode) {

node.static = isStatic(node)

if (node.type === 1) {

...

}

}

其实 markStatic 就是一个递归的过程,不断地去检查 AST 上的节点,然后打上标记。

刚刚我们说过,AST 节点分三种,在 isStatic 这个函数中我们对不同类型的节点做了判断:

function isStatic (node: ASTNode): boolean {

if (node.type === 2) { // expression

return false

}

if (node.type === 3) { // text

return true

}

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)

))

}

可以看到 Vue 对下面几种情况做了处理:

当这个节点的 type 为 2,也就是表达式节点的时候,很明显它不是一个静态节点,所以返回 false

当 type 为 3 的时候,也就是文本节点,那它就是一个静态节点,返回 true

如果你在元素节点中使用了 v-pre 或者使用了

 标签,就会在这个节点上加上 pre 为 true,那么这就是个静态节点

如果它是静态节点,那么需要它不能有动态的绑定、不能有 v-if、v-for、v-else 这些指令,不能是 slot 或者 component 标签、不是我们自定义的标签、没有父节点或者元素的父节点不能是带 v-for 的 template、 这个节点的属性都在 type,tag,attrsList,attrsMap,plain,parent,children,attrs 里面,满足这些条件,就认为它是静态的节点。

接下来,就开始对 AST 进行递归操作,标记静态的节点,至于里面做了哪些操作,可以到上面那个仓库里去看,这里就不展开了。

第二遍遍历

第二遍遍历的过程是标记静态根节点,那么我们对静态根节点的定义是什么,首先根节点的意思就是他不能是叶子节点,起码要有子节点,并且它是静态的。在这里 Vue 做了一个说明,如果一个静态节点它只拥有一个子节点并且这个子节点是文本节点,那么就不做静态处理,它的成本大于收益,不如直接渲染。

同样的,我们在函数中不断的递归进行标记,最后在所有静态根节点上加上 staticRoot 的标记,关于这段代码也可以去上面的仓库看一看

3. generate(代码生成器)

在这个函数中,我们将 AST 转换成为 render 函数字符串,代码量还是挺多的,我们可以来看一看。

export function generate (

ast: ASTElement | void,

options: CompilerOptions

): CodegenResult {

// 这就是编译的一些参数

const state = new CodegenState(options)

// 生成 render 字符串

const code = ast ? genElement(ast, state) : '_c("div")'

return {

render: `with(this){return ${code}}`,

staticRenderFns: state.staticRenderFns

}

}

可以看到在最后代码生成阶段,最重要的函数就是 genElement 这个函数,针对 AST 的属性(不同的指令、属性),我们会选择不同的代码生成函数。最后我们按照 AST 生成拼接成一个字符串,如下所示:

export function genElement (el: ASTElement, state: CodegenState): string {

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

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 {

const data = el.plain ? undefined : 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

}

}

在 render 这个函数字符串中,我们会看到一些函数,那么这些函数是在什么地方定义的呢?我们可以在 core/instance/index.js 这个文件中找到这些函数:

// v-once

target._o = markOnce

// 转换

target._n = toNumber

//把一个值转换为字符串。(eg: {{data}})

target._s = toString

// v-for

target._l = renderList

// slot

target._t = renderSlot

// 是否相等

target._q = looseEqual

// 检测数组里是否有相等的值

target._i = looseIndexOf

// 渲染静态树

target._m = renderStatic

// 过滤器处理

target._f = resolveFilter

// 检查关键字

target._k = checkKeyCodes

// v-bind

target._b = bindObjectProps

// 创建文本节点

target._v = createTextVNode

// 创建空节点

target._e = createEmptyVNode

// 处理 scopeslot

target._u = resolveScopedSlots

// 处理事件绑定

target._g = bindObjectListeners

// 创建 VNode 节点

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

生成 render 的 generate 函数的输入也是 AST,它递归了 AST 树,为不同的 AST 节点创建了不同的内部调用方法,等待后面的调用。生成 render 函数的过程如下:

743166a8968c

假设我们有这么一段 template

{{val}}

xx.jpg

最终会被转换成这样子的函数字符串

{render: "with(this){return _c('div',{attrs:{"id":"test"}},[[_v(_s(val))]),_v(" "),_m(0)])}"}

以上就是 compile 函数中三个核心步骤的介绍,compile之后我们得到了 render 函数 的字符串形式,后面通过 new Function 得到真正的渲染函数。数据发现变化后,会执行 Watcher 中的 _update 函数(src/core/instance/lifecycle.js),_update 函数会执行这个渲染函数,输出一个新的 VNode 树形结构的数据。然后在调用 patch 函数,拿这个新的 VNode 与旧的 VNode 进行对比,只有发生了变化的节点才会被更新到真实 DOM 树上。

mount后续

通过compile生成render方法之后,会进一步执行mount方法,在$mount中可以看到最后一句话:return mount.call(this, el, hydrating),这个mount实际上就是runtime中的mount,执行的就是lifecycle中的mountComponent方法,看一下基本逻辑:

// 触发 beforeMount 生命周期钩子

callHook(vm, 'beforeMount')

let updateComponent //updateComponent是watcher更新时的回调,用于更新视图操作

/* istanbul ignore if */

if (process.env.NODE_ENV !== 'production' && config.performance && mark) {

updateComponent = () => {

const name = vm._name

const id = vm._uid

const startTag = `vue-perf-start:${id}`

const endTag = `vue-perf-end:${id}`

mark(startTag)

const vnode = vm._render()

mark(endTag)

measure(`vue ${name} render`, startTag, endTag)

mark(startTag)

vm._update(vnode, hydrating)

mark(endTag)

measure(`vue ${name} patch`, startTag, endTag)

}

} else {

updateComponent = () => {

vm._update(vm._render(), hydrating)

}

}

// 以前是直接new Watch赋值给vm._watcher,现在这一步放到了watcher的构造函数中

// we set this to vm._watcher inside the watcher's constructor

// since the watcher's initial patch may call $forceUpdate (e.g. inside child

// component's mounted hook), which relies on vm._watcher being already defined

new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)

hydrating = false

// manually mounted instance, call mounted on self

// mounted is called for render-created child components in its inserted hook

if (vm.$vnode == null) {

vm._isMounted = true

callHook(vm, 'mounted')

}

return vm

首先会new一个watcher对象(主要是将模板与数据建立联系),在watcher对象创建后,会运行传入的方法 vm._update(vm._render(), hydrating) 。其中的vm._render()主要作用就是运行前面compiler生成的render方法,并返回一个vNode对象。vm.update() 则会对比新的 vdom 和当前 vdom,并把差异的部分渲染到真正的 DOM 树上。

patch

patch.js 就是新旧 VNode 对比的 diff 函数,主要是为了优化dom,通过算法使操作dom的行为降到最低,diff 算法来源于 snabbdom,是 VDOM 思想的核心。snabbdom 的算法为了 DOM 操作跨层级增删节点较少的这一目标进行优化,它只会在同层级进行, 不会跨层级比较。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值