白话vue3——vue3文档详读(第一期)

387 阅读8分钟

前言

众所周知,vue的文档被公认为写得十分完美,所以笔者打算从头开始仔细读一遍vue文档,并在阅读的过程中结合vue源码相关知识进行解读。所以这第一期就从vue3文档的开始,vue的介绍和使用方式开启第一期的分享。

vue3简介——渐进式框架

互联网人的词汇就是这样,一个初次听起来似懂非懂的词,仔细琢磨下,确实入木三分。

首先vue是个框架,框架就是造轮子的人使用者规定的开发流程,让使用者不用耗费心力去想设计模式相关问题,而是考虑如何用这个流程能够解决我的业务问题。

使用场景

前端的业务场景很多,从简单到复杂有多种情况:

1、增强html

比如我只想让静态的html页面具备响应式,实现一个双向数据绑定的表单,或者使用v-for动态创建列表元素,那么我只需要采用script标签引入vue,引入的方法有如下几种:

// index.html
// 普通script标签引入
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
// esmodule 方式引入
<script type="module">
    import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js' 
</script>
// Import maps 方式引入
<script type="importmap">
    {
        "imports":{
            "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
         }
    }
</script>

第一种普通script标签引入的方式兼容性最好,因为不依赖浏览器对esmodule的支持,而且在调试时也不用本地起一个服务器来访问html文件,但是问题在于这种方式不好进行模块管理,相互依赖的代码组织起来比较难受,毕竟该方式并没有用到模块化的思想,

第二种是通过esmodule模块化规范引入,在script标签类型为module模块中,浏览器便会以esmodule的语法来解析模块的导入导出符合esmodule规范的代码。

第三种是通过script标签的特性import maps,在不使用构建工具的情况下,esmodule在浏览器端需要以绝对路径或者相对路径的形式引入第三方模块,而我们在有webpack或者node.js加持的项目中,都是采用别名的方式引入,例如:

// node.js
const dayjs = require('dayjs')
// webpack
import dayjs from 'dayjs'

使用import maps,则可以给路径冠以别名,从而让前端开发者以熟悉的方式引入第三方模块。 不仅如此,import maps还具有合并请求,使用不同版本的第三方包等能力,这里就不再赘述了。

总结:通过以上3种引入方式,我们可以方便地在html项目中使用vue的能力,此时的vue就和jquery一样。

2、web components(自定义元素)

web components可以简单理解为react或者vue的组件(其实也是非常类似),因为无论是什么组件,最终渲染引擎将其渲染到页面上时,它们都是一个个实例化的对象而已,并无本质区别,只是实现方式有所不同。由于都是组件化的思想,我们可以利用vue来构建一个web component,甚至都不用使用vue-cli等构建工具:

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>hello world!!</h1>
    <my-vue-element></my-vue-element>
    <my-vue-comp></my-vue-comp>
    <script type="module">
        import { defineCustomElement, defineComponent } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js';
        const MyVueElement = defineCustomElement({
            // 这里是同平常一样的 Vue 组件选项
            props: {},
            data(){
                return {
                    name: 'garrett',
                }
            },
            template: `
                <div class="test">{{name}}</div>
            `,
            // defineCustomElement 特有的:注入进 shadow root 的 CSS
            styles: ['.test{color:red}']
        });
        // 定义一个vue组件实例
        const MyVueApp = defineComponent({
            data() {
                return {
                    message: 'hello world'
                }
            },
            template: `<div>{{message}}</div>`
        });
        // 将vue实例转换为自定义元素
        const myVueComponent = defineCustomElement(MyVueApp)
        customElements.define('my-vue-comp', myVueComponent);
        customElements.define('my-vue-element', MyVueElement);

    </script>
</body>
</html>

以上代码仅仅在一个html文件中完成,展示了两种由vue创建web component的方法:
1、直接使用defineCustomElement方法定义一个继承于HTMLElement的构造器;
2、先创建一个vue组件,然后利用defineCustomElement将vue组件转换为自定义元素的构造器,也可以实现自定义元素的效果
这个时候有同学就要问了,“你都可以直接创建自定义元素了,为什么要通过vue组件转换一下呢?”,这是为了证明vue组件能够被转换成自定义元素,既然能够转换,那就意味着我可以将vue的单文件组件借助构造工具转换成自定义元素,这也是vue的一种使用方式。

3、单页面应用(SPA)

一说到单页面应用,相信各位读者肯定比我更清楚如何使用,笔者在这里就不赘述了,这里借助vue官方文档的一句话来介绍vue在单页面应用中发挥的作用:“Vue 不仅控制整个页面,还负责处理抓取新数据,并在无需重新加载的前提下处理页面切换。”

