Skip to content

第十五章:运行时 + 编译时 - 合并 vue 处理逻辑

01:前言

到目前位置我们已经完成了:

  1. 响应性
  2. 运行时
  3. 编辑器

三大模块。这三大模块基本上描述了 vue 的核心业务逻辑。

但是对于目前而言,这三大模块还是完全独立的系统。比如:如果想要渲染,那么必须要单独的导入 render

如有大家有过 vue 3 的使用经验,那么我们知道,当我们构建一个 vue 3 实例时,可以这么做:

html
<script>
  const { createApp } = Vue
  
  const APP = {
    template: `<div>hello world</div>`
  }

  const app = createApp(APP)
  app.mount('#app')
</script>

那么这里就会涉及到两个方法:

  1. createApp:创建 app 实例
  2. mount:挂载

通过这两个方法,我们就可以直接关联上 运行时 + 编译器,直接实现模板的渲染。

咱们的框架目前还不支持这样的使用方式,所以本章我们就要去实现这个功能。

对于上述的功能我们需要分成两块来看:

  1. 构建 createApp 通过 render 进行渲染:

    html
    <script>
      const { createApp, h } = Vue
      const APP = {
        render() {
          return h('div', 'hello world')
        }
      }
    
      const app = createApp(APP)
      app.mount('#app')
    </script>
  2. 绑定 compiler ,直接通过模板渲染:

    html
    <script>
      const { createApp } = Vue
      
      const APP = {
        template: `<div>hello world</div>`
      }
    
      const app = createApp(APP)
      app.mount('#app')
    </script>

那么明确好了以上内容之后,接下来我们就分别去进行对应的渲染。

02:基于 render 渲染的 createApp 的构建逻辑

本小节我们先完成第一步,最终期望的渲染逻辑为:

html
<script>
  const { createApp, h } = Vue
  // 构建组件实例
  const APP = {
    render() {
      return h('div', 'hello world')
    }
  }
	
  // 通过 createAPP 标记挂载组件
  const app = createApp(APP)
  // 挂载位置
  app.mount('#app')
</script>

对于以上代码而言,createAppmount 这两个方法我们是不熟悉的,我们可以先看下之前的渲染逻辑,然后倒推一下 createAPPmount 都做了什么。

以下为之前的渲染逻辑:

html
<script>
  const { h, render } = Vue

  const component = {
    render() {
      return h('div', 'hello component')
    }
  }
	// 生成 vnode
  const vnode = h(component)
  // 挂载
  render(vnode, document.querySelector('#app'))
</script>

由以上代码我们知道,想要挂载一个组件,那么必须经历 生成 vnoderender 挂载 的过程。

那么对比两次的实例,我们由此可以推断出:

  1. createApp 中,必然要生成对应的 vnode
  2. mount 方法,必然要触发 render,生成 vnode

明确好了这样的逻辑之后,下面我们去实现对应的实现代码就比较简单了。

  1. packages/runtime-dom/src/index.ts 中构建 createApp 方法:

    js
    /**
     * 创建并生成 app 实例
     */
    export const createApp = (...args) => {
    	const app = ensureRenderer().createApp(...args)
    
    	return app
    }
    1. 其中 ensureRenderer 方法会返回一个 renderer 实例,我们之前实现过对应的代码,可以看一下:

      js
      export function createRenderer(options: RendererOptions) {
      	return baseCreateRenderer(options)
      }
      
      function baseCreateRenderer(options: RendererOptions): any {
      	....
      	return {
      		render
      	}
      }
    2. 不知道大家还记不记得,之前我们在实现 baseCreateRenderer 时,返回的对象中,其实需要包含三个属性:

      js
      return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
      }
    3. 我们之前只实现了一个 render,那么现在是时候实现 createApp 了。

  2. 创建 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
    	}
    }
  3. baseCreateRenderer 中,配置 createAPP 属性:

    js
    return {
    	render,
    	createApp: createAppAPI(render)
    }

那么此时我们就已经得到了 createApp 方法。

但是此时,我们虽然已经可以通过 createApp 方法获取到 app 实例了,但是还存在一个问题,那就是 mount 挂载时,我们期望传递一个 #app 的字符串,但是我们查看 mount 函数,会发现他期望得到的应该是一个 element 对象。

所以我们需要对 mount 进行重构

  1. packages/runtime-dom/src/index.tscreateApp 方法中:

    js
    export 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
    }
  2. 那么至此,我们就成功的完成了 mount 挂载操作。

接下来,我们就导出 createApp 函数,以便直接通过 const {} = Vue 的形式进行访问。

  1. 查看 packages/vue/src/index.ts 模块,我们可以发现现在已经导出了很多方法了,所以我们可以直接使用 * 通配符,简化一下对应代码量:

    js
    export * 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 的模板渲染:

html
<script>
  const { createApp, h } = Vue
  const APP = {
    template: `<div>hello world</div>`
  }

  const app = createApp(APP)
  app.mount('#app')
</script>

但是如果现在我们尝试运行这个测试实例的话,那么将会得到一个对应的错误:

image-20230813162957348

查看我们当前的代码,我们可以发现:

  1. createAppAPI 中的 mount 函数中,我们直接触发了 createVNode,传递了当前的组件实例,从而得到 vnode
  2. 但是我们知道,我们当初是先实现了 render,后实现了 compiler,这也就意味着在 renderer 渲染器中,是不存在 模板解析器的。
  3. 所以也就意味着,createVNode 将无法解析 template 模板。

那么这样应该怎么做呢?

template 模板的解析,必然需要依赖于 compiler,所以这就意味着,我们需要在 renderer 中导入 compiler 才可以。

  1. packages/runtime-core/src/component.ts 中,创建 registerRuntimeCompiler 方法,获取 compile 实例

    js
     /**
      * 编辑器实例
      */
     let compile
     
     /**
      * 用来注册编译器的运行时
      */
     export function registerRuntimeCompiler(_compile: any) {
     	compile = _compile
     }
  2. 接下来我们需要在 finishComponentSetup,判断当前组件是 template 还是 render,从而通过不同的方式进行渲染:

    js
     export 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)
     }
  3. 最后我们只需要在 packages/vue-compat/src/index.ts 中注册 compile 即可:

    js
     /**
      * 注册 compiler
      */
     registerRuntimeCompiler(compileToFunction)

此时,再次运行测试实例,实例可正常运行。

04:总结

到这里我们已经成功的完成了 运行时 + 编译器 的合并逻辑,那么我们可以运行如下测试实例,来查看一个相对完整的测试实例:

js
<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>

Released under the MIT License.