vue3源码分析系列之一 (createApp)__Vue.js
发布于 3 年前 作者 banyungong 3387 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black, awesome-green, qklhk-chocolate

贡献主题:https://github.com/xitu/juejin-markdown-themes

theme: juejin highlight:

前言

编写本系列的说明如下:

  1. 首先是为了我自己在架构设计和程序设计上有一个更好的提升,以及对web更升入的了解, 在语法层面基本都ok的前提下,学习最优秀的源代码是最好的提升途径.

  2. 提升自己技术写作的能力, 我一直希望能用更简单易懂的语言传播技术,因为我也是前端技术的受益者.

  3. 如何分析优秀的代码,观察优秀代码如何运用底层思想,希望能和各位同仁交流一些经验.

注意事项:

  1. ts基础就不做解释, 可以去看官网语法说明

  2. __DEV__暂不分析,用来判断是否是开发环境的全局变量,在编译的时候替换, 一般与调试输出相关

首先是创建一个vue对象 const {createApp} = Vue

createApp({
            data(){
                return {
                    one: "one",
                    two: "two",
                    three: "three"
                }
            },
        }).mount("#app")

断点分析createApp函数

export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)

  if (__DEV__) {
    injectNativeTagCheck(app)
  }

  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

ensureRenderer函数

// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer<Element> | HydrationRenderer
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

以上代码在runtime-dom包中

为了理解什么是renderer,我们倒推一下,

先分析app.mount()函数

function normalizeContainer(
  container: Element | ShadowRoot | string
): Element | null {
  if (isString(container)) {
    const res = document.querySelector(container)
    if (__DEV__ && !res) {
      warn(
        `Failed to mount app: mount target selector "${container}" returned null.`
      )
    }
    return res
  }
  if (
    __DEV__ &&
    container instanceof window.ShadowRoot &&
    container.mode === 'closed'
  ) {
    warn(
      `mounting on a ShadowRoot with \`{mode: "closed"}\` may lead to unpredictable bugs`
    )
  }
  return container as any
}

normalizeContainer()函数

接收 Element | ShadowRoot | string

返回 Element | null

关于 window.ShadowRoot
ShadowRoot.mode

https://developer.mozilla.org/zh-cn/docs/web/api/shadowroot 如何既能将内部节点的信息封装起来,又能将这些节点给渲染出来呢?W3C提出了ShadowDOM的概念,ShadowDOM可以使一些DOM节点在特定范围内可见,而在网页DOM树中不可见,但是网页渲染的结果包含了这些节点。 某些视频网站video标签下的ShadowDOM结构。

可以在setting设置 中勾选 show user agent shadoW DOM 打开查看

Element参数类型源码暂未处理, 将来说不定会有

const component = app._component

暂时不分析, 看调试观察代码运行

container.innerHTML = ‘’

传入的dom元素节点下面的内容全部被清空, 页面白屏时间

接着单步调试, const proxy = mount(container)执行之后,发现页面数据被渲染出来了

查看调试发现这是一个代理对象,通过这行代码, 我们传入createApp({…})函数的json对象中的data数据就和页面中的设置模板{{}}关联起来了, 那么如何关联的呢?

function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
const app = ensureRenderer().createApp(...args)
const { mount } = app

ensureRenderer() 单例模式, renderer没有就去创建一个 思考一下什么是app对象,

通过app.mount这个函数,我们知道这个对象可以将我们自定义的html页面转换成真实的html

为什么我们需要自定义页面呢?

因为jquery那一套已经难以满足越来越广泛的开发需求了, 我们需要更加高效快捷自定义页面逻辑, 然后再将其转换成浏览器能识别的文件格式.

这个app对象就可以理解为映射自定义页面逻辑到浏览器能识别的页面逻辑的一个工具对象,用来分析管理我们传入的json对象中的信息到真实页面的投射.

ensureRenderer() ==> renderer

renderer.createApp(…arg)

可以理解为ensureRenderer(翻译器工厂,单例模式),该工厂拥有多条翻译器生产线(一组翻译器生产线),工厂运行一条产线,即执行createApp(),创造出一种类型的翻译器,用来翻译我们的逻辑然后提交给浏览器处理dom ,(针对浏览器场景的翻译器)

为什么不直接创造一个翻译器呢? 而是分这么多条生产线

因为功能复杂

现实需求中,还有针对服务端场景的翻译器需求, 所以有其他的翻译器产线createSSRApp()创建的一种服务端渲染翻译器

超级工厂(createRenderer) ==> 单例工厂(ensureRenderer) ==> 产线(renderer) ==> 产品(app)