4、服务端渲染(SSR)

在前端性能越来越重要的今天,服务端渲染逐渐成为前端页面展示的又一重要方式。当我们通过http请求数据时,如果http请求返回的是html文本,那么浏览器会自动解析html文本变成可视化的页面。服务端渲染的第一步也是如此,只不过vue在SSR中担任了两个职责:
1、构建html文本。当我们在浏览器端使用createApp创建一个vue应用时,我们一般会得到一个vue的应用实例对象,然后渲染函数会根据这个对象在需要挂载的节点上创建dom。而在服务端,vue完成的是利用createSSRApp将vue的实例转变为html字符串。
2、激活静态html(水合)。有同学会说了,前后端分离之前,不就是服务端通过模板渲染的方式直接输出html文档嘛,这咋还退步呢?这是因为服务端渲染结合了SPA和服务端直出html的优点,既保证了第一屏的性能和SEO,又保证了丝滑的如原生应用般的响应式体验。那vue是如何做到这一点的呢,让我们直接冲进源码:
首先我们对比一下我们在SPA中使用vue的方式和在SSR中使用vue的方式
(1)SPA(假设html上已经有了app节点)

// app.js
import { createApp } from 'vue' 
 createApp({
    data: () => ({ count: 1 }),
    template: `<button @click="count++">{{ count }}</button>`
}).mount('#app')

(2)SSR

// app.js 客户端代码
import { createSSRApp } from 'vue'
createSSRApp({
    data: () => ({ count: 1 }),
    template: `<button @click="count++">{{ count }}</button>`
}).mount('#app')
// 

从代码层面可以发现,在web端,SPA和SSR挂载vue实例的方式十分相似,差别就在于createAppcreateSSRApp上,那么这两者究竟有什么区别呢,我们可以从其被调用的链路查到线索(这两个函数的定义位于core/packages/runtime-dom/src/index.ts):
createApp -> ensureRenderer -> baseCreateRenderer -> createAppAPI -> mount createSSRApp -> ensureHydrationRenderer -> baseCreateRenderer -> createAppAPI -> mount 由于两个函数的创建链路过长,这里为了展示两者的差异性,所以从最终的mount函数做分析,中间的调用链路上的差异性主要在于参数。

\\ mount函数
  mount(
    rootContainer: HostElement, // 挂载的根节点
    isHydrate?: boolean, // 是否是水合(SSR该值为true)
    namespace?: boolean | ElementNamespace,
  ): any {
    if (!isMounted) {
      // 页面挂载时
      // 第一步根据传入参数的根组件,创建vnode(虚拟节点)
      const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
      // 给虚拟节点扩展应用上下文,可以通过该上下文调用api
      vnode.appContext = context
      if (isHydrate && hydrate) {
        // 如果是SSR的水合
        hydrate(vnode as VNode<Node, Element>, rootContainer as any)
      } else {
        // 非SSR,只是需要渲染
        render(vnode, rootContainer, namespace)
      }
      isMounted = true
      app._container = rootContainer
      // 返回根组件的实例
      return getComponentPublicInstance(vnode.component!)
    } else if (__DEV__) {
        ...
    }
  };
  
  

从代码层面可以看出,两者最大的区别在于挂载时,调用的是hydtate还是render,render函数其实很简单,就是大家熟知的diff算法和操作dom的方法。而hydrate有所不同,它的职责在于对比客户端的根组件vnode和服务端直出的html的dom结构,如果某个节点不匹配,客户端则会移除这个节点并按照客户端的vnode重新渲染,保证客户端vnode和html的一致性。在匹配过程中,如果碰到vue组件,则会递归执行mountComponent方法,该方法内部激活了响应式,使得客户端直出的html具备随数据更新的能力。

5、静态SSG

静态站点生成,简单理解就是直接在服务端构建html的全部元素,用户访问时则将html静态文件直接返回,SSG同SSR一样,具备SEO和首屏性能高的优点,但是这种页面更适用于不频繁改动的页面,比如企业官网、项目文档等。
此时vue担任的工作更多倾向于编译工具,开发仍然可以采用单文件组件的形式,但是构建出的产物是可以直接访问的静态文件。
SSG性能好,部署方便(不像SSR需要部署服务),并且可以在首屏后被vue激活成SPA,也具有更好的用户体验。但是首屏静态意味着没办法针对不同用户做不同展示,应用的范围相对于SSR和SPA来说更窄,不过技术服务于业务,针对不同场景选择更合适的技术才是开发之道。

第一期先简单分享一下vue的使用方式,后面将继续掺合vue3的源码来读文档,感兴趣的同学可以关注我哦!
(本文有什么不对的地方请大家在评论区讨论!)