分类 前端 下的文章

剑指题解


互联网人题解神器,神器在手,offer我有

随手刷题 | 无广告绿色免费 | 个性推荐 | 每日自动推送 | 多维度丰富领域知识

项目的起源是一个 github 仓库,名为前端知识每日 3+1,正是这个优秀的题库项目才有了我们团队开发聚合题库的想法。在我们国内程序员圈子,大多数小伙伴为了更好的技术发展和 offer,都通过训练题来提高自己的眼界,其中不乏包括算法/业务/软技能的题目,但是因为我们的环境和侧重点不一样,所以我们接触的项目和题目都不一样,而市面上很少有一个项目能做到,涵盖不同圈子的聚合题库。 《剑指题解》项目就是一个优秀的互联网行业聚合题库神器。


开源免费不易,请关注我们的公众号以及开源仓库

沈昊Seho/InternetQuestionBank<br/>
Github 点 star, 拉取代码时请选择最新分支
Dcloud 插件市场支持我们

目录

特点

  1. 汇聚优秀的开源题目,以技术社区为主线展开的刷题 APP
  2. 根据用户的喜好,去推送相关的最新题目,不会再刷到 “牛头不对马嘴”无用的题目
  3. 我们绑定了服务号,每日定时推送给用户最新的题目,让你能在每天清晨就开启活力新一天
  4. 刷题 APP 无广告,无硬性推广,一切盈利皆在赞助/官网流量
  5. 我们还有很多有意思的待开发 feature,比如大佬带萌新模式,还将开启校园/企业的通道,让每一个学生/员工都能每时每刻提高自己
  6. 剑指题解开源项目,从原型图到小程序,UI 图,UML,API 文档,第三方 API 对接说明书,使用说明都将免费开源,欢迎各位同僚二开,让这个行业变得更好,我们将用心地辅助你们解决在技术上的难题。

技术栈

关于技术栈的选型可以参考我们的语雀文档
周边物料的开源,还需要感谢 mockPlus 以及语雀:

<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/69752764374919.png" width="10%">


<img class="imgIcon" src="http://static.yinzhuoei.com/typecho/2021/01/17/697832282748067.jpg" width="20%"></img>

UML

由于此部分在开发新 feature 的工作中可能会进行变更或者本就有实质性的错误,欢迎向我们反馈错误,我们将感激不尽。

类图

拉取任务

更多

更多相关资料请移步
out/doc或者语雀-UML

设计概览



设计源文件请移步:点击查看-为了您的体验,请务必下载最新版本的设计图
原型图在线预览: 点击查看, 如果失效请联系我们

API

《剑指题解》团队在 API 层面,开源的内容非常有限制,因为涉及到多位合作伙伴的题库授权,所以如果您要使用聚合题库 API 请联系我们商议,如果您是学习者,那么在语雀中的 API 文档会帮助到您,在这里我们将简单介绍。

uniapp 的云函数开发,我们是第一次接触,所以在此之前我们做了很多调研,在下文也提到了关于 explain.js 这个框架的作用,我们也是非常感谢 uniapp 开源社区有这样创作质量的第一批的 unicloud 框架开发者,在咨询了官方人员之后,我们了解到如果要使用 ts 来开发,需要把 ts 编译成 js 来放在云函数目录,所以我们选择了以下的技术栈,开发者可以很爽的使用 ts 来开发 unicloud 云函数:

  • esbuild-node-tsc
  • nodemon

esbuild-node-tsc 这个工具是基于 esbuild 的,利用其特性,我们可以比 ts 官方提出的 tsc 编译工具更快,快几十倍到上百倍,这也是得益于 esbuild 的使用 go 语言直接编译的特性。

nodemon 可以帮助我们监听目录文件,一旦改变就执行打包 ts 命令直接将编译好的 js 放在指定目录
所以我们把 explain.js 的配置也相应的改变,我们在 services 目录中存储的是我们 ts 文件,在 dist 中存储的是我们编译好的 js 文件,而 dist 目录就是 explain 需要的:

config.init({
  baseDir: __dirname,
  serviceDir: "/dist/",
});

需要注意的事情是:
由于云函数大小限制,我们需要把相关依赖全部安装到全局,包括 typescript

读写职责分离

读写职责分离模式(CQRS)是一种把查询(Queries) 数据和和更新(Commands) 数据通过使用各自独立的接口分开的模式。
Uniapp 的 Unicloud 很好,其中的 jql 也是大开眼界,确实统一了前端开发操作数据库的体验,让我们前端开发操作数据库非常友好,但是我们使用 CQRS 将这些原本封装的很好的 API 不予以使用。

  • 前端使用 jql 去读取想要的数据
  • 写入操作就交给云函数去执行

