第十五章:运行时 + 编译时 - 合并 vue 处理逻辑
01:前言
到目前位置我们已经完成了:
- 响应性
- 运行时
- 编辑器
三大模块。这三大模块基本上描述了 vue
的核心业务逻辑。
但是对于目前而言,这三大模块还是完全独立的系统。比如:如果想要渲染,那么必须要单独的导入 render
。
如有大家有过 vue 3
的使用经验,那么我们知道,当我们构建一个 vue 3
实例时,可以这么做:
<script>
const { createApp } = Vue
const APP = {
template: `<div>hello world</div>`
}
const app = createApp(APP)
app.mount('#app')
</script>
那么这里就会涉及到两个方法:
createApp
:创建app
实例mount
:挂载
通过这两个方法,我们就可以直接关联上 运行时 + 编译器,直接实现模板的渲染。
咱们的框架目前还不支持这样的使用方式,所以本章我们就要去实现这个功能。
对于上述的功能我们需要分成两块来看:
构建
createApp
通过render
进行渲染:html<script> const { createApp, h } = Vue const APP = { render() { return h('div', 'hello world') } } const app = createApp(APP) app.mount('#app') </script>
绑定
compiler
,直接通过模板渲染:html<script> const { createApp } = Vue const APP = { template: `<div>hello world</div>` } const app = createApp(APP) app.mount('#app') </script>
那么明确好了以上内容之后,接下来我们就分别去进行对应的渲染。
02:基于 render 渲染的 createApp 的构建逻辑
本小节我们先完成第一步,最终期望的渲染逻辑为:
<script>
const { createApp, h } = Vue
// 构建组件实例
const APP = {
render() {
return h('div', 'hello world')
}
}
// 通过 createAPP 标记挂载组件
const app = createApp(APP)
// 挂载位置
app.mount('#app')
</script>
对于以上代码而言,createApp
和 mount
这两个方法我们是不熟悉的,我们可以先看下之前的渲染逻辑,然后倒推一下 createAPP
和 mount
都做了什么。
以下为之前的渲染逻辑:
<script>
const { h, render } = Vue
const component = {
render() {
return h('div', 'hello component')
}
}
// 生成 vnode
const vnode = h(component)
// 挂载
render(vnode, document.querySelector('#app'))
</script>
由以上代码我们知道,想要挂载一个组件,那么必须经历 生成 vnode
、render
挂载 的过程。
那么对比两次的实例,我们由此可以推断出:
createApp
中,必然要生成对应的vnode
mount
方法,必然要触发render
,生成vnode
明确好了这样的逻辑之后,下面我们去实现对应的实现代码就比较简单了。
在
packages/runtime-dom/src/index.ts
中构建createApp
方法:js/** * 创建并生成 app 实例 */ export const createApp = (...args) => { const app = ensureRenderer().createApp(...args) return app }
其中
ensureRenderer
方法会返回一个renderer
实例,我们之前实现过对应的代码,可以看一下:jsexport function createRenderer(options: RendererOptions) { return baseCreateRenderer(options) } function baseCreateRenderer(options: RendererOptions): any { .... return { render } }
不知道大家还记不记得,之前我们在实现
baseCreateRenderer
时,返回的对象中,其实需要包含三个属性:jsreturn { render, hydrate, createApp: createAppAPI(render, hydrate) }
我们之前只实现了一个
render
,那么现在是时候实现createApp
了。
创建
packages/runtime-core/src/apiCreateApp.ts
模块,实现createAppAPI
函数:js/** * 创建 app 实例,这是一个闭包函数 */ export function createAppAPI<HostElement>(render) { return function createApp(rootComponent, rootProps = null) { const app = { _component: rootComponent, _container: null, // 挂载方法 mount(rootContainer: HostElement): any { // 直接通过 createVNode 方法构建 vnode const vnode = createVNode(rootComponent, rootProps) // 通过 render 函数进行挂载 render(vnode, rootContainer) } } return app } }
在
baseCreateRenderer
中,配置createAPP
属性:jsreturn { render, createApp: createAppAPI(render) }
那么此时我们就已经得到了 createApp
方法。
但是此时,我们虽然已经可以通过 createApp
方法获取到 app
实例了,但是还存在一个问题,那就是 mount
挂载时,我们期望传递一个 #app
的字符串,但是我们查看 mount
函数,会发现他期望得到的应该是一个 element
对象。
所以我们需要对 mount
进行重构:
在
packages/runtime-dom/src/index.ts
的createApp
方法中:jsexport const createApp = (...args) => { const app = ensureRenderer().createApp(...args) // 获取到 mount 挂载方法 const { mount } = app // 对该方法进行重构,标准化 container,在重新触发 mount 进行挂载 app.mount = (containerOrSelector: Element | string) => { const container = normalizeContainer(containerOrSelector) if (!container) return mount(container) } return app } /** * 标准化 container 容器 */ function normalizeContainer(container: Element | string): Element | null { if (isString(container)) { const res = document.querySelector(container) return res } return container }
那么至此,我们就成功的完成了
mount
挂载操作。
接下来,我们就导出 createApp
函数,以便直接通过 const {} = Vue
的形式进行访问。
查看
packages/vue/src/index.ts
模块,我们可以发现现在已经导出了很多方法了,所以我们可以直接使用*
通配符,简化一下对应代码量:jsexport * from '@vue/reactivity' export * from '@vue/runtime-core' export * from '@vue/runtime-dom' export * from '@vue/vue-compat' export * from '@vue/shared'
至此,整个 render
渲染逻辑完成。
03:基于 template 渲染的 createApp 的构建逻辑
对于组件而言,我们不光要支持 render
还需要支持 template
的模板渲染:
<script>
const { createApp, h } = Vue
const APP = {
template: `<div>hello world</div>`
}
const app = createApp(APP)
app.mount('#app')
</script>
但是如果现在我们尝试运行这个测试实例的话,那么将会得到一个对应的错误:
查看我们当前的代码,我们可以发现:
- 在
createAppAPI
中的mount
函数中,我们直接触发了createVNode
,传递了当前的组件实例,从而得到vnode
。 - 但是我们知道,我们当初是先实现了
render
,后实现了compiler
,这也就意味着在renderer
渲染器中,是不存在 模板解析器的。 - 所以也就意味着,
createVNode
将无法解析template
模板。
那么这样应该怎么做呢?
template
模板的解析,必然需要依赖于 compiler
,所以这就意味着,我们需要在 renderer
中导入 compiler
才可以。
在
packages/runtime-core/src/component.ts
中,创建registerRuntimeCompiler
方法,获取compile
实例js/** * 编辑器实例 */ let compile /** * 用来注册编译器的运行时 */ export function registerRuntimeCompiler(_compile: any) { compile = _compile }
接下来我们需要在
finishComponentSetup
,判断当前组件是template
还是render
,从而通过不同的方式进行渲染:jsexport function finishComponentSetup(instance) { const Component = instance.type // 组件不存在 render 时,才需要重新赋值 if (!instance.render) { + // 存在编辑器,并且组件中不包含 render 函数,同时包含 template 模板,则直接使用编辑器进行编辑,得到 render 函数 + if (compile && !Component.render) { + if (Component.template) { + // 这里就是 runtime 模块和 compile 模块结合点 + const template = Component.template + Component.render = compile(template) + } + } // 为 render 赋值 instance.render = Component.render } // 改变 options 中的 this 指向 applyOptions(instance) }
最后我们只需要在
packages/vue-compat/src/index.ts
中注册compile
即可:js/** * 注册 compiler */ registerRuntimeCompiler(compileToFunction)
此时,再次运行测试实例,实例可正常运行。
04:总结
到这里我们已经成功的完成了 运行时 + 编译器 的合并逻辑,那么我们可以运行如下测试实例,来查看一个相对完整的测试实例:
<script>
const { createApp } = Vue
const APP = {
template: `<div>hello world, <h1 v-if="isShow">{{ msg }}</h1></div>`,
data() {
return {
msg: '你好,世界',
isShow: false
}
},
created() {
setTimeout(() => {
this.isShow = true
}, 2000);
}
}
const app = createApp(APP)
app.mount('#app')
</script>