vue模版编译

模板编译:模板 => render函数,这个过程分两步:先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再使⽤AST⽣成渲染函数。
由于静态节点不需要总是重新渲染,所以在⽣成AST之后、⽣成渲染函数之前这个阶段,需要做⼀个操作,那就是遍历⼀遍AST,给所有静态节点做⼀个标记,这样在虚拟DOM中更新节点时,如果发现节点有这个标记,就不会重新渲染它。

在这里插入图片描述

所以,在⼤体逻辑上,模板编译分三部分内容:

  • 将模板解析为AST(解析器)
  • 遍历AST标记静态节点(优化器)
  • 使⽤AST⽣成渲染函数(代码⽣成器)
  1. 解析器:在解析器内部,分成了很多⼩解析器,其中包括过滤器解析器、⽂本
    解析器和HTML解析器。然后通过⼀条主线将这些解析器组装在⼀起,在使⽤模板时,我们可以在其中使⽤过滤器,⽽过滤器解析器的作⽤就是⽤来解析过滤器的。
    ⽂本解析器就是⽤来解析⽂本的。你可能会问,⽂本就是⼀段⽂字,有什么好解析的?其实⽂本解析器的主要作⽤是⽤来解析带变量的⽂本,什么是带变量的⽂本?下⾯这段代码中的name 就是变量,⽽这样的⽂本叫作带变量的⽂本:Hello {{ name }},不带变量的⽂本是⼀段纯⽂本,不需要使⽤⽂本解析器来解析。最后也是最重要的是HTML解析器,它是解析器中最核⼼的模块,它的作⽤就是解析模板,每当解析到HTML标签的开始位置、结束位置、⽂本或者注释时,都会触发钩⼦函数,然后将相关信息通过参数传递出来。主线上做的事就是监听HTML解析器。每当触发钩⼦函数时,就⽣成⼀个对应的AST节点。⽣成AST前,会根据类型使⽤不同的⽅式⽣成不同的AST。例如,如果是⽂本节点,就⽣成⽂本类型的AST。这个AST其实和vnode有点类似,都是使⽤JavaScript中的对象来表⽰节
    点。当HTML解析器把所有模板都解析完毕后,AST也就⽣成好了。

  2. 优化器:优化器的⽬标是遍历AST,检测出所有静态⼦树(永远都不会发⽣变化的DOM节点)并给其打标记。例如: <p>我是静态节点,我不需要发⽣变化</p>,在上⾯的代码中,p 标签就是⼀个静态节点,它没有使⽤任何变量,所以⼀旦⾸次渲染完毕后,⽆论状态怎么变,这个节点都不需要重新渲染。当AST中的静态⼦树被打上标记后,每次重新渲染时,就不需要为打上标记的静态节点创建新的虚拟节点,⽽是直接克隆已存在的虚拟节点。在虚拟DOM的更新操作中,如果发现两个节点是同⼀个节点,正常情况下会对这两个节点进⾏更新,但是如果这两个节点是静态节点,则可以直接跳过更新节点的流程。总体来说,优化器的主要作⽤是避免⼀些⽆⽤功来提升性能。因为静态节点除了⾸次渲染,后续不需要任何重新渲染操作。标记优化 对静态语法做静态标记 markup(静态节点如div下有p标签内容不会变化) diff来做优化 静态节点跳过diff操作
    Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用
    等待后续节点更新,如果是静态的,不会在比较children了

  3. 代码⽣成器:是模板编译的最后⼀步,它的作⽤是将AST转换成渲染函数中的内容,这个内容可以称为“代码字符串”。例如,⼀个简单的模板:<p title="Berwin" @click="c">1</p>,⽣成后的代码字符串是:with(this){return _c('p',{attrs:{"title":"Berwin"},on:{"click":c}},[_v("1")])},这样⼀个代码字符串最终导出到外界使⽤时,会将代码字符串放到函数⾥,这个函数叫作渲染函数。当渲染函数被导出到外界后,模板编译的任务就完成了。那么,如何将代码字符串放到函数⾥?举个例⼦:

    const code = `with(this){return 'Hello Berwin'}`
    const hello = new Function(code)
    hello()
    // "Hello Berwin"
    

已知template内容

<div id="app">
    <h1 key1="key1">
        h1-h1
        <h3 key3="key3">{{a}}</h3>
    </h1>
    <h2 key2="key2">{{b}}</h2>
</div>

打印内容

var ast = parse(template.trim(), options);   //console.log(ast)
var code = generate(ast, options);

ast内容如下

{type: 1, tag: "div", attrsList: Array(1), attrsMap: {}, rawAttrsMap: {},}
attrs: [{name: "id", value: "app", dynamic: undefined, start: 5, end: 13}]
attrsList: [{name: "id", value: "app", start: 5, end: 13}]
attrsMap: {id: "app"}
children: (4) [{}, {}, {}, {}] 展开在下面
end: 132
parent: undefined
plain: false
rawAttrsMap: {id: {name: "id", value: "app", start: 5, end: 13}}
start: 0
static: false
staticRoot: false
tag: "div"
type: 1