如图,前端读取一个列表数据

const db = uniCloud.database();
const databaseName = "testInit";
// 获取测试数据列表
export function getTestList() {
  return new Promise((resolve) => {
    db.collection(databaseName)
      .get()
      .then((res) => {
        // res 为数据库查询结果
        resolve(res);
      })
      .catch((err) => {
        // err.message 错误信息
        // err.code 错误码
      });
  });
}

我们将这样的文件抽出了一个一个查询模块,每一个模块对应了一个数据表(这里是 testInit)封装在了 API 这个文件夹中。

Vue 页面将这样去调用,这样我们在 API 查询层将做好数据的处理,确保 Vue 拿到的是可以直接渲染的干净数据。

import { getTestList } from "../../api/test";
const data = getTestList();

我们写入操作将通过云函数直接调用,唯一设计不同的地方在于,我们将云函数这个概念改变了,应该是模块化,我们把一个一个函数变成了模块。

在我们的初步技术调研过程中,由于 Uni 官方并没有提供给用户云函数开发框架,但是我们在插件市场中找到了一款名为 explain 的开发框架,它可以迅速的帮助我们实现 restapi 风格的单路由云函数,这款框架我们不多做介绍,文档在这里:explain.js 快速开发 uni 云函数的框架

// 注册用户根据手机号
  addUserByPhone() {
    return handleMustRequireParam(
        [{
            key: "username",
            value: "用户名",
          },
          {
            key: "password",
            value: "密码",
          },
        ],
        this.event.params
      )
      .then(async () => {
        const {
          username,
          password
        } = this.event.params;
        if (!/^1[3456789]\d{9}$/.test(username)) {
          return appErrorMessage("用户名格式不正确");
        } else if (password === "" || password.length < 6) {
          return appErrorMessage("密码格式不正确");
        } else {
          // 校验手机号
          return await uniID.register({
            username,
            password,
          });
        }
      })
      .catch((err) => {
        return err;
      });
  }

那么我们如果要调用 testPrint 这个模块中的增加操作

uniCloud.callFunction({
  name: "application",
  data: {
    route: "api/user",
    method: "POST",
    params: {
      username: "18291563764",
      password: "sas",
    },
  },
});

贡献者

感谢为《剑指题解》这个优秀项目贡献自己一份力的小伙伴们:

如果您想加入到我们的贡献者队列中,请联系我们,这里还有相关贡献者的介绍,希望对您有帮助

核心维护者

相关开源物料

题库合作伙伴

QQ 群和钉钉群欢迎大家加入


许可

Apache License © 剑指题解
如果您在其他有关我们的文档见到了与之不符合的协议内容,请联系我们,这可能是我们的历史遗留的代码问题。

微信截图_20201212173122.png
最近一直在调研unicloud云函数开发,所以一直想给js加入类型推导,这篇文章就记录一下我是如何开发TS版本的云函数的吧。

  1. 确定使用TS还是Flow.js

Flow.js在基本语法上和TS很相像,我认为它是一个针对老项目的类型推导方案,因为只需要安装简单的包和给文件加入Flow的标识就可以给对应的文件提供类型推导的功能,所以针对目前我所做的这个业务来讲,不存在老项目,所以既然是新项目就不如直接上TS。

在云函数开发过程中,我们在插件市场选择了一款非常简洁已拓展的explain框架,这个框架目前已经支持单路由和restfulAPI还有基本的过滤拦截器,那么目前这个框架没有做TS的解决方案,我就斗胆替作者大大想一个曲线救国的方案,而且这个方案有以下特点:

  1. TS的编译速度比官方提供的TSC编译要快几十倍到上百倍
  2. 支持重新加载打包
  3. 云函数项目0依赖就可以玩转这套方案

uni官方的云函数大小限制是10M,所以我们不能把依赖都安装在项目中,需要我们全局安装:

npm i -g typescript esbuild-node-tsc nodemon
  1. esbuild-node-tsc是基于esbuild的ts编译器,大名鼎鼎的esbuild由于其出色的编译特性,能够让我们在大型项目中编译ts速度更快。
  2. nodemon帮助我们在文件变更时重新编译ts

2个插件的玩法很多,尤其是nodemon,在我们这个解决方案中我们只需要简单的配置几个文件就可以把我们的项目跑起来了。

微信图片_20201212171411.png

我们的云函数目录是这样的,这是搭配了前面提到的explain.js,在etsc.config.js中我们可以配置一下,输出的js版本规范以及目录和是否进行压缩:

