返回

揭秘vue3.0简易版的createApp功能实现过程

前端

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

我们先来看一下之前的分析,

// 放入一个临时变量中,避免被回收
let app = {
  message: "hello world"
}
// 把实例导出
export default app
<div id="app">
  ${app.message}
</div>

<script>
import {app} from "./app"

// 获取 DOM 元素
let div = document.querySelector("#app")

// 手动创建一个实例
// 一个 app 就是一个对象,成员变量是 message
const app1 = {
  message: "hello world 1"
}
// 给实例对象挂载到全局
window.app = app1

// 订阅 app.message
new Proxy( app1, {
  get:function(target, name){
    return Reflect.get(target, name)
  },
  set:function(target, name, value){
    Reflect.set(target, name, value)
    // 发生了更新,调用更新函数
    update()
  }
})

// 更新函数
function update(){
  // 将数据转换为 HTML
  let htmlStr = app.message
  // 把数据渲染到 DOM
  div.innerHTML = htmlStr
}
</script>

这是一个最简易版的,创建一个临时变量存储 app,然后导出,导入后,再 挂载到全局,给全局变量增加一个代理,代理会在设置值时触发更新函数,更新函数会将数据转换为 HTML 并渲染到 DOM。

现在我们来看看 createApp 函数能做什么

import {app} from "./app"

const {createApp} = require("vue")

// 把实例导出
export default createApp(app)
// main.js
import {createApp} from "./app"

createApp(app).mount("#app")

我们发现 createApp 这个函数,会传入一个 app 实例,然后返回一个新的 app 实例。

我们来看看这个返回的 app 实例

// 创建实例
let app = {
  message: "hello world"
}

// 创建 app 实例
const app1 = createApp(app)

console.log(app1)

这里我们会发现,app1 是一个函数,跟以前看到的 app 实例不一样。这个函数能做什么呢?

// 挂载到页面
app1.mount("#app")

我们发现执行 app1.mount 之后,这个函数内部会直接创建一个新的 app 实例,并将其挂载到 DOM 元素中。这个 app 实例就和上篇文章一样,也是 app1 的代理, 当触发更新时,也会调用更新函数。

上面的步骤虽然是写的简化版,但是执行起来还是会非常慢的。

在之前我们使用 createApp 时,我们可以使用其提供的一些方法。

createApp(app).use(plugin1).use(plugin2)...

我们发现在调用 createApp 时,可以连续调用 use 函数,use 函数还返回 createApp 函数。其实这就是闭包函数的妙用。

上篇讲的太概略了,这篇文章并没有写到这个闭包函数的概念。use 函数就是这个闭包函数的典型案例。

app1 有一个内部函数,可以调用它返回一个新的实例,但如果我们使用 app1.use(plugin1),其实 app1 的内部函数会变,再用 app1 就不是原来的 app1 函数了。

import { app } from "./app"

const app1 = createApp(app)

app1.use(plugin1)
app1.use(plugin2)

app1.mount("#app")

我们发现,其实就是一个链式调用, app1.use(plugin1),plugin1 函数返回一个 app1 参数,我们继续 app1.use(plugin2),plugin2 的 app1 也是 上一次返回的 app1,所以 app1.use(plugin2) 返回的 app1 就是一个 plugin2 包裹住的 app1,当调用 app1 时,内部函数执行就是 plugin2 内部函数的执行,会调用 plugin2 的逻辑。

import { app } from "./app"
import plugin1 from "./plugin1"
import plugin2 from "./plugin2"

const app1 = createApp(app)

app1
  .use(plugin1)
  .use(plugin2)
  .mount("#app")

这是 createApp 的原理,能让我们一个个挂载,并允许我们自定义想要的插件的行为。

一个插件可以对 app 进行一些自定义的处理。

一个插件其实就是一个函数,这个函数会接收 app 作为参数,然后返回一个新的函数。

上面提到,当调用 app1 时,内部函数执行就是 plugin2 内部函数的执行,会调用 plugin2 的逻辑。

那么我们看看 app1.mount 这个函数内部的逻辑。

const app1 = createApp(app)
app1.mount("#app")
mount(app){
  // 创建一个 app 实例
  let app = createAppInstance(app)

  // 挂载到 DOM
  mount(app, "#app")
}

mount 函数会根据 app 和对应的选择器,挂载到 DOM 上。

我们还是来看一看 createAppInstance 这个函数

function createAppInstance(app){
  // 返回新的 app 实例
  return new App(app)
}

这个函数其实就是返回一个 app 实例,这个实例就是前面讲的那一套。

然后我们把 plugin1 和 plugin2 分别导入进来,然后把他们分别传入 app1.use(plugin1).use(plugin2)

import {app} from "./app"

const { createApp } from "./app"
const plugin1 = require ("./plugin1")
const plugin2 = require ("./plugin2")

const app1 = createApp(app)
  .use(plugin1)
  .use(plugin2)

app1.mount("#app")
app1.use(plugin1)

调用这个函数后,app1 就是一个新的 app1,这个 app1 包裹着原来的 app1,因此返回 app1 时,原来的 app1 会作为参数传入到新的 app1 中。然后新的 app1 被执行。

这就是闭包函数的作用,我们传入 app1 后,依然能够在内部访问到 app1。

接下来我们就来看一下 plugin1 和 plugin2 如何编写。

function plugin1(app){
  // 返回一个新的 app 实例
  return function(c){
    return new App(c)
  }
}

在这个插件中,我们实际上返回一个函数。

当执行 app1.use(plugin1) 时,这个函数会调用这个 plugin1 函数,把 app1 作为参数传进去。

其实返回的函数就是新的 app 实例的构造函数。

我们再继续看看 plugin2 如何编写。

function plugin2(app){
  // 返回一个新的 app 实例
  return function(c){
    // 这里的 app 实例是 plugin1 的内部函数返回的 app 实例
    // 当我们在 main.js 中使用 app.use(plugin1) 时,这个 app 实例就是我们传入的 app 实例
    return new App(app,c)
  }
}

这里 app 是第一次使用 use 时,传入的 app 实例,c 是第二次使用 use 时,传入的 app 实例。

我们在 main.js 中 app1.use(plugin2) 中的 app 是不是第一个 app,是第二个 app 实例。

然后我们创建一个 app 的构造函数,这个构造函数其实就是一个实例,它包含了 data。

export class App{
  constructor(app){
    // 一个临时变量,存储 app
    let internal = {}
    // 挂载到全局变量
    window.app = app
    // 创建代理
    let proxy = new Proxy( app, {
      get:function(target, name