上面的children属性展开

0: {type: 1, tag: "h1", attrsList: Array(1), attrsMap: {}, rawAttrsMap: {},}
1: {type: 1, tag: "h3", attrsList: Array(1), attrsMap: {}, rawAttrsMap: {},}
2: {type: 3, text: " ", start: 89, end: 99, static: true}
3: {type: 1, tag: "h2", attrsList: Array(1), attrsMap: {}, rawAttrsMap: {},}

generate函数

function generate (
  ast,
  options
) {
  var state = new CodegenState(options);
  var code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")';
  return {
    render: ("with(this){return " + code + "}"),
    staticRenderFns: state.staticRenderFns
  }
}

源代码 对照 code代码

<div id="app">
    <h1 key1="key1">
        h1-h1
        <h3 key3="key3">{{a}}</h3>
    </h1>
    <h2 key2="key2">{{b}}</h2>
</div>

_c('div',
    { attrs:{"id":"app"} },
    [ _c('h1',{attrs:{"key1":"key1"}},[_v("\n  h1-h1\n  ")]),_c('h3',{attrs:{"key3":"key3"}},[_v(_s(a))]),_v(" "),
        _c('h2',{attrs:{"key2":"key2"}},[_v(_s(b))])
    ]
)

渲染函数的作⽤是创建vnode。渲染函数之所以可以⽣成vnode,是因为代码字符串中会有很多函数调⽤(例如,上⾯⽣成的代码字符串中有两个函数调⽤ _c 和 _v ),这些函数是虚拟DOM提供的创建vnode的⽅法。vnode有很多种类型,不同的类型对应不同的创建⽅法,所以代码字符串中的 _c 和 _v 其实都是创建vnode的⽅法,只是创建的vnode的类型不同。例如,_c 可以创建元素类型的vnode,⽽ _v可以创建⽂本类型的vnode

解析器

解析器的作⽤:
解析器要实现的功能是将模板解析成AST。
例如:

< div >
    <p>{{ name }}</p>
</div >
// 上⾯的代码是⼀个⽐较简单的模板,它转换成AST后的样⼦如下:
{
    tag: "div"
    type: 1,
    staticRoot: false,
    static: false,
    plain: true,
    parent: undefined,
    attrsList: [],
    attrsMap: {},
    children: [
        {
            tag: "p"
            type: 1,
            staticRoot: false,
            static: false,
            plain: true,
            parent: { tag: "div", ...},
            attrsList: [],
            attrsMap: {},
            children: [{
                type: 2,
                text: "{{name}}",
                static: false,
                expression: "_s(name)"
            }]
        }
    ]
}

其实AST并不是什么很神奇的东⻄,不要被它的名字吓倒。它只是⽤JavaScript中的对象来描述⼀个节点,⼀个对象表⽰⼀个节点,对象中的属性⽤来保存节点所需的各种数据。⽐如parent 属性保存了⽗节点的描述对象,children 属性是⼀个数组,⾥⾯保存了⼀些⼦节点的描述对象。再⽐如,type 属性表⽰⼀个节点的类型等。当很多个独⽴的节点通过parent 属性和children 属性连在⼀起时,就变成了⼀个树,⽽这样⼀个⽤对象描述的节点树其实就是AST
解析器内部运⾏原理:
事实上,解析器内部也分了好⼏个⼦解析器,⽐如HTML解析器、⽂本解析器以及过滤器解析器,其中最主要的是HTML解析器。顾名思义,HTML解析器的作⽤是解析HTML,它在解析HTML的过程中会不断触发各种钩⼦函数。这些钩⼦函数包括开始标签钩⼦函数、结束标签钩⼦函数、⽂本钩⼦函数以及注释钩⼦函数。
伪代码如下:

parseHTML(template, {
    start(tag, attrs, unary) {
        // 每当解析到标签的开始位置时,触发该函数
    },
    end() {
        // 每当解析到标签的结束位置时,触发该函数
    },
    chars(text) {
        // 每当解析到⽂本时,触发该函数
    },
    comment(text) {
        // 每当解析到注释时,触发该函数
    }
})

你可能不能很清晰地理解,下⾯我们举个简单的例⼦:

<div><p>我是Berwin</p></div>