module.exports = {
  outDir: "./dist",
  esbuild: {
    minify: true,
    target: "es2015",
  },
  assets: {
    baseDir: "services",
    filePatterns: ["**/*.json"],
  },
};

在services目录中编写完ts文件之后,esbuild-node-tsc会把js文件放到dist目录之下,我们现在只需要更改explain.js默认配置:

config.init({
  baseDir: __dirname,
  serviceDir: "/dist/"
});

这样explain会从dist下找文件而不是从services文件下找

我们在这个根目录下运行编译命令即可

etsc

这个时候我们运行这个函数就会发现,它已经达到了我们的目标了:

  1. 开发使用了ts
  2. 可以正常的跑通和上传云函数

但是我们需要services下的文件一变更就编译放到dist下,我们就需要nodemon帮助我们做这个事情(nodemon.json):

{
    "watch": ["services"],
    "ext": "ts,js,json",
    "exec": "etsc",
    "legacyWatch": true
  }

监听services目录,包括文件名为ts,js,json,执行命令etsc

然后我们再把这个运行nodemon的命令放到package.json中:

"scripts": {
    "dev": "nodemon"
  },

这样我们就可以启动nodemon这个监听服务,可以很爽的使用ts来开发云函数啦~~
微信截图_20201212172316.png

微信截图_20210331210548.png

从0开始理解Vite的主要新特性(一)

vite这个工具确实尤大在微博上造势很猛,又以各种骚操作着实火了一把,那我们今天就一起了解一下vite吧~



我们可以从vite仓库的readme可以看到安装vite非常的方便

// https://github.com/vitejs/vite
npm init vite-app <project-name>
cd <project-name>
npm install
npm run dev

我们在本地环境运行之后可以看到这样的页面,就说明我们可以开始使用了。




我们首先要理解vite的工作原理,它为什么这么快?



当我们打开工程的index.html的时候,我们可以发现script的type是module

<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>


如果你不了解script标签中的module是什么意思,那么MDN解释说如果标示了module的话会把代码当作js模块来执行,一篇关于es6的文章也很好的介绍了:

https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/script

如果你熟悉es6的模块概念,模块仅仅就是普通的含有js代码的文件而已,我们可以用importexport关键字对变量,对象的导出和导入,而这个机制在高级浏览器已经完全实现了。



不得不说,尤大不仅是技术上的大神,而且富有创造和想象力。让vite变成了一个0捆绑的开发服务器,利用浏览器高级特性让开发体验变得更好

当浏览器解析保留了模块关键字的代码,从而会导致HTTP请求,vite通过koa拦截了这些请求:

.vue => 拦截请求 => 编译 => 返回给客户端

那么没有打包的vite和老东家的基于打包的vue-cli就有了一些明显的优势了:

1. vite利用了客户端能力不用打包其他服务,原生的ES Import直接输出提高了冷启动速度。

2. vite按需编译当前页面需要的组件,而不需要打包整个APP的组件,这样的提升对比cli无疑是项目越大速度差距越大。

3. HMR更新速度不会和模块数量牵扯,vite会让HMR一如既往的保持快速。

使用vite开发应用可能在前期除了启动速度,其他功能是要等到应用慢慢变大才能真实的感受vite的强大。


TS支持

vite内置TS的支持,开箱即用:

<script lang="ts">
import { ref } from "vue";

export default {
  name: 'App',
  setup(){
    let hello = ref<string>("1");
    console.log(hello.value);
  }
}
</script>

值得一提的是,内置的TS不是TS官方出的tsc cli,而是之前就听说过的ESBuild,现在vite的TS支持是ESBuild也不奇怪,毕竟是要一快到底么。

——ESBuild的ReadMe说了一句振奋人心的话

实际上,我们目前的网站构建工具比实际速度慢10-100倍



为什么ESbuild这么快?

  • 因为Esbuild是用GO语言直接编译成原生代码
  • 由于GO语言特点,它的解析和映射并发非常快
  • 避免了不必要的配置
  • 数据转换很简单很快速

虽然ESbuild是一个非常快速的打包器,但是不支持热模块更新和没有开箱即用的工具,而且要像webpack一样想做一款基于ESbuild的插件,我认为目前是非常难的。所以Vite将它的长处用在了处理ts编译上,大型项目中编译TS文件,Vite几乎是一瞬间的事情。


热模块更新

我们要知道热模块更新和我们传统的刷新页面的区别,以webpack的dev-server服务器举例,通过启动开发服务器,页面与服务器建立了websocket,我们修改了代码之后给页面发送消息,页面才会执行刷新命令,本质上这种live-reload机制已经对开发非常友好了,但是在带有状态的页面上,reload不会有更好的开发体验:

