【vue3系列】2.一步步实现vue3.0简易版的createApp功能

1,022 阅读4分钟

上篇文章我们了解vue3可以通过解构的方式引入createApp创建app实例,本来想直接分析源码,发现如果一开始不懂设计理念,实力又不足,直接看源码会被劝退的,已经被教育很多次了。

还是老老实实一步步来,这篇文章先来分析下createApp的简单版实现过程,后面再单独分析数据响应式(减轻学习负担)。

1. 基本结构

我们先看下使用方式,然后把这个功能当成需求来实现,通过逆向分析可能会更容易理解。

  const {createApp} = Vue;
  const app = createApp({
      setup() {
          return {
              title: 'hi,vue3'
          }
      }
  });
   app.mount('#app');

通过vue3使用实例(也可以理解为测试用例)看到createApp能够解构引入,我们大概可以猜出reateApp是Vue对象的一个方法。

再看该方法支持参入对象,并且返回的app可以调用mount方法。可见这个函数会返回一个mount方法,同时mount方法支持传入选择器字符串。

由此可推断基本结构应该为:

const Vue = {
    // options接收参数
    createApp(options) {
        return {
            // 返回一个mount方法
            mount(selector) {
                // xxx
            }
        }
    }
}

2. 页面渲染流程

createApp主要是返回一个应用程序实例app,页面渲染最终靠app.mount('#app')。mount方法是如何把传入的参数配置与dom相结合渲染页面的呢?

为了简化整个流程,我们先不考虑虚拟dom(其实无论是否有虚拟dom,最终页面渲染都会操作真实dom)。通过执行mount,传入了选择器会得到要渲染的根元素,在这之前需要找到要插入的元素。核心点如下:

2.1 获取渲染函数

主要目的是得到要渲染的dom元素,为了更好的扩展性,vue3.0支持自定义渲染函数,可通过options传入,如果不传会提供默认的渲染函数。继续完善基本结构:

const Vue = {
    // options接收参数
    createApp(options) {
        return {
            // 返回一个mount方法
            mount(selector) {
                // 获取根元素容器
                let parent = document.querySelector(selector);
                // 如果没有配置渲染函数,使用自定义的编译函数得到渲染函数
                if (!options.render) {
                    options.render = this.comiple(parent.innerHTML);
                }
            },
            // 编译模板得到渲染函数
            comiple(template) {
                return function render() {
                    // xxx
                }
            }
        }
    }
}

2.2 执行渲染函数

执行渲染函数将传入参数的配置和模板结合得到渲染的元素内容。继续完善基本结构:

const Vue = {
    // options接收参数
    createApp(options) {
        return {
            // 返回一个mount方法
            mount(selector) {
                // 获取根元素容器
                let parent = document.querySelector(selector);
                // 如果没有配置渲染函数,使用自定义的编译函数得到渲染函数
                if (!options.render) {
                    options.render = this.comiple(parent.innerHTML);
                }
                // 通过call执行渲染函数,并将setup的返回值作为this
                options.render.call(options.setup());
            },
            // 自定义渲染函数
            comiple(template) {
                // template暂时不解析
                return function render() {
                    // 先不考虑虚拟dom,直接用真实dom代替
                    const div = document.createElement('div');
                    // 这里的this就是setup函数的返回值
                    div.innderHTML = this.title;
                    return div
                }
            }
        }
    }
}

2.3 追加到根元素

在追加前需要清空根元素内容,然后再将处理好的模板元素进行追加

const Vue = {
    // options接收参数
    createApp(options) {
        return {
            // 返回一个mount方法
            mount(selector) {
                // 宿主元素
                let parent = document.querySelector(selector);
                // 如果没有配置渲染函数,使用自定义的编译函数得到渲染函数
                if (!options.render) {
                    options.render = this.comiple(parent.innerHTML);
                }
                // 通过call执行渲染函数,并将setup的返回值作为this
                const el = options.render.call(options.setup());
                // 清空根元素内容
                parent.innerHTML = '';
                // 将el追加到根元素
                parent.appendChild(el);
            },
            // 自定义渲染函数
            comiple(template) {
                // template暂时不处理
                return function render() {
                    // 先不考虑虚拟dom,直接用真实dom代替
                    const div = document.createElement('div');
                    // 这里的this就是setup函数的返回值
                    div.innderHTML = this.title;
                    return div
                }
            }
        }
    }
}

把之前html引入的vue3去掉,换成我们自己写的,下面是个完整可运行的html文件,浏览器可以看到页面渲染出了hi,vue3

<html>
    <body>
        <div id="app">
            <h3>{{title}}</h3>
        </div>
        <script>
            const Vue = {
                createApp(options) {
                    return {
                        mount(selector) {
                            const parent = document.querySelector(selector);
                            if (!options.render) {
                                // 如果没有配置渲染函数,使用自定义的编译函数得到渲染函数
                                options.render = this.compile(parent.innerHTML);
                            }
                            // 通过call执行函数,并将setup的返回值作为this
                            const el = options.render.call(options.setup());
                            // 清空宿主元素的内容
                            parent.innerHTML = '';
                            // 将el追加到宿主元素
                            parent.appendChild(el);
                        },
                        compile(template){
                            // template暂时不处理
                            return function render() {
                                const div = document.createElement('div');
                                // 这里的this就是setup函数的返回值
                                div.innerHTML = this.title;
                                return div
                            }
                        }
                    }
                }
            }
        </script>
        <script>
            const {createApp} = Vue;
            const app = createApp({
                setup() {
                    return {
                        title: 'hi,vue3'
                    }
                }
            });
            app.mount('#app');
        </script>
    </body>
</html>

3. 总结

上面步骤只是简易版的实现流程,可以帮助我们更好的理解整体流程和阅读源码。vue3源码其实做了很多工作,例如兼容vue2写法、虚拟dom节点处理、模板编译优化、渲染器和利用工厂函数来支持跨平台功能等等。路漫漫其修远兮...

感谢点赞支持~

附上webpack系列历史文章(比较干货):

【webpack系列】从源码角度分析webpack打包产出及核心流程

【webpack系列】从源码角度分析loader是如何触发和执行的

【webpack系列】webpack是如何解析模块的

【webpack系列】webpack的plugin插件是如何运行的

【webpack系列】从源码角度分析webpack热更新流程

【webpack系列】从源码角度深度剖析html-webpack-plugin执行过程