vue.js设计与实现

内容一共6篇,共18个章节

第一篇:框架设计概览

第一章:权衡的艺术

框架的设计,本身就是一种权衡的艺术。

命令式与声明式

命令式:关注过程

const div = document.querySelector('#app');
div.innerText = 'hello world';
div.addEventListener('click',() => {alert('ok')});

声明式:关注结果

<div @click="() => alert('ok')">hello world </div>

所以对于vue来说,它封装了命令式的过程,对外暴露了声明式的结果。

性能与可维护性的权衡

命令式的性能>声明式的性能

因为命令式的代码是直接通过原生的js进行实现的,但是vue还是对外暴露了声明式的接口,是因为声明式的维护要远远大于命令式的可维护性。

在前端领域,想要使用 JavaScript 修改 html 的方式,主要有三种:**原生 JavaScript、innerHTML、虚拟 DOM**

image-20230207233547097.png

由上图可以看出,虚拟DOM的心智负担(书写难度)最小,可维护性最高,性能中等。

所以哪怕它的性能不是最好的,但是vue选择虚拟DOM来进行渲染层的构建,这个也是一种性能和可维护性的权衡。

运行时和编译时

它们都是框架设计的一种方式,可以单独出现,也可以组合使用。

运行时:runtime

利用render函数,直接把DOM转化为真实DOM元素的一种方式,在整个过程中,不包含编译的过程,所以无法分析用户提供的内容。

编译时:compiler

直接把template模板中的内容,转化为真实DOM元素。因为存在编译的过程,所以可以分析用户提供的内容,同时没有运行时理论上性能会更好。目前该方式,有具体的实现库,那就是现在也非常火的 Svelte,但是这里要注意: 它的真实性能,没有办法达到理论数据。

运行时+编译时

它的过程被分为两步:

  1. 先把 template 模板转化为 render 函数。也就是 编译时
  2. 再利用 render 函数,把虚拟 DOM 转化为 真实 DOM。也就是 运行时

两者的结合,可以:在编译时,分析用户提供的内容在运行时,提供足够的灵活性。这也是 vue 的主要实现方式。

第二章:框架设计的核心要素

框架设计时需要注意的点

通过 环境变量 和 TreeShanking 控制打包之后的体积

构建不同的打包产物,以应用不同的场景

提供了 callWithErrorHandling 接口函数,来对错误进行统一处理

源码通过 TypeScript 开发,以保证可维护性。

内部添加了大量的类型判断和其他工作,以保证开发者使用时的良好体验。

第三章:vue.js 3的设计思路

在vue中UI形式主要分为两种:

声明式的模板描述

<div :id="tId" :class="&#123; tClass: true &#125;" @click="onTClick"></div>

命令式的render函数

import &#123; h &#125; from "vue"