当页面存在弹窗或者编辑框等,代码修改之后,liveReload会重载页面,如果刷新代码的同时不会重载页面而是重新加载修改过的文件就完美,所以这个机制就是webpack提出的热替换技术,也就是我们说的热更新

webpack.config.js

module.exports = {
  mode: "development", // 开发环境
  devtool: "cheap-module-eval-source-map",
  devServer: {
    contentBase: "./bundle",
    open: true,
    hot: true, // 开启热模块更新
    hotOnly: true  // 更新失败不会刷新页面配置
  }
  module: {
    rules: [{
      test: /\.css$/,
      use: ["style-loader", "css-loader"]
    }]
  }
}

css的loader中的实现已经做了热更新的处理,通过HMR这个插件中的API

// main.jsif (module.hot) {
  module.hot.accept(function() {
    // 监听变化,则修改
  });
  module.hot.dispose(function() {
    // 移除  });
}

我们了解了HMR基本操作之后就可以看看Vite是如何做HMR的:

vite和webpack的HMR实现机制是一样的,都是通过客户端和服务端建立socket连接,服务端有变化则通知客户端做出改变:

 // server/serverPlugin.ts
 watcher.on('change', (file) => {
    if (!(file.endsWith('.vue') || isCSSRequest(file))) {
      handleJSReload(file);
    }
  })

在handleJSReload函数中递归调用了walkImportChain这个函数,这个函数的作用就是查看当前变化的文件是谁引入了它(JS/Vue),那么在递归中没有找到谁引入它,就Full-reload

 send({
     type: 'full-reload',
     path: publicPath
  })

如果找到了引用这个JS的文件了就热更新:

send({
    type: 'multi',
    updates: boundaries.map((boundary) => {
      return {
        type: boundary.endsWith('vue') ? 'vue-reload' : 'js-update',
        path: boundary,
        changeSrcPath: publicPath,
        timestamp
      }
    })
  })

在客户端中,vite则在核心处理函数handleMessage中定义了消息的类型:

  • connected: websocket连接
  • vue-reload: vue文件的script更新
  • vue-rerender: vue文件的template更新
  • style-update:css样式表变更
  • style-remove: css样式表移除
  • js-update: js文件更新
  • full-reload:刷新页面

客户端接受了不同的消息类型去做不同处理,根据timestamp时间戳去请求新文件,而vue文件则通过HMRRuntime更新。


裸模块导入

vite同样也支持其他家打包器的日常功能,浏览器不允许我们直接引入裸模块,例如:

import { add } from "lodash"

vite在裸模块处理上有着对vue得天独厚的优势,vite不仅仅的可以改写普通模块的路径然后正确的解析,还对vue这个依赖有特殊的处理:

如果项目本地没有安装vue依赖,那么引入vue模块会按照vite依赖的vue版本去执行,这就说明了如果你全局安装了vue,那么在vite项目中能更方便的找到它

vite重写了模块加载路径:

 // src/node/server/serverPluginModuleRewrite.ts
 ctx.body = rewriteImports(
    root,
    content!,
    importer,
    resolver,
    ctx.query.t
  )

引入的模块上下文在经过rewriteImports方法处理body以后,就会造成这样的效果:

import vue from "vue" => import vue from "@/modules/vue.js"

rewriteImports这个方法中使用到了Esbuild中的es-module-lexer进行词法分析,对esbuild本来就不熟悉的我去看了这个插件发现这个词法分析器又小,又可以快速对JS进行分析,这里就简单看一下官网的demo吧。

// 伪代码
// 和vite源码中一样
import {   
  init as initLexer,   
  parse as parseImports,   
  ImportSpecifier   
} from 'es-module-lexer' 

// 需要初始化
await initLexer;
const [import, export] = parseImports(`
  import {a} from "a"
  export const add = 1;
`
console.log(import[0].s); 
// 解析结果的返回有这样几种
// "s" is shorthand for "start"
// "e" is shorthand for "end"
// "ss" is shorthand for "statement start"
// "se" is shorthand for "statement end"

经过这样的词法处理,我们的模块引入路径就被这样替换了,尽管这个插件这么强,对于词法分析这种东西我们开发者平时也用不到,所以大家只需要知道vite的模块路径替换是借助es-module-lexer进行词法分析的就可以啦~


总结

vite和webpack对比,我认为webpack是一个纯正的打包工具,它的生态非常丰富,可以基于插件做各种事情,但是像尤大说的一样,很少有基于webpack上层封装的工具出现,也就是具有很大学习和配置成本,而vite提供了更丝滑的开发体验,以及内置的强大的HMR,TS支持,WebAssembly支持等等,所以使用哪款产品要看业务需要。


由于vite目前还在不断的更新中,但是主要特性的原理应该是不会变的,因为vite有非常多优秀的其他特性,还有我们这篇文章提到的3个特性还有很多值得细细研究的地方,所以这个vite系列会继续做,谢谢大家支持哦

好家伙,他闭着眼睛写Vue3

一个风和日丽的下午,我用了3个小时写了一个Vue3的小应用,这个应用小到不足为奇,但是Vue3的开发体验最值得我吹一波,这个小应用在登录我用了Vuex4,路由管理用到了最新的VueRouter4,而UI框架选择的是Vant3,有赞团队的Vant3最新版也是目前为止Vue3支持度最高的移动UI库,放一个项目预览GIF图

整个的开发体验,我总结了一下:有近似ReactHook的开发体验,又保留了Vue2的原汁原味,同时因为Vue3的本身设计原因,也提升了在react中没有的hook开发体验,所以如同标题,如果开发者拥有reactHook开发经验和vue2开发经验,可谓是“闭着眼睛”都能撸Vue3。

Github地址:https://github.com/1018715564/PlanListForVue3


main.js

新版的vue一改以前的构造函数去挂载,而是把函数风格贯彻到底了,在入口文件中我们使用链式调用,一直use一直爽。

import { createApp } from "vue";
import App from "./App.vue";
import Router from "./router";
import Vuex from "./store";
import Vant from "vant";
import "vant/lib/index.css";

createApp(App).use(Router).use(Vant).use(Vuex).mount("#app");

Router

import { createRouter, createWebHistory } from "vue-router";
import Login from "../pages/Login/Index.vue";
import Index from "../pages/Index/Index.vue";
const routerHistory = createWebHistory();
const Router = createRouter({
  history: routerHistory,
  routes: [
    {
      name: "Index",
      path: "/",
      component: Index
    },
    {
      name: "Login",
      path: "/Login",
      component: Login
    }
  ]
});

export default Router;

可以看到Router还是保留了vueRouter3的路由配置,但是创建路由的方式改为了createRouter,原先的路由模式也从原来的mode: "history"也改为了通过函数引入:

"history":createWebHistory()
"hash":createWebHashHistory()
"abstract":createMemoryHistory()

Vuex

import Vuex from "vuex";
export default Vuex.createStore({
  state: {
    user: null,
    planList: []
  },
  mutations: {
    // 删除一个计划
    deletePlan(state, index) {
      state.planList.splice(index, 1);
    },
    // 新增一个计划
    addPlan(state, plan) {
      state.planList.push(plan);
    },
    setUser(state, user) {
      state.user = user;
    }
  },
  actions: {},
  modules: {}
});

我们通过vuex4中的createStore创建了实例,其中我们定义了一些方法和state,这些我们在登录和添加删除计划时会用到,下来我们再看看如何在页面中使用Vue3的组合API以及路由和状态管理。

登录页面

import { reactive } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { Toast } from "vant";
export default {
  name: "App",
  setup() {
    const store = useStore();
    const router = useRouter();
    // 用户名和密码
    const Form = reactive({
      username: "",
      password: "",
    });
    // 登录
    function handelLogin() {
      store.commit("setUser", {
        username: Form.username,
        password: Form.password,
      });
      Toast(`登录成功,你好: ${Form.username}, 请添加你的计划吧~`);
      // 跳转到首页
      router.push({
        path: "/",
      });
    }
    return {
      Form,
      handelLogin,
      handelNavBack,
    };
  }

我们使用路由以及状态管理需要从2个包中引入对应的hook去调用

调用vuex中的mutation:

store.commit(fnName, args);

路由也沿用了VueRouter3中一些老API,replace, go, back等方法都基本不变

 const router = useRouter();
 router.push({
   path: "/",
 });

如果从来没有使用过组合API的开发者,应该需要了解一下前置知识,例如在老版本中,我们定义变量和方法是这样:

data(){
  return {
    count: 0
  }
},
methods: {
  back(){
    return "It is back"
  }
}
{{count}} //在template中这样渲染

我们在vue3中需要把这些东西写到setup这个函数中,包括变量,函数,监听,计算属性,生命周期等等,然后把变量return出去供模板使用,很显然我们在登录时候用了reactive这个API,它可以传入一个普通对象,返回一个响应式的代理,我们用这个对象中的用户名和密码与视图绑定最后调用vuex的方法即可让全局状态管理知道此时 “计划APP” 是有身份进入的。

const Form = reactive({
  username: "",
  password: "",
});

// 调用vuex登录方法
store.commit("setUser", {
  username: Form.username,
  password: Form.password,
});

首页

import { ref, reactive, computed, watchEffect } from "vue";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { Toast, Dialog } from "vant";

export default {
  name: "Index",
  setup() {
    const store = useStore();
    const router = useRouter();
    const planList = store.state.planList;
    const addPlanForm = reactive({
      title: "",
      remark: "",
    });
    // 添加的弹出层
    const addPopup = ref(false);
    // 计算计划的个数
    const planCount = computed(() => store.state.planList.length);
    // 如果没登录重定向到登录
    if (store.state.user === null) {
      Toast("未登录,请先登录");
      router.replace("Login");
    }
    const handelNavBack = () => {
      router.go(-1);
    };
    const handelAddPlan = (e) => {
      store.commit("addPlan", {
        ...e,
        id: Math.random().toString(36).substr(2),
        date: new Date().toDateString(),
      });
      Toast("添加计划成功");
      addPopup.value = false;
      addPlanForm.title = "";
      addPlanForm.remark = "";
    };
    const handelDeletePlan = (index) => {
      Dialog.confirm({
        title: "提示",
        message: "您缺点要删除此计划吗?",
      }).then(() => {
        store.commit("deletePlan", index);
        Toast("删除计划成功");
      });
    };
    return {
      handelNavBack,
      planList,
      addPopup,
      addPlanForm,
      handelAddPlan,
      handelDeletePlan,
      planCount,
    };
  },
};

引入了vuex和router的hook之后,我们导出了vuex和router的实例,从这个实例中我们可以获得到vuex中的state,则可以判断APP是否有登录用户,如果没有登录就重定向到登录页面。

const store = useStore();
if (store.state.user === null) {
  Toast("未登录,请先登录");
  router.replace("Login");
}

添加计划额外传递id和时间

store.commit("addPlan", {
    ...e, // Form表单的回调,是计划标题和计划备注
    id: Math.random().toString(36).substr(2),
    date: new Date().toDateString(),
  });

使用refAPI将传入的参数返回其响应式代理

// 控制弹窗显示/隐藏的变量
const addPopup = ref(false);

如果传入的是一个对象,那么Vue内部自动会调用reactive,值得注意的是如果需要更改此响应变量,需要对响应式对象中的value属性进行更改。

响应式变量在template渲染时我们不需要写.value

计划APP的全部代码都已经梳理完毕,我们可以在这个APP中学到,状态管理,路由,组合常用的API的简单应用,在组合API中还有一些我们以前经常用到的API,比如计算属性,生命周期,watch等等在组合API拓展阅读中有简单的总结。


组合API拓展阅读

  • computed

计算属性在vue3中非常简单和vue2中如出一辙,在新版本中计算属性需要从vue引入:

import { computed } from "vue"

setup(){
  const planList = computed(() => state.list);
}

计算属性默认传入一个get函数,当然可以像以前一样传入一个set函数

 const planList = computed({
  get(){
    return state.list
  },
  set(list){
    state.list = list
  }
})

planList.value = []; // 重新set值
  • watchEffect

这个Hook非常简单,如果使用过reactHook的useEffect应该非常好理解,如果这个函数所依赖的内容变化,它会重新执行这个函数。

const countEffect = watchEffect(() => {
  console.log(count); // 定义的响应式变量,如果该变量有变更,将会重新打印count
});

当组件卸载或者调用返回中的stop方法即可停止监听,这个机制和vue2中的watch是一样滴。

watchEffect我们称之为副作用函数,我们的业务场景中如果有好友列表,鼠标移动上去能异步获取好友详情,那么如果在鼠标在移动新好友之后,上一次好友详情的请求还没结束,这可能会造成数据混淆的Bug,所以我们副作用函数支持我们这样清除副作用:

watchEffect((onInvalidate) => {
  const detail = getFriendDetail(id.value)
  onInvalidate(() => {
    // id 改变时 或 停止侦听时
    detail.cancel() // 取消请求
  })
})

第一遍看文档时感觉这样清除副作用很麻烦,React是这样清除的:

useEffect(() => {
  // do something
  return () => {
    // 清除副作用
  }
})

Vue设计副作用返回是通过传入一个回调注册清除函数,是因为使用effect钩子往往是异步请求,而异步请求返回的是Promise,所以清除函数一定要在被resolve之前注册,当然文档还说这样的好处还有可以自动帮助我们处理Promise潜在的错误(这个就是后话了嘻嘻)。

下面就是重头戏了,大胆预测一波,未来关于Vue3面试题必将有它的一席之地:watchEffect的刷新时机是什么?

第一次看文档就有猜测过这样傻白甜的问题,副作用监听了依赖它的变量,是如何很好的控制多个变量触发的机制呢,是每个变量触发都会快速的执行一次吗?

用户自定义的副作用函数会在全局缓存一遍会异步地刷新它们,Vue组件的更新函数也是一个副作用函数,刷新机制是在更新函数之后去一遍一遍走自定义的副作用函数
  • watch

watch和Vue2一样,只不过和watchEffect的区别就是:

1. 仅在依赖数据源变化才会回调副作用函数

2. 可以访问到变化前和变化后的值

3. 可以自由设置哪些值是需要监听的

共同点就是:

1. 清除副作用和停止监听

2. 副作用刷新机制也是一样的哟

const star = ref(0);
watch(star, (star, prevStar) => {
  // do something
});

// 监听多个
watch([star, rose, flower], ([star, rose, flower], [prevstar, prevrose, prevflower]) => {
  // do something
});


结语

这段时间拖得太久,Vue系列还有一个Vite的浅析还没发,也基本差亿点就结束了,因为是国庆节写的,也希望大家多多支持呀,冲鸭!

1_-Ijet6kVJqGgul6adezDLQ.png

在以前的react 开发中我们习惯使用class类的写法写react组件,但是现在随着函数式编程的流行,函数作为js的第一公民,我们在react使用函数编写业务组件,在原来只能写无状态的组件,没有自身的state,hook的出现可以让函数组件钩入一些方法,可以让我们在函数组件中使用state和一些class类的组件写法,那么我们这篇笔记将以react-hook文档为基础来做一些总结。

Hook 简介 - React

State Hook

import React, { useState } from "react";

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  return (
    <div>
      {count}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        增加
      </button>
    </div>
  );  
}

export default Main;

我们可以使用useState这个hook来声明state,返回是一个数组,第一个是我们的变量,第二个是设置这个变量的函数,我们使用数组解构的方式结构出来,那么在这个函数的作用域中直接使用count来展示这个state,我们调用函数直接调用setCount这个函数,这样我们就简单的使用statehook这个钩子来实现class组件this.state这个功能了,注意这个set函数和class中的setState不同,在class组件中的setState中,是要传入当前示例的全部state对象,我们往往要使用对象合并API来操作,但是hook中的set函数只需要传递当前的要设置的值即可。

setCount(100);

我们可以设置更多的state

const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);

我们没有必要使用这样的hook来声明多个变量,在之后的Q&A中,文档会建议我们更好的方法。

Effect Hook

在class写法中的各种生命周期的钩子,在函数组件中也有对应的hook来替代它们,那么这些钩子我们叫做effect hook,在组件渲染成功后通常会进行一些基于业务的操作,所以我们也称之为“副作用hook”,下面我们对比一下class写法和hook写法

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }
  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

