「这是我参与2022首次更文挑战的第37天,活动详情查看:2022首次更文挑战」。
一、前情回顾 & 背景
本篇小作文讨论了另外两个常用的功能对应的渲染函数:自定义组件、动态组件;二者的处理都是处理成 _c(componentName, data, children)
的调用形式;
此时此刻尚在根实例编译阶段
,只有当根实例的编译
阶段结束,得到根实例的渲染函数
,接下来创建根实例
的渲染 watcher
就会调用根实例的渲染函数。
本篇小作文准备先来看看我们整个模板编译后得到的 render
函数主体全貌,接着我们看看渲染函数的生成;
二、根实例的渲染函数
我们这里一直讨论的都是根实例的渲染函数,有别于子实例的渲染函数;
2.1 test.html 模板
<div id="app">
<div class="staticR">
<article>hahahah</article>
</div>
<input :type="inputType" v-model="inputValue" />
<span v-for="item in someArr" :key="index">{{item}}</span>
<div v-if="inputType === 'checkbox'">inputType=checkBox</div>
<div v-else-if="inputType === 'radio'">inputType=radio</div>
<div v-else>inputType=something-else</div>
{{ msg }}
<some-com :some-key="forProp"></some-com>
<div>someComputed = {{someComputed}}</div>
<component is="someCom">
<div>component 的插槽</div>
</component>
<div class="static-div">静态节点</div>
</div>
2.2 根实例的 render 函数主体
上面的 2.1
的模板最终都会被编译成下面的 render 函数主体
,注意这个表述,严格意义上来说它还不是 render 函数
,只是主体,真的 render 函数
,需要被 with(this)
语句包裹;
"_c('div',{attrs:{\"id\":\"app\"}},[_m(0),_v(\" \"),((inputType)==='checkbox')?_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":\"checkbox\"},domProps:{\"checked\":Array.isArray(inputValue)?_i(inputValue,null)>-1:(inputValue)},on:{\"change\":function($event){var $$a=inputValue,$$el=$event.target,$$c=$$el.checked?(true):(false);if(Array.isArray($$a)){var $$v=null,$$i=_i($$a,$$v);if($$el.checked){$$i<0&&(inputValue=$$a.concat([$$v]))}else{$$i>-1&&(inputValue=$$a.slice(0,$$i).concat($$a.slice($$i+1)))}}else{inputValue=$$c}}}}):((inputType)==='radio')?_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":\"radio\"},domProps:{\"checked\":_q(inputValue,null)},on:{\"change\":function($event){inputValue=null}}}):_c('input',{directives:[{name:\"model\",rawName:\"v-model\",value:(inputValue),expression:\"inputValue\"}],attrs:{\"type\":inputType},domProps:{\"value\":(inputValue)},on:{\"input\":function($event){if($event.target.composing)return;inputValue=$event.target.value}}}),_v(\" \"),_l((someArr),function(item){return _c('span',{key:index},[_v(_s(item))])}),_v(\" \"),(inputType === 'checkbox')?_c('div',[_v(\"inputType=checkBox\")]):(inputType === 'radio')?_c('div',[_v(\"inputType=radio\")]):_c('div',[_v(\"inputType=something-else\")]),_v(\"\\n\\t\"+_s(msg)+\"\\n\\t\"),_c('some-com',{attrs:{\"some-key\":forProp}}),_v(\" \"),_c('div',[_v(\"someComputed = \"+_s(someComputed))]),_v(\" \"),_c(\"someCom\",{tag:\"component\"},[_c('div',[_v(\"component 的插槽\")])]),_v(\" \"),_c('div',{staticClass:\"static-div\"},[_v(\"静态节点\")])],2)"
上面这一大坨着实让人有点蒙圈,所以我手动格式化了一下,这个可是个体力活😂😂,这个比码字还痛苦;
先提示一下 _c
方法的作用:它用于创建元素,关注一下他的参数能更好的理解这个过程:
- 第一个参数是标签名,可以是自定义的组件或者动态组件名,也可以是
HTML
原生标签; - 第一个参数的行内属性,
attrs
- 第三个参数是一个数组,标识的是当前元素的子元素;
let renderFunctionsString = `_c( // 渲染 div#app
'div',
{ attrs: { "id": "app" } },// div#app 的行内属性
[ // 这个数组是 div#app 的子元素
_m(0),
_v(" "),
((inputType)==='checkbox') // 处理 <input :type="inputType"... /> 不同 type 的 v-model
? _c( // 如果 type 是 checkbox
'input',
{
directives: [{
name: "model",
rawName: "v-model",
value: (inputValue),
expression: "inputValue"
}],
attrs: { "type": "checkbox" },
domProps: { "checked": Array.isArray(inputValue)
? _i(inputValue,null) > -1
: (inputValue)
},
on: { "change": function ($event) {
// 如果 type 是 checkbox,绑定 change 事件
// 事件 handler 就是这个函数
var $$a = inputValue,
$$el = $event.target,
$$c = $$el.checked ? (true) : (false);
if (Array.isArray($$a)){
// 处理 checbox 绑定数组项
// 将绑定的值更新到数组中
var $$v = null,
$$i = _i($$a,$$v); // _i 是 indexOf
if ($$el.checked) {
$$i < 0 && (inputValue = $$a.concat([$$v]))
} else {
$$i > -1 && (inputValue = $$a.slice(0, $$i).concat($$a.slice($$i+1)))
}
} else {
// 非数组
inputValue = $$c
}
}
}
}
)
: ((inputType) === 'radio') // input type 是 radio 时
? _c(
'input',
{ directives: [{
name: "model",
rawName: "v-model",
value: (inputValue),
expression: "inputValue"
}],
attrs: { "type": "radio" },
domProps: { "checked": _q(inputValue, null) },
on: { "change": function($event) { inputValue = null } }
}
)
: _c(
'input',
{
directives:[{
name: "model",
rawName: "v-model",
value: (inputValue),
expression: "inputValue"
}],
attrs: { "type": inputType },
domProps:{
"value": (inputValue)},
on:{ "input": function ($event) {
if ($event.target.composing) return;
inputValue = $event.target.value
}
}
}
),
_v(" "),
_l( // 处理 <span v-for="item in someArr" :key="index">{{item}}</span>
(someArr),
function (item) {
return _c(
'span',
{ key:index },
[
_v(_s(item))
]
)
}
),
_v(" "),
(inputType === 'checkbox') // <div v-if="inputType === 'checkbox'">
? _c(
'div',
[
_v("inputType=checkBox")
]
)
: (inputType === 'radio') // <div v-else-if="inputType === 'radio'">
? _c(
'div',
[
_v("inputType=radio")
]
)
: _c( // <div v-else>
'div',
[
_v("inputType=something-else")
]
),
_v(\n\t"+_s(msg)+"\n\t"), // 渲染文本 {{ msg }}
_c( // 渲染 <some-com></some-com> 组件
'some-com',
{ attrs: { "some-key": forProp } }
),
_v(" "), // 空行
_c( // 渲染 <div>someComputed = {{someComputed}}</div>
'div',
[
_v("someComputed = "+_s(someComputed))
]
), //
_v(" "),
_c( // 渲染动态组件 <component is="someCom"> ...
"someCom",
{ tag: "component" },
[
_c( // 动态组件 component 标签的子元素的子元素
'div',
[
_v("component 的插槽")
]
)
]
),
_v(" "),
_c(
'div', // 渲染 <div class="static-div">静态节点</div>
{ staticClass: "static-div" },
[_v("静态节点")]
)
],
2
)`
这里缺少一个对照视图,建议把 test.html
中的模板代码和这个 render
函数体进行对照,效果会十分明显;
我敢保证,这是全网你距离 Vue
渲染函数最近的一次,写这个专栏的初衷就是让每个人都能看得懂 Vue
的源码,加油吧老铁们;
三、渲染函数
前面我们也称为上面第二个主题中说的渲染函数主体为渲染函数,这是个不严谨的过程,刚刚我们也提及了。本主题就是要说真正的渲染函数的产生过程;
3.1 包裹 with(this)
关于 generate
我们说了很懂,调用栈十分的深,到这里将 parse
阶段获得到的 ast
变成渲染函数体的过程已经结束。这个过程是 generate
的一个中间过程,后面的过程来自 generate
方法的后半部分,代码如下
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// 生成字符串格式的代码,比如 '_c(tag, data, children, normalizationType)'
const code = ast
? (ast.tag === 'script'
? 'null'
: genElement(ast, state))
: '_c("div")'
// 以下为生成 render 函数体的后续工作
// 这里 return 的对象,对前面的生成的 render 函数主体包裹 with 语句
// 把静态根节点的渲染函数提升
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
从上面的代码可以看到出来,生成 render
函数主体后,generate
返回一个对象,对象的 render
属性值就是上面那一大坨 _c(div, { id: "app" }, ...)
被包裹 with(this)
语句之后的字符串;
这里先提一下,为啥要包裹 with(this)
语句呢?其实前面也提到过,with(this)
语句的作用就是延长 js
的作用域链,使用 with(obj)
块中的变量可以查找到 obj
中的属性作值为变量的值;
这里使用 with(this)
,this
就是当前的 Vue
实例,所以后面 _c
执行时用到的变量先从 Vue
实例上取值,这也就是我们在模板中绑定数据时,例如<span v-for="someArr"/>
,someArr
此时绑定到 Vue
实例上的 someArr
,即 this.someArr
;
那么 this.someArr
又是哪里来的呢?
还记得前面讲响应式数据的时候,data、computed、props
最终都会代理到 vm
吗?如此一来,data,computed,props
都已经代理到 Vue
实例,这就大大简化了 Vue
的渲染函数处理工作;
试想一下,如果没有代理到 Vue 实例,那么我们写模板的时候就要写 data.someArr, computed.someArr, props.someArr
。
3.2 生成渲染函数
上面 3.1 的返回结果 { render, staticRenderFns }
返回,使得 comile
方法调用栈出栈,回到下面的 compileToFunctions
作用域中:
export function createCompileToFunctionFn (compile: Function): Function {
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
// compileToFunctions 作用域
// compile
// 执行编译函数,得到编译结果:{ render, renderStaticFns }
const compiled = compile(template, options)
const res = {}
// 转换编译得到的字符串代码为函数,通过 new Function(code) 实现
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
return createFunction(code, fnGenErrors)
})
// 缓存编译结果
return (cache[key] = res)
}
}
3.3 createFunction 方法
方法位置:src/compiler/to-function.js -> function createFunction
方法参数:
code
: 代码字符串errors
: 收集
方法作用:通过 new Function
传入字符串,创建函数
function createFunction (code, errors) {
try {
return new Function(code)
} catch (err) {
errors.push({ err, code })
return noop
}
}
3.4 render 函数成形调用栈依次出栈
所以经历过这个步骤之后,compileToFunctions
也会获取到最终的结果形如:
const res = {
render: function () {
return with (this) {
return _c('div', { id: 'app' }, [.....])
}
},
staticRenderFns: [....]
}
此时,compileToFunctions
的作用域也会在添加缓存之后出栈,回到 createCompiler
作用,将 compileToFunctions
返回结果作为 compileToFunctions
属性的值;
export function createCompilerCreator (baseCompile) {
return function createCompiler () {
function compile () {
const compiled = baseCompile(template.trim(), finalOptions)
return compiled
}
return {
compile,
// createCompileToFunctionFn(compile)->
// -> { render, staticRenderFns }
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
createCompiler
作用域(调用栈)返回 { compile, compileToFunctions }
以后也就退出了,这回退出到了哪里呢?
这回退出到 Vue.proptotype.$mount
方法上,终于回来了啊~~~
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ...
// 编译模板,得到动态渲染函数和静态渲染函数
// 这个对象就是上面一系列加工后得到的
const {
render,
staticRenderFns
} = compileToFunctions(template,...)
// 将两个渲染函数放到 vm.$options 上,
// 即 this.$options,给 Vue.prototype._render 方法用
options.render = render // funciton () { return with(this) {...} }
options.staticRenderFns = staticRenderFns
}
}
// 执行挂载
return mount.call(this, el, hydrating)
}
四、总结
本篇小作文我们给大家展示了根实例的最终得到的 render 函数主体
:是一个 _c()
的一个调用,他的第一个参数是标签名,第二个是行内属性,第三个是代表子元素的数组;
从这个 render 函数的主体
可以看出,他是一个递归调用的过程,最终完成顶层 div#app
的渲染工作;
获得 render 函数主体
之后,又经过 createFunction
得到最终的 render 函数
,接着就是一些列的返回结果并出栈,回到了 Vue.prototype.$mount
方法,将获得的 render、renderStaticFn
赋值到 this.$options
上,以备 Vue.prototype._render
调用进行挂载;