如果是java等面向对象的编程范式, 就是类的继承

但是js是多范式编程语言, 我们可以采用函数式编程的思想

let production = createApp(a, b, c) // 这是目的, (针对浏览器场景的翻译器)

// 函数科里化
let factory = createfactory(a)    // 工厂
let productionLineOne = factory(b)   // 产线一, 根据参数不同, 创建不同类型的产线
let productionLineTwo = factory(bb)   // 产线二, 根据参数不同, 创建不同类型的产线
let productionOne = productionLineOne(c)    // 产线一的一号产品, (针对浏览器场景的翻译器) 
let productionTwo = productionLineTwo(cc)    // 产线二的二号产品, (针对服务端场景的翻译器)  

createfactory(a)(b)(c)   ==> 一号产品

类抽象逻辑, 总有一种全局归纳的思想, 只有认识到了, 才能够理解类

例如, 有陆生生物, 水生生物, 还没有单栖生物的概念的时候, 两栖生物可能会被人们认为是怪物, 至少如果有三栖生物的话, 会被认为是怪物,所以类是不自热的, 先有实现, 类是一种被迫归纳总结的思想. 使用类, 必须得理解全局, 你才能很好的归纳总结(定义类)

函数科里化就不一样, 这是一种渐进式的功能扩展, 将多参数的函数转换成一个个单一的参数的函数, 使得函数能力层层递进, 能力的每一层依赖于上一层,每一层都可以自由扩展,提高了核心逻辑的复用能力和可扩展性.

函数科里化只是一种思想, 实际应用中并不一定按照上述思路, 我们简化一下核心逻辑

createRenderer().createApp(...arg).mount(container) 
createHydrationRenderer().createSSRApp(...arg).mount(container, true)

第一层,无参数,直接两个独立函数, 因为实际变成中,场景已经确定,没必要一个函数用参数区别场景, (浏览器,服务端), 两个函数生成了两种renderer对象,

let renderer: Renderer<Element> | HydrationRenderer

第二层, 在第一层的基础上,renderer对象接收我们的自定义页面逻辑, 调用不同的函数,生成一个打工仔app,

第三层, 对象app开始干活啦, 把我们的第一义页面逻辑mount()提交给了浏览器或者服务端.

这里与科里化的区别只是返回对象的不同,科里化返回的仍然是一个函数, 可以直接在接收参数, 源码这里, 返回的是一个对象, 这个对象内置了某种方法, 我们再去调用这个对象中的函数, 为什么使用对象而不是函数进行科里化操作, 可能是因为对象能包含更多的能力, 当然函数也可以, 但是函数中内部封装新的能力, 会让函数功能定义不清晰, 不符合单一职责的编程原则, 所以这里使用对象更好一些, 再结合单例模式, 依然缓存了渐进式的函数能力, 即对象的能力层层递进.

我们已经大致知道renderer是什么了, 学习概念性的东西, 需要自己体会, 它是什么, 我们可以通过它的行为去感知.

创建打工仔app只是它的一个能力之一, 已经renderer之间为啥还有不同的类型, 它们之间有啥区别和联系 ?

renderer可以通过createRenderer()或者createHydrationRenderer()创建,这两个函数都在 renderer.ts文件中, 即renderer出生的文件, 下篇文章我来分析renderer是如何诞生的.


__额外分析__

解构赋值拷贝原来的app中的mount函数, 然后在新函数中使用原来的通用函数.

为什么要这样做呢?

vue面向SSR有不同的实现逻辑, 除了createApp 

还有createSSRApp 

export const createSSRApp = ((…args) => { const app = ensureHydrationRenderer().createApp(…args)

if (DEV) { injectNativeTagCheck(app) }

const { mount } = app app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { const container = normalizeContainer(containerOrSelector) if (container) { return mount(container, true) } }

return app }) as CreateAppFunction<Element>


这种架构方式可以提高基础代码的复用

container.removeAttribute('v-cloak')

container.setAttribute('data-v-app', '')

这两行代码可以已删一添加,有什么好处?

如果你不想在页面数据渲染完成之前显示任何东西 , 例如: {{one}}

可以将传入的 #app标签设置 v-cloak属性   再将 [v-cloak] {
            display: none;
        }
        
data-v-app后面添加有啥好处呢? 原理还是一样, 可以提前设置css样式   





<p style="line-height: 20px; color: #ccc">
        版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
        作者: 晚风的编程世界
        原文链接:<a href='https://juejin.im/post/6930073899086872584'>https://juejin.im/post/6930073899086872584</a>
      </p>
回到顶部