在componentDidMount以及componentDidUpdate这两个生命周期中,当组件被挂载或者已有prop或者state变更都会触发对应的document.title的业务逻辑,那么在class这样的写法,同样的逻辑在不同的生命写,这会让代码又臭又长,不好维护,我们使用hook来写,那么就非常简单。

import React, { useState, useEffect } from "react";

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = "你点击了" + count + "次";
  });
  return (
    <div>
      {count}
      <button
        onClick={() => {
          setCount(count + 1);
        }}
      >
        增加
      </button>
    </div>
  );
}

export default Main;

useEffect可以传递一个函数作为入参,我们写一个useEffect就相当于写了组件挂载和更新2个钩子,那么当组件销毁(清除副作用)我们可以这么写

useEffect(() => {
    API.on(); // 比如当页面进入调用订阅API
        return () => {
            API.off(); // 销毁时再取消订阅API
        }
});

useEffect会在每次挂载和更新都会执行我们传入的逻辑,如何控制它将在后面我们会有具体的例子。

那么我们使用effectHook和传统的class中的挂载和更新生命周期还是有区别的。effectHook不会阻塞浏览器的屏幕更新,它会让应用看起来更快。

我们肯定写过下面的逻辑代码:

componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
        // 调用A API
        A();
        // 调用B API
        B();
        // 调用C API
        C();
  }

