VueStudyNote
VueStudyNote copied to clipboard
03 生成render函数
取得了语法树之后,分三步。
- 生成render函数字符串
- 生成render函数
- 如何调用
生成函数字符串
我们使用 snabbdom
来产生和处理我们的虚拟dom,所以我们需要把AST生成类似下面的render函数字符串。
产生类似这样的snabbdom函数字符串
h('div#container.two.classes', { on: { click: someFn } }, [
h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
h('h1', strVar),
'this is string',
h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
])
这里只有一个h函数,实际上VUE有多个函数,分别为
- _c 是 createElement(创建元素),
- _m 是 renderStatic(渲染静态节点),
- _v 是 createTextVNode(创建文本dom),
- _s 是 toString (转换为字符串)
- _f :
filter函数
,如message | capitalize | wrap('-')
会编译成_f("wrap")(_f("capitalize")(message),'-')
这里的h相当于vue的 _c。
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
}
现在还没有处理class和attribute,所以代码很简单,一个递归即可。
处理字符串类型的时候,需要把它解析为变量。就是
变量1:{{message}}。
需要处理为
"变量1:"+this.message+"。"
最后的this我们提取到外面 with(this)
, 调用的时候用 call
绑定this到我们自己的实例上。
使用的 正则表达式。代码来自vue
export function parseText(
text: string,
re: string
) {
if (!re.test(text)) {
return
}
const tokens = []
let lastIndex = re.lastIndex = 0
let match, index, tokenValue
while ((match = re.exec(text))) {
index = match.index
// push text token
if (index > lastIndex) {
tokenValue = text.slice(lastIndex, index)
tokens.push(JSON.stringify(tokenValue))
}
// tag token
var exp = match[1].trim()
tokens.push(exp)
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokenValue = text.slice(lastIndex)
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
}
}
所以根据AST语法树生成render函数字符串的 递归 处理代码如下:
function createRenderStr(ast: ASTNode): string {
let str: string = ""
if (ast.type == 1) {
str = createRenderStrElemnet(ast)
} else if (ast.type == 3) {
str = createRenderStrText(ast)
} else {
warn(`wrong type:${ast.type}`)
}
return str
}
function createRenderStrElemnet(node: any): string {
log('createRenderStrElemnet', node)
let str: string = 'h(' + JSON.stringify(node.tag)
let attrs = node.attrsMap
if (attrs) {
str += ',{'
// why not use for..in, see eslint `no-restricted-syntax`
Object.keys(attrs).every(attrname => {
// str += JSON.stringify(attrname) + '=' + JSON.stringify(attrs[attrname]) + ' '
})
str += '}'
}
if (node.children) {
str += ',['
node.children.forEach(child => {
str += createRenderStr(child) + ','
})
str += ']'
}
str += ')'
return str
}
function createRenderStrText(node: any): string {
return node.text
}
相当简单清晰,有没有??
生成函数
使用 new Function,参考 这里
function renderToFunctions(renderStr: string): Function {
return new Function(`with(this){return ${renderStr}}`)
}
生成之后,保存到实例的 $render
上
const { render } = compileToFunctions(this.$options.template)
// save to this.$render
this.$render = render
如何调用
大家注意到,我们生成的函数是没有指定参数的。调用的时候使用call绑定当前的实例到this上,这就是为什么渲染函数前面有个 with(this)
// 新的虚拟节点
let vnode = vm.$render.call(proxy)
vue里面处理传入当前实例,还会把 $createElement
函数传入,一开始没有太明白,后面想应该是自定义渲染函数render函数的时候需要用到的。这个自定义render函数我们后面再支持,到时候再加,目前就一个当前实例即可。
// \vue\src\core\instance\render.js 的 _render 函数
//xiaowenjie 第二个参数,应该是给自定义render函数用的。
vnode = render.call(vm._renderProxy, vm.$createElement)
最终效果
测试代码 Xiao/example/helloworld.html
模板
template: '<h1>变量1:{{message}}。<br/>变量2:{{message2}}。</h1>',
生成的渲染函数字符串
h("h1",{},["变量1:"+message+"。",h("br",{},[]),"变量2:"+message2+"。",])
生成的render函数
render ƒ anonymous() {
with(this){return h("h1",{},["变量1:"+message+"。",h("br",{},[]),"变量2:"+message2+"。",])}
}