当上⾯这个模板被HTML解析器解析时,所触发的钩⼦函数依次是:start 、start 、chars 、end 和end 。也就是说,解析器其实是从前向后解析的。解析到 <div> 时,会触发⼀个标签开始的钩⼦函数start ;然后解析到 <p> 时,⼜触发⼀次钩⼦函数start ;接着解析到我是Berwin 这⾏⽂本,此时触发了⽂本钩⼦函数chars ;然后解析到 </p> ,触发了标签结束的钩⼦函数end ;接着继续解析到 </div> ,此时⼜触发⼀次标签结束的钩⼦函数end ,解析结束。因此,我们可以在钩⼦函数中构建AST节点。在start 钩⼦函数中构建元素类型的节点,在chars 钩⼦函数中构建⽂本类型的节点,在comment 钩⼦函数中构建注释类型的节点。
HTML解析器不再触发钩⼦函数时,就说明所有模板都解析完毕,所有类型的节点都在钩⼦函数中构建完成,即AST构建完成。我们发现,钩⼦函数start 有三个参数,分别是tag 、attrs 和unary ,它们分别说明标签名、标签的属性以及是否是⾃闭合标签。⽽⽂本节点的钩⼦函数chars 和注释节点的钩⼦函数comment 都只有⼀个参数,只有text 。这是因为构建元素节点时需要知道标签名、属性和⾃闭合标识,⽽构建注释节点和⽂本节点时只需要知道⽂本即可。
什么是⾃闭合标签?举个简单的例⼦,input 标签就属于⾃闭合标签:<input type="text" /> ,⽽div 标签就不属于⾃闭合标签:<div></div>
在start 钩⼦函数中,我们可以使⽤这三个参数来构建⼀个元素类型的AST节点,例如:

function createASTElement(tag, attrs, parent) {
    return {
        type: 1,
        tag,
        attrsList: attrs,
        parent,
        children: []
    }
}
parseHTML(template, {
    start(tag, attrs, unary) {
        let element = createASTElement(tag, attrs, currentParent)
    }
})

在上⾯的代码中,我们在钩⼦函数start 中构建了⼀个元素类型的AST节点。
如果是触发了⽂本的钩⼦函数,就使⽤参数中的⽂本构建⼀个⽂本类型的AST节点,例如:

parseHTML(template, {
    chars(text) {
        let element = { type: 3, text }
    }
})

如果是注释,就构建⼀个注释类型的AST节点,例如:

parseHTML(template, {
    comment(text) {
        let element = { type: 3, text, isComment: true }
    }
})

源码溯源

export function compileToFunctions(template) {
  // 我们需要把html字符串变成render函数
  // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
  // 很多库都运用到了ast 比如 webpack babel eslint等等
  let ast = parse(template);
  // 2.优化静态节点:对ast树进行标记,标记静态节点
    if (options.optimize !== false) {
      optimize(ast, options);
    }

  // 3.通过ast 重新生成代码
  // 我们最后生成的代码需要和render函数一样
  // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
  // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
  let code = generate(ast);
  //   使用with语法改变作用域为this  之后调用render函数可以使用call改变this 方便code里面的变量取值
  let renderFn = new Function(`with(this){return ${code}}`);
  return renderFn;
}

模板编译

前置知识

  • 模板是vue开发中最常用的,即与使用相关联的原理
  • 它不是HTML,有指令、插值、JS表达式,能实现循环、判断,因此模板一定转为JS代码,即模板编译
  • 面试不会直接问,但会通过组件渲染和更新过程考察

模板编译

  • vue template compiler将模板编译为render函数
  • 执行render函数,生成vnode
  • 基于vnode在执行patchdiff
  • 使用webpack vue-loader插件,会在开发环境下编译模板

with语法

  • 改变{}内自由变量的查找规则,当做obj属性来查找
  • 如果找不到匹配的obj属性,就会报错
  • with要慎用,它打破了作用域规则,易读性变差

vue组件中使用render代替template

// 执行 node index.js

const compiler = require('vue-template-compiler')

// 插值
const template = `<p>{message}</p>`
with(this){return _c('p', [_v(_s(message))])}
// this就是vm的实例, message等变量会从vm上读取,触发getter
// _c => createElement 也就是h函数 => 返回vnode
// _v => createTextVNode 
// _s => toString 
// 也就是这样 with(this){return createElement('p',[createTextVNode(toString(message))])}

// h -> vnode
// createElement -> vnode

// 表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// 属性和动态属性
const template = `
    <div id="div1" class="container">
        <img :src="imgUrl"/>
    </div>
`
with(this){return _c('div',
     {staticClass:"container",attrs:{"id":"div1"}},
     [
         _c('img',{attrs:{"src":imgUrl}})])}

// 条件
const template = `
    <div>
        <p v-if="flag === 'a'">A</p>
        <p v-else>B</p>
    </div>
`
with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
const template = `
    <ul>
        <li v-for="item in list" :key="item.id">{{item.title}}</li>
    </ul>
`
with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
const template = `
    <button @click="clickHandler">submit</button>
`
with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// 从 vue 源码中找到缩写函数的含义
function installRenderHelpers (target) {
    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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值