不得不说,不同的业务逻辑混在一个生命去写,尽管我们有时会把不同的逻辑封装到不同函数中进行组合调用,但是代码还是难以直接去表达业务逻辑,这让维护代码的人叫苦连连,那么我们使用hook,将可以使用像stateHook一样,可以调用多个hook:

function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });
  useEffect(() => {
    API.on(); // 比如当页面进入调用订阅API
    return () => {
      API.off(); // 销毁时再取消订阅API
    };
  });
  // ...
}

react会顺序执行多个effectHook

那么在官方文档中,提到了一个我也很苦恼的问题,在我进行第一次学习effectHook的时候,为什么组件更新还需要调用effect,直到文档做了一个很简单的查询好友信息的demo:

通过好友ID查询好友详情,好友详情组件需要接收一个prop,那么如果当prop进行更新,组件内的effect回调不会触发还是展示上一个好友的信息,而在清除副作用的时候,因为ID错误导致程序异常,那么显然effect设计如此是为了规避程序BUG,那么在class类中,我们应该使用如下方式来规避这样的更新问题:

componentDidUpdate(prevProps) {
    // 取消订阅之前的 friend.id
      API.off(prevProps.id); // 销毁时再取消订阅API
    // 订阅新的 friend.id
    API.on(prevProps.id);
 }