export default &#123;
    render() &#123;
        return h('h1', &#123; onClick: handler &#125;) // 虚拟DOM
    &#125;
&#125;

声明式的模板描述本质上就是我们常用的template模板,它会被编译器编译,得到渲染函数render。渲染函数接收两个参数VNode和container,VNode表示虚拟DOM,它本质上就是一个JS对象,container是一个容器,表示被挂载的位置,而render函数的作用就是把VNode挂载到container上。

渲染器和渲染函数并不是一个东西,渲染器是函数createRenderer的返回值,是一个对象,被叫做renderer。renderer对象中有一个方法render,这个render就是我们常说的渲染函数。

因为Vue以组件代表最小粒度,所以vue内部的渲染,本质上是:大量组件的渲染。而组件本质上是一组DOM的集合,所以渲染一个一个的组件,本质上就是在渲染一组DOM,也就是说Vue本质上是:以组件作为介质,来完成针对于一组、一组的DOM渲染。

第二篇:响应式系统

第四章:相应系统的作用与实现

响应式系统

副作用函数会产生副作用的函数

// 全局变量
let val = 1;
function effect() &#123;
    val = 2; // 修改全局变量,产生副作用
&#125;

上述代码中,effect函数的触发会导致全局变量val发生变化,那么effect就可以被叫做副作用函数,而如果val这个数据发生了变化,导致了视图的变化,那么val就会被叫做响应式数据

那么如果想要实现响应式数据的话,那么它的核心逻辑,必然要依赖两个行为:

  • 第一个是 getter 行为,也就是 数据读取
  • 第二个是 setter 行为,也就是 数据修改

vue2:Object.defineProperty实现

vue3:Proxy实现

首先是getter形式:

function effect() &#123;
    document.body.innerText = obj.text;
&#125;

执行effect方法时,方法内部触发了getter行为,一旦getter被触发,则把对应的effect方法保存到一个bucket(数据对象)中。

触发setter行为时:

obj.text = 'hello vue3';

触发setter时,会从bucket中取出effect方法,并执行,那么此时因为obj.text的值发生了变化,所以effect被执行时document.body.innerText会被赋上新的值,从而导致视图发生变化。

调度系统(scheduler)

调度系统,指的就是 响应性的可调度性,而所谓的可调度,指的就是 当数据更新的动作,触发副作用函数重新执行时,有能力决定:副作用函数(effect)执行的时机、次数以及方式

// 原始打印顺序
1
2
"finish!"

// 改变打印顺序
1
"finish!"
2

想要实现一个调度系统,则需要依赖 异步:Promise队列:jobQueue 来进行实现。咱们需要 基于 Set 构建出一个基本的队列数组 jobQueue,利用 Promise 的异步特性,来控制执行的顺序

计算属性

当控制了执行顺序之后,那么就可以利用这个特性来完成计算属性(computed)的实现了。计算属性本质上是: 一个属性值,当依赖的响应式数据发生变化时,重新计算。它的实现需要彻底依赖于 调度系统(scheduler) 来进行实现。

惰性执行(lazy)

watch监听器

watch 监听器本质上是 观测一个响应式数据,当数据发生变化时,通知并执行相应的回调函数,这也就意味着,watch 很多时候并不需要立刻执行。

惰性执行的实现要比调度系统简单。它本质上 是一个 boolean 型的值,可以被添加到 effect 函数中,用来控制副作用的执行

if(!lazy) &#123;
    // 执行副作用函数
&#125;

watch的实现原理

基于 调度系统 与 惰性执行,那么就可以实现 watch 监听器了。

过期的副作用

watch监听器实现非常广泛,有时候甚至可以在watch中完成一些异步操作,但是大量的异步操作,既有可能会导致竞态问题。

竞态问题:在描述一个系统或者进程的输出,依赖于不受控制的事件出现顺序或者出现时机。

let finalData;
watch(obj, async() => &#123;
    // 发送并等待网络请求
    const res = await fetch('/path/to/request')
    // 将请求结果赋值给data
    finalData = res
&#125;)

如果obj连续被修改了两次,那么就会发起两个请求,假设我们最终期望的时finalData被赋值为请求B的结果,但是因为异步的返回结果我们可能无法预计,所以,如果请求B先返回,那么finalData的值就会变为请求A的返回值。这样就会导致竞态问题。

解决:

使用watch回调函数的第三个参数。

第五章:非原始值(对象)的响应性方案

Proxy和Reflect接口

这两个接口通常会一起进行使用,其中:

  • Proxy 可以 代理一个对象(被代理对象)的 getter 和 setter 行为,得到一个 proxy 实例(代理对象)
  • Reflect 可以 在 Proxy 中使用 this 时,保证 this 指向 proxy,从而正确执行次数的副作用

第六章:原始值(非对象)的响应性方案

proxy只能代理复杂的数据类型,这意味着简单数据类型无法具备响应性。

在vue中,我们可以通过ref构建简单数据类型的响应。

第三篇:渲染器

第七章:渲染器的设计

前面说过,渲染器和渲染函数不是一个东西

渲染器:是js中的一个对象,是createRenderer的返回值,一个对象。

渲染函数:首先它是一个函数,是渲染器对象中的render函数。

核心思路:vue中的渲染器总体可以分为两部分

1、浏览器渲染时,利用DOM API完成DOM的操作,比如,如果渲染DOM那么就使用createElement,如果要删除DOM那么就使用removeChild。

2、渲染器不能与宿主环境(浏览器)产生强耦合。因为vue不光有浏览器渲染还有服务器渲染,如果在渲染器中绑定了宿主环境,那么就不好实现服务器渲染了。

vnode详解:

是js中的一个普通对象,代表了渲染的内容。其中有一个属性叫做type,这个type表示了渲染的DOM,例如:type===div:则表示div标签.

第八章:挂载与更新

对于渲染器而言,最核心的事情就是对节点进行挂载、更新的操作。

  • 挂载节点:所谓的挂载就是节点的初次渲染,可以通过createElement方法新建一个DOM节点,再利用parent.insertBefore方法插入节点

  • 更新节点:当响应性数据发生变化时,可能会涉及到DOM的更新,此时的更新本质上是属性的更新。

    除了属性更新还有事件的更新。

    属性节点的操作:

    对于属性而言,大致可以分为两类:

    属性:class、id、value、src…

    事件:click、input…

    非事件的属性部分分为 html属性和DOM properties(浏览器的属性分类)

    html attributes:

    • 表示定义在html标签中的属性,这类属性只能在html中进行操作

    DOM properties:

    • 在dom中拿到DOM节点后使用js语句添加的属性,这类属性可以通过js语句修改

    • class、value、type属性详细操作

    • 可通过el.setAttribute(‘属性名’,’属性值’)、el.属性名 = ‘ ‘、el[‘属性’] = ‘ ‘ 进行修改

      // 上述三个属性通过例子进行详细说明
      <textarea class='class-name' type='text'></textarea>
      
      const el = document.querySelector('textarea');
      // 修改类名
      el.setAttribute("class","class-Name");
      el.className = 'class-Name';
      // 修改type
      el.setAttribute("type","input");
      // 修改value
      el['value'] = 'hello world!'
      

    事件更新

    • 添加事件 el.addEventListener
    • 删除事件 el.removeEventListener
    • 更新事件一般是删除旧事件,添加新事件,但是每次都是addEventListener和removeEventListener都很消耗性能,所以一般使用vei这个概念,意思就是它在addEventListener回调函数上添加了一个value属性方法,在回调函数中只要触发了这个方法,通过更新该属性方法的形式达到事件更新的效果。
  • 删除节点:parent.removeChild

第九、十、十一章:Diff算法

这一章文章中讲解的非常少,需要自己补充额外的知识,虚拟DOM和diff算法

webpack

简要介绍

尚硅谷的网上课件

https://yk2012.github.io/sgg_webpack5/

这里大致开始编写一段基础的webpack5的知识点

因为我们在开发时会使用框架、ES6模块化语法,less等css预处理器,这样的代码必须编译成浏览器能够识别的js、css语法才能够在浏览器上运行,所以我们需要webpack等的打包工具。

gulp、parcel、webpack、vite等都是打包工具,但是webpack的功能更加强大,使用率更高,所以现在学习webpack。

webpack是一个静态资源的打包工具。webpack输出的文件叫做bundle。

资源项目
webpack_code # 项目根目录(所有指令必须在这个目录运行)
    └── src # 项目源码目录
        ├── js # js文件目录
        │   ├── count.js
        │   └── sum.js
        └── main.js # 项目主文件d

main.js中依赖了count和sum.js

启用webpack
npx webpack ./src/main.js --mode=development

npx webpack: 是用来运行本地安装 Webpack 包的。

./src/main.js: 指定 Webpackmain.js 文件开始打包,不但会打包 main.js,还会将其依赖也一起打包进来。

--mode=xxx:指定模式(环境)。

默认webpack会将文件打包输出到dist目录下

基本配置

entry入口:指示 Webpack 从哪个文件开始打包

output(输出):指示 Webpack 打包完的文件输出到哪里去,如何命名等

loader(加载器):webpack 本身只能处理 js、json 等资源,其他资源需要借助 loader,Webpack 才能解析

plugins(插件):扩展 Webpack 的功能

mode(模式):主要由两种模式:

  • 开发模式:development
  • 生产模式:production

webpack的配置文件:webpack.config.js

Webpack 是基于 Node.js 运行的,所以采用 Common.js 模块化规范

// Node.js的核心模块,专门用来处理文件路径
const path = require("path");

module.exports = &#123;
  // 入口
  // 相对路径和绝对路径都行
  entry: "./src/main.js",
  // 输出
  output: &#123;
    // path: 文件输出目录,必须是绝对路径
    // path.resolve()方法返回一个绝对路径
    // __dirname 当前文件的文件夹绝对路径
    path: path.resolve(__dirname, "dist"),
    // filename: 输出文件名
    filename: "main.js",
  &#125;,
  // 加载器
  module: &#123;
    rules: [],
  &#125;,
  // 插件
  plugins: [],
  // 模式
  mode: "development", // 开发模式
&#125;;

运行指令:npx webpack

开发模式

1、编译代码,使得浏览器能够识别运行

开发时我们有样式资源、字体图标、图片资源、html资源等,webpack不能处理这些资源,所以我们要加载配置来编译这些资源。

2、代码质量检查、梳理代码规范

处理样式资源

处理css

// 下载包 需要下载两个loader 
npm i css-loader style-loader -D
  • css-loader:负责将 Css 文件编译成 Webpack 能识别的模块
  • style-loader:会动态创建一个 Style 标签,里面放置 Webpack 中 Css 模块内容

此时样式就会以style标签的形式在页面上生效

配置(其余不变,只需要在module也就是加载器loader中添加rules即可)

module: {
    rules: [
        // 用来匹配.css结尾的文件 i表示不区分大小写
        test: /\.css$/i,
        // use数组里面loader执行顺序是从右到左
        use: ["style-loader", "css-loader"],
    ],
},
  • src/css/index.css

    .box1 &#123;
      width: 100px;
      height: 100px;
      background-color: pink;
    &#125;
    
  • src/main.js

    import count from "./js/count";
    import sum from "./js/sum";
    // 引入 Css 资源,Webpack才会对其打包
    import "./css/index.css";
    
    console.log(count(2, 1));
    console.log(sum(1, 2, 3, 4));
    
  • public/index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>webpack5</title>
      </head>
      <body>
        <h1>Hello Webpack5</h1>
        <!-- 准备一个使用样式的 DOM 容器 -->
        <div class="box1"></div>
        <!-- 引入打包后的js文件,才能看到效果 -->
        <script src="../dist/main.js"></script>
      </body>
    </html>
    

    运行指令:npx webpack

处理图片资源

配置

module: &#123;
    rules: [
      &#123;
          test: /\.(png|jpe?g|gif|webp)$/,
        type: "asset",
      &#125;,
       ],
&#125;,

添加图片资源

  • src/images/1.jpeg
  • src/images/2.png
  • src/images/3.gif

使用图片资源

.box2 &#123;
  width: 100px;
  height: 100px;
  background-image: url("../images/1.jpeg");
  background-size: cover;
&#125;

运行指令

npx webpack

打开index.html页面查看效果

此时如果查看 dist 目录的话,会发现多了三张图片资源

因为 Webpack 会将所有打包好的资源输出到 dist 目录下

修改输出资源的名称和路径 需要在配置中output添加filename:static/js/main.js(这个路径是在path路径之后新开辟的路径

)

处理js资源(webpack对js的处理是有限的,只能编译js中ES模块化语法,不能编译其他语法,导致js不能在IE等浏览器中运行,所以我们希望做一些兼容性处理,类似Eslint:检查代码格式;Babel:代码兼容性处理)

生产模式介绍
├── webpack-test (项目根目录)
    ├── config (Webpack配置文件目录)
    │    ├── webpack.dev.js(开发模式配置文件)
    │    └── webpack.prod.js(生产模式配置文件)
    ├── node_modules (下载包存放目录)
    ├── src (项目源码目录,除了html其他都在src里面)
    │    └── 略
    ├── public (项目html文件)
    │    └── index.html
    ├── .eslintrc.js(Eslint配置文件)
    ├── babel.config.js(Babel配置文件)
    └── package.json (包的依赖管理配置文件)
devServer

应用场景:

当我们创建了一个HTML文件,并在里面写入 Hello world, 这个时候我们使用webpack命令,对这个html进行打包, 并看打包后的html效果,页面中会呈现出 Hello world,但是当我们再次修改原文件html为 Hello Hello world, 这个时候我们需要重新使用webpack命令进行打包,然后才能看到打包后的html效果。每当我们修改的频率越来越多了, 这样的重复动作会使我们的效率十分低下,这个时候就需要webpack提供的devServer了。

提高开发效率的一个配置,它可以帮助我们自动的打包代码,开发者只需要进行开发源代码就可,不用多次webpack。

TypeScript

typeScript是javascript的、带有类型超集,并且能够编译为普通的JavaScript。

编译:TypeScript编译器本身是不能够在nodejs或者浏览器下运行的,需要typescript编译器将其编译成普通的js

带有类型:要求变量有确定的类型,在编写代码时就已经确定了变量的类型,且不能随意赋值给不同类型的变量。

let str: string = "hello";
str = 10; // error

超集:typescript本身支持所有的js语法,并在此基础上添加了额外的功能和特性,这样就使得所有的JavaScript代码能够完全被typescript编译。

虚拟DOM和Diff算法

虚拟DOM是表示真实DOM的JS对象

真实DOM

<div class="container">
    <p class="item">
        思学堂
    </p>
    <strong class='item'>课程很精彩</strong>
</div>

虚拟DOM

let Vnode = &#123;
    tagName: 'div',
    props: &#123;
        'class': 'container',
    &#125;,
    children: [
        &#123;
            tagName: 'p',
            props: &#123;
                'class': 'item',
            &#125;
            text: '思学堂'
        &#125;,
        &#123;
            tagName: 'strong',
            props: &#123;
                'class': 'item',
            &#125;
            text: '课程很精彩'
        &#125;,
    ]
&#125;

如果修改了真实DOM中的js代码,那么虚拟DOM也会发生变化,如何最小化的进行虚拟DOM的更新呢,就会用到diff算法,进行对比,找到差异之后进行更改即可(最小化的更新视图)。

diff算法是一种比对算法,它可以进行精细化的比对,比对两者是旧虚拟DOM和新虚拟DOM,实现最小量的更新。Diff算法一般都只在同级进行比对(深度优先算法),比对的时候采用首尾指针法。新虚拟DOM的首指针与旧虚拟DOM的首指针进行对比,新DOM首指针与旧DOM尾指针对比,新DOM尾指针与旧DOM首指针对比,新DOM尾指针与旧DOM尾指针对比,如果某一个过程对比上了,首指针右移,尾指针左移,直到首指针在尾指针的右边则结束对比。

diff算法的整个流程

当数据改变时,会触发setter,并且通过Dep.notify去通知所有订阅者Watcher,订阅者们就会调用patch方法,给真实DOM打补丁,更新相应的视图。

15张图,20分钟吃透Diff算法核心原理,我说的!!! - 掘金 (juejin.cn)

上面是diff算法中源码的详解。

使用虚拟DOM算法的损耗计算:总损耗 = 虚拟DOM增删改 + (与Diff算法效率有关)真实DOM差异增删改+(较少节点的重绘与排版)

直接操作真实DOM的损耗计算:总损耗 = 真实DOM完全增删改 + (可能较多的节点)排版与重绘

第四篇:组件化

第十二章:组件的实现原理

组件本质上就是一个 JavaScript 对象,比如,以下对象就是一个基本的组件

// MyComponent是一个组件,它的值是一个选项对象
const MyComponent = &#123;
    name: 'MyComponent',
    data() &#123;
        return &#123; foo: 1 &#125;
    &#125;
&#125;

对于组件而言,同样需要使用 vnode 来进行表示,当 vnodetype 属性是一个 自定义对象 时,那么这个 vnode 就表示组件的 vnode

// 该vnode用来描述组件,type属性存储组件的选项对象
const vnode = &#123;
    type: MyComponent
    // ...
&#125;

组件的渲染,本质上是 组件包含的 DOM 的渲染。 对于组件而言,必然会包含一个 render 渲染函数。如果没有 render 函数,那么 vue 会把 template 模板编译为 render 函数。而组件渲染的内容,其实就是 render 函数返回的 vnode。具体的渲染逻辑,全部都通过渲染器执行。

组件名就相当于html中的标签,通过在html中使用组件名,达到复用组件的功能。

组件总共分为四个部分:1、组件注册 2、组件通信 3、组件插槽 4、内置组件

组件注册

全局注册

全局注册的组件在注册后可以用于任意实例或者组件中。

Vue.component('组件名', &#123; /* 选项对象 */ &#125;);

全局注册必须设置在根Vue实例创建之前。

<body>
  <!-- 这个div的id设置为app说明是通过Vue创建的视图 -->
  <div id="app">
    <p>这是一个p标签</p>
    <my-component></my-component>
  </div>
  <!-- 现在使用的是vue2版本 -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.js"></script>
  <!-- <script src="./vue.js"></script> -->
  <script>
    // 注册全局组件
    Vue.component('my-component', &#123;
      template: '<div>这是全局组件</div>'
    &#125;);

    new Vue(&#123;
      el: '#app',
      data: &#123;

      &#125;
    &#125;)
  </script>
</body>

组件基础

组件就是html标签的一个形式,是可复用的Vue实例,所以它们可与new Vue接收相同的选项,例如data、mehods以及声明周期钩子等。

但是像el这样根实例特有的选项不能在组件中使用,因为el代表的是挂载的元素,根实例是需要挂载到页面上的元素的,而组件是被根实例或者其它组件使用的,它不需要直接挂载到我们的页面中。

  • 组件命名规则

    kebab-case:‘my-component’(短横线连接多个单词,建议,因为在DOM中引入组件时只能按照这种方式引入)

    PascalCase:‘MyComponent’ (每个单词部分的首字母大写)

  • template选项

    template选项用于设置组件的结构,最终被引入根实例或者其它组件中。

    Vue.component('my-com-b', &#123;
          // 注意:这里模板里里面可以使用反引号添加变量,且只能有一个根标签div
          template: `
            <div>
              这是组件b的内容: &#123;&#123; 1+2*3 &#125;&#125;  
            </div>
          `
        &#125;)
    
  • 单项数据流(父组件向子组件传值)

    子组件接收父组件的传值使用prop属性接收

    父子组件间的所有prop都是单项下行绑定的(说人话就是只能从父组件向子组件传值,子组件不能影响父组件,父组件的值修改后子组件的值也都会相应的修改,但是子组件不能直接修改父组件传过来的值,而需要进行保存在data中来修改,且修改后对父组件的值没有任何影响。)

    如果子组件要处理prop数据,应当存储在data中后操作。

    // 在html中使用组件标签时
    <my-com-c
    :initial-title = "title"
    ></my-com-c>
    
    // 在script中编写组件时
    Vue.component('my-com-c', &#123;
        props: ['initialTitle'],
        template: `
            <div>
              &#123;&#123; title &#125;&#125;  
              <button @click = "fn">button</button>
            </div>
          `,
        data() &#123;
            return &#123;
                title: this.initialTitle
            &#125;
        &#125;,
        methods: &#123;
            fn() &#123;
                this.title = "这是一个新的标题!"
            &#125;
        &#125;
    
    &#125;)
    new Vue(&#123;
        el: '#app',
        data: &#123;
            title: "这是一个标题"
        &#125;
    &#125;)
    
  • data选项

    data选项用于存储组件的数据,与根实例不同,组件的data选项必须为函数,数据设置在返回值对象中。

    使用函数的实现方式主要是为了确保每个组件实例可以维护一份被返回对象的独立的拷贝,不会相互影响。

    因为组件在页面中不一定是单个,它可以进行多次数的复用,为了确保多个复用组件数据的独立,这时就需要作用域进行隔离,如果data是一个函数,而函数都有单独的作用域,因此就算使用了多个复用组件,它们之间的数据也是相互独立开的,不会互相影响。

局部注册

局部注册的组件只能用在当前实例或者组件中。

<body>
    <my-com-d></my-com-d>
    <script>
    new Vue(&#123;
        el: '#app',
        data: &#123;
            title: "这是一个标题"
        &#125;,
        // 开始编写局部组件
        components: &#123;
            'my-com-d': &#123;
                template: `
              <div>
                <h3>&#123;&#123; title &#125;&#125;</h3>
                <p>&#123;&#123; content &#125;&#125;</p>
              </div>
              `,
                data() &#123;
                    return &#123;
                        title: '组件标题',
                        content: '组件内容'
                    &#125;
                &#125;
            &#125;
        &#125;
    &#125;)
    </script>
</body>

组件通信

组件间传递数据的操作,称为组件通信。

父组件向子组件传值

使用props进行传值,有静态属性设置、动态属性绑定

<!-- 这个div的id设置为app说明是通过Vue创建的视图 -->
      <div id="app">
        <!-- 这里的 : 是 v-bind: 的缩写,用来绑定值 -->
        <!-- 动态属性绑定: 常用操作 -->
        <my-component-a 
          :title="item.title"
          :content="item.content"
        ></my-component-a>
        <!-- 静态属性设置 -->
        <my-component-a
          title="这是静态标题"
          content="这是静态内容"
        ></my-component-a>
      </div>
    <script>
    // 全局注册一个子组件 从根组件向子组件传值
    Vue.component('my-component-a', &#123;
      props: ['title','content'],
      template: `
      <div>
        <h3> &#123;&#123; title &#125;&#125; </h3>
        <p> &#123;&#123; content &#125;&#125; </p>  
      </div>
      `
    &#125;)
    // 新建一个Vue实例(也叫做根实例)作为父组件 绑定id为app
    new Vue(&#123;
      el: '#app',
      data: &#123;
        item: &#123;
          title: '这是示例标题',
          content: '这是示例内容'
        &#125;
      &#125;
    &#125;);
  </script>

props命名规则

建议prop命名使用camelCase,父组件绑定时使用kebab-case

子组件向父组件传值

通过自定义事件实现,$emit()触发自定义事件,$emit是Vue实例的一个方法,它内部有一个名称,它会触发指定名称的自定义事件,名称可以自己设置。

自定义事件名称建议使用kebab-case

<!-- 这个div的id设置为app说明是通过Vue创建的视图 -->
  <div id="app">
    <h3>购物车</h3>
    <product-item
      v-for="product in products"
      :key = product.id
      :title = product.title

      @count-change="totalNum++"
    ></product-item>
    <p>商品总个数为&#123;&#123;totalNum&#125;&#125;</p>
  </div>
<script>
    // 创建全局注册组件 
    Vue.component('product-item',&#123;
      props: ['title'],
      template: `
        <div>
          <span>商品名称:&#123;&#123; title &#125;&#125;,商品个数:&#123;&#123; count &#125;&#125;</span>
          <button @click='countIns'>+1</button>
        </div>
      `,
      data() &#123;
        return &#123;
          count: 0
        &#125;
      &#125;,
      // 添加方法,使得按下+1之后每个数据可以增加
      methods: &#123;
        countIns() &#123;
          this.count++;
          // 添加自定义事件,表示如果发生了countIns事件
          // 就会触发count-change事件
          this.$emit('count-change');
        &#125;
      &#125;
    &#125;)
    // 新建Vue实例
    new Vue(&#123;
      el: "#app",
      data () &#123;
        return &#123;
          // 添加数据
          products: [&#123;id: 1, title: '苹果'&#125;, &#123; id: 2, title: '香蕉'&#125;, &#123; id: 3, title: '橙子'&#125; ],
          totalNum: 0
        &#125;
      &#125;
    &#125;)
  </script>

自定义事件,可以传递参数

this.$emit(‘count-change’, 1);

参数直接接收使用$event

@count-change = “totalCount += $event”

也可以直接设置函数接收

@count-change = “onCountChange”

onCountChange(productcount) {

​ this.totalCount += productCount;

}

非父子组件传值

非父子组件指的是兄弟组件或者完全无关的两个组件。

兄弟组件传值

通过父组件进行数据中转

<!-- 这个div的id设置为app说明是通过Vue创建的视图 -->
  <div id="app">
    <!-- 将coma中的值传递到comb中 -->
    <com-a
      @change-a="value = $event"
    ></com-a>
    <com-b
      :value="value"
    ></com-b>
  </div>
<script>
    Vue.component('ComA',&#123;
      template: `
        <div>
          &#123;&#123; value &#125;&#125;
          <button @click='$emit("change-a", value)'> 发送</button>
        </div>
      `,
      data() &#123;
        return &#123;
          value: "这是组件A的内容"
        &#125;
      &#125;
    &#125;)
    Vue.component('ComB',&#123;
      props: ['value'],
      template: `
        <div>
          &#123;&#123; value &#125;&#125;
        </div>
      `
    &#125;)
    new Vue(&#123;
      el:"#app",
      data() &#123;
        return &#123;
          // 用于数据中转
          value: ''
        &#125;
      &#125;
    &#125;)
  </script>
EventBus(事件总线)

当组件嵌套关系复杂时,根据组件关系传值会比较繁琐。组件为了数据中转,data会存在许多与当前组件功能无关的数据。

EventBus是一个独立的事件中心,用于管理不同组件间的传值操作。它通过一个新的Vue实例来管理组件传值操作,组件通过给实例注册事件、调用事件来实现数据传递。

// Eventbus.js
var bus = new Vue();

原理:发送数据的组件触发bus事件,接收的组件给bus注册对应事件。

视频进展

EventBus_哔哩哔哩_bilibili

其它传值方式

第十三章:异步组件与函数式组件

异步组件,指的是: 异步加载的组件

函数式组件指的是 没有状态的组件。本质上是一个函数,可以通过静态属性的形式添加 props 属性 。在实际开发中,并不常见。

第十四章:内建组件和模块

keepAlive

非常常用的内置组件。它可以 缓存一个组件,避免该组件不断地销毁和创建

原理:

主要围绕着 组件卸载组件挂载 两个方面:

  • 组件卸载:当一个组件被卸载时,它并不被真正销毁,而是把组件保存在一个容器中
  • 组件挂载:因为组件被保存了。所以当这个组件需要被挂载时,就不需要在重新创建,而是直接从容器中获取即可。

Teleport

Teleportvue 3 新增的组件,作用是 Teleport 插槽的内容渲染到其他的位置。比如我们可以把 dialog 渲染到 body 根标签之下。

它的实现原理,主要也是分为两部分:

  1. 把 Teleport 组件的渲染逻辑,从渲染器中抽离
  2. 在指定的位置进行独立渲染

Transition

Transition 是咱们常用的动画组件,作用是 实现动画逻辑

其核心原理同样被总结为两点:

  1. DOM 元素被挂载时,将动效附加到该 DOM 元素上
  2. DOM 元素被卸载时,等在 DOM 元素动效执行完成后,执行卸载 DOM 操作

小结

清楚组件的原理和内建组件原理即可