而使用hook则不需要关心更新组件带来的异常, 因为使用effect时,在调用一个新effect时就会清除上一个effect(触发清除回调effect),也就不会产生因为忘记写update逻辑造成的BUG

我们既然知道了effectHook更新设计的原因,那么当部分业务逻辑因为更新而造成的多余性能浪费,class和hook解决方案是怎么样的呢?

// class的写法,比较ID是否变更,变更再进行逻辑
componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

// effectHook提供了第二个可选参数,我们可以传入一个数组,比如
useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

当我们传入一个空数组时

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, []);

那么此effect将在挂载销毁执行,任何值的update都和此effect无关,所以这样的写法一定要注意应用场景。

总结Effect Hook:

  1. 自带的清除机制,可以避免在class中因为update而造成的问题
  2. 使用effect可以让业务逻辑分离解耦,不会像class中的生命周期分离逻辑变的很难

Hook规则概要

hook本质就是JS的函数,但是使用它也需要规则,虽然有专门的语法检查工具:

eslint-plugin-react-hooks

  1. hook需要写在函数中最高的层级,不要在循环,嵌套中去使用它们
  2. 只能在React中使用hook,不要在普通JS函数中去使用hook

关于插件的使用,文档也有非常清楚的配置方法

Hook 规则 - React

至于为什么hook只能在函数中最高层级去使用,官方也在文档中说明了:

demo1:

function Form() {
  // 1. Use the name state variable
  const [name, setName] = useState('Mary');

  // 2. Use an effect for persisting the form
  useEffect(function persistForm() {
    localStorage.setItem('formData', name);
  });

  // 3. Use the surname state variable
  const [surname, setSurname] = useState('Poppins');

  // 4. Use an effect for updating the title
  useEffect(function updateTitle() {
    document.title = name + ' ' + surname;
  });

  // ...
}

useState和useEffect如果都在最高层(函数作用域的最高层)中,那么React就可以把对应的Maryname 进行关联Poppinssurname进行关联,react通过声明的顺序去查找对应的变量,每次渲染的顺序和声明的顺序都是相同的,所以React才可以找到,那么如果我们写了如下的代码:

if(name !== null){
    useEffect(function persistForm() {
    localStorage.setItem('name ', name);
  });
}

这样触犯了hook的第一条规定,导致了程序有可能不会声明useEffect这个钩子,如果没有声明,那么声明的顺序和渲染的顺序就会不一致,就导致这个钩子之后的所有声明的钩子都将会被提前执行,导致BUG产生

自定义Hook的实现

我们自己写一个求差的小demo,参数传入减数和被减数简单体验一下自定义hook:

这个demo展示了hook和组件的传参,当组件的count改变时会触发到hook中的effect,effect会重新计算差并返回。

import { useState, useEffect } from "react";

export function useSeekingDifference(
  subtraction: number,
  minuend: number
): number {
  const [difference, setDifference] = useState(0);
  useEffect(() => {
    setDifference(subtraction - minuend);
  }, [subtraction, minuend]);
  return difference;
}

使用hook

import React, { useState, useEffect } from "react";
// 引入自定义hook
import { useSeekingDifference } from "./../hook/computed";

// 计算器的组件
function Main() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = "你点击了" + count + "次";
  });
 console.log(useSeekingDifference(count * 2, count)); // 使用自定义hook
}

export default Main;

我们写自定义hook的目标就是:

  1. 抽离业务逻辑

但是对于业务性非常强,以及对状态管理有频繁操作的建议使用redux(尽管我没用过,但是官方建议这样)

我们书写hook时需要注意:

  1. hook不是react特有,只是遵循了hook的规定的函数
  2. 必须使用use开头,因为不仅仅代表了它是一个hook,也可以让插件去做校验规则
  3. 相同hook不会共享state,每一个hook都是有独立的state和effect的,是重用状态逻辑机制