伍陆

伍陆 查看完整档案

上海编辑  |  填写毕业院校上海  |  前端工程师 编辑填写个人主网站
编辑

如果觉得我的文章对大家有用的话, 可以去我的github start一下https://github.com/Ray-56

个人动态

伍陆 赞了文章 · 3月1日

"undefined reference to" 问题汇总及解决方法

在实际编译代码的过程中,我们经常会遇到"undefined reference to"的问题,简单的可以轻易地解决,但有些却隐藏得很深,需要花费大量的时间去排查。工作中遇到了各色各样类似的问题,按照以下几种可能出现的状况去排查,可有利于理清头绪,从而迅速解决问题。

链接时缺失了相关目标文件

首先编写如下的测试代码:

// test.h

#ifndef __TEST_H__
#define __TEST_H__

void test();

#endif

// test.c

#include <string.h>
#include <stdio.h>



void test()
{
    printf("just test it\n");
}

// main.c

#include "test.h"

int main(int argc, char **argv)
{
    test();

    return 0;
}

通过以下的命令,我们将会得到两个.o文件。

$ gcc -c test.c  
$ gcc –c main.c 

随后,我们将main.o这个文件,编译成可执行文件。

$ gcc -o main main.o
Undefined symbols for architecture x86_64:
  "_test", referenced from:
      _main in main.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

编译时报错了,这是最典型的undefined reference错误,因为在链接时发现找不到某个函数的实现文件。如果按下面这种方式链接就正确了。

$ gcc -o main main.o test.o 

当然,也可以按照如下的命令编译,这样就可以一步到位。

$ gcc -o main main.c test.c

链接时缺少相关的库文件

我们把第一个示例中的test.c编译成静态库。

$ gcc -c test.c  
$ ar -rc test.a test.o 

接着编译可执行文件,使用如下命令:

$ gcc -o main main.c 
Undefined symbols for architecture x86_64:
  "_test", referenced from:
      _main in main-6ac26d.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

其根本原因也是找不到test()函数的实现文件,由于test()函数的实现在test.a这个静态库中,故在链接的时候需要在其后加入test.a这个库,链接命令修改为如下形式即可。

$ gcc -o main main.c test.a

链接的库文件中又使用了另一个库文件

先更改一下第一个示例中使用到的代码,在test()中调用其它的函数,更改的代码如下所示。

// func.h

#ifndef __FUNC_H__
#define __FUNC_H__

void func();

#endif

// func.c

#include <stdio.h>

void func()
{
    printf("call it\n");
}

// test.h

#ifndef __TEST_H__
#define __TEST_H__

void test();

#endif

// test.c

#include <string.h>
#include <stdio.h>

#include "func.h"



void test()
{
    printf("just test it\n");

    func();
}

// main.c

#include "test.h"

int main(int argc, char **argv)
{
    test();

    return 0;
}

我们先对fun.ctest.c进行编译,生成.o文件。

$ gcc -c func.c  
$ gcc -c test.c

然后,将test.cfunc.c各自打包成为静态库文件。

$ ar –rc func.a func.o  
$ ar –rc test.a test.o 

这时将main.c编译为可执行程序,由于main.c中包含了对test()的调用,因此,应该在链接时将test.a作为我们的库文件,链接命令如下。

$ gcc -o main main.c test.a
Undefined symbols for architecture x86_64:
  "_func", referenced from:
      _test in test.a(test.o)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

就是说,链接的时候发现test.a调用了func()函数,找不到对应的实现,我们还需要将test.a所引用到的库文件也加进来才能成功链接,因此命令如下。

$ gcc -o main main.c test.a func.a 

同样,如果我们的库或者程序中引用了第三方库(如pthread.a)则在链接的时候需要给出第三方库的路径和库文件,否则就会得到undefined reference的错误。

多个库文件链接顺序问题

这种问题非常隐蔽,不仔细研究,可能会感到非常地莫名其妙。以第三个示例为测试代码,把链接库的顺序换一下,如下所示:

$ gcc -o main main.c func.a test.a
test.a(test.o): In function `test':  
test.c:(.text+0x13): undefined reference to `func'  
collect2: ld returned 1 exit status

因此,在链接命令中给出所依赖的库时,需要注意库之间的依赖顺序,依赖其他库的库一定要放到被依赖库的前面,这样才能真正避免undefined reference的错误,完成编译链接。

备注:在MAC上可以正常编译通过。

定义与实现不一致

编写测试代码如下:

// test.h

#ifndef __TEST_H__
#define __TEST_H__

void test(unsigned int c);

#endif

// test.c

#include <string.h>
#include <stdio.h>



void test(int c)
{
    printf("just test it\n");
}

// main.c

#include "test.h"

int main(int argc, char **argv)
{
    test(5);

    return 0;
}

先将test.c编译成库文件。

$ gcc -c test.c 
$ ar -rc test.a test.o

main.c编译成可执行文件。

$ gcc -o main main.c test.a
ld: warning: ignoring file test.a, file was built for archive which is not the architecture being linked (x86_64): test.a
Undefined symbols for architecture x86_64:
  "_test", referenced from:
      _main in main-f27cf1.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

链接出错了,原因很简单,test()这个函数的声明和定义不一致导致,将两者更改成一样即可通过编译。

c++代码中链接c语言的库

代码同示例一的代码一样,只是把main.c更改成了main.cpp。编译test.c,并打包为静态库。

$ gcc -c test.c  
$ ar -rc test.a test.o

编译可执行文件,用如下命令:

$ g++ -o main main.cpp test.a 
Undefined symbols for architecture x86_64:
  "test()", referenced from:
      _main in main-7d7fde.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

原因就是main.cppc++代码,调用了c语言库的函数,因此链接的时候找不到,解决方法是在相关文件添加一个extern "C"的声明即可,例如修改test.h文件。

// test.h

#ifndef __TEST_H__
#define __TEST_H__

#ifdef __cplusplus
extern "C" {
#endif

void test();

#ifdef __cplusplus
}
#endif

#endif
查看原文

赞 3 收藏 4 评论 2

伍陆 赞了文章 · 1月26日

Yarn 2.0介绍

Yarn作为JavaScript生态的一个强大的依赖管理工具在今年1月24日的时候正式发布了v2版本。在本篇文章中,我将会为大家介绍以下内容:

备注:如果你想知道如何直接使用v2版本可以查看Getting Started,如果你想从v1版本迁移到v2版本可以查看Migrations

为什么要开发v2版本

原有代码架构满足不了新的需求

Yarn创建于2016年初,它在刚开始的时候借鉴了很多npm的东西,其中的架构设计本身就不是很符合Yarn开发者的愿景。在那之后,由于不断有新的需求产生,Yarn在接下来的几年中还添加了很多新的功能,其中包括Workspaces(2017), Plug'n'Play(2018)和Zip loading(2019)。这些新的概念在Yarn刚刚被创建的时候压根就不存在,所以Yarn的架构设计也就没有考虑到日后这些新功能的添加,因此随着时间的推移,Yarn的代码变得越来越难维护和扩展。由于这个技术原因,Yarn需要一个更加现代化的代码架构来满足新需求的开发。

鼓励开发者贡献代码

Yarn作为一个社区项目,秉承的一个理念就是: we don't want to work for you, we want to work with you。由此可以看出Yarn的开发者其实是希望更加多的开发者参与到这个项目的开发,而不是只有他们来维护。为了降低开发者为Yarn项目贡献代码的门槛,Yarn v2版本做了以下的一些改变:

  • 从Flow迁移到了现在更加流行的TypeScript作为开发语言,让开发者可以用更加熟悉的技术栈来贡献代码。
  • 采用基于插件(Plugin)的模块化(Modular)代码架构,让开发者不用搞懂Yarn的核心代码就可以通过实现插件的方式来为Yarn添加新的功能。而且Yarn的核心功能也是由不同的内置插件实现的,这点和Webpack的设计思想如出一辙,因此开发者可以很容易就搞懂每个功能是如何实现的。

v2都有什么新的特性

说完了为什么要开发v2版本之后,我们再来看一下它都有什么新的特性。

可读性更高的输出日志

虽然相对于其他替代方案(例如npm)Yarn的输出日志的可读性算是比较高的了,可是它还是存在各种各样的问题,例如当输出信息特别多的时候,开发者很难在一大堆输出中找到有用的内容,而且输出日志的颜色并没有起到帮助用户快速识别出重要信息的作用,甚至还会对日志的阅读造成一定的干扰。基于这些原因,v2版本对输出日志进行了一些改进,我们先来看一下它大概变成了什么样子了:

由上面的输出内容我们可以看到现在每一行日志的开头添加了一个错误号码(error code),不同的错误号码代表的意思可以在这个文档中找到。这些错误号码可以让开发者快速定位错误并且可以更加方便地搜索到修复错误的办法。除了新增错误号码,输出日志在颜色上也有很大的改进,例如上面输出中会用鲜艳的颜色来突出依赖的名称以及它的版本号,这样可以更加方便开发者获取有用的信息。

Yarn dlx

yarn dlx的功能和npx类似。dlx是download and execute的简称,这个命令会在本地创建一个临时的环境来下载指定的依赖,依赖下载完成后,它会在当前的工作目录(cwd)中执行这个依赖包含的可执行二进制文件,这个临时的空间会在命令完成后被删除,所以这些操作都是一次性的。

yarn dlx这个命令不会改变当前项目的package.json的内容,而且它只可以执行远端的脚本而不能执行本地的脚本(本地脚本可以用yarn run来执行),所以它相对于npx有更高的安全性。

由于v2版本默认开启了Plug'n'Play的功能,当你使用了一次yarn dlx命令执行某个远端脚本后,这个脚本的依赖会被缓存到本地环境中,这样当它被再次执行的时候它就不需要下载依赖了,所以它的速度会变得很快。

更好的workspaces支持

v2版本一个最大的改变就是将workspaces变成了一等公民(first-class citizen),这样就可以更好地支持monorepo的开发了。v2版本对workspaces的支持体现在以下这些方面:

yarn add 添加交互模式(interactive mode)

假如你要在项目的某个workspace中引入某个依赖,你可能要考虑其他workspaces是否也用到了这个依赖,而且要避免引入不兼容的版本。v2版本中,你可以使用-i参数来让yarn add命令进入到交互模式,这样yarn就会帮你检查这个依赖有没有在其他workspaces中被使用,并且会让你选择是要复用其他workspaces中的依赖版本还是使用另外的版本。

一次更新所有workspaces某个依赖的版本

v2版本新加了一个yarn up命令。这个命令和yarn upgrade命令类似,都是用来更新某个依赖的版本的。和yarn upgrade不同的是它可以同时更新所有workspaces的该依赖的版本,而不用切换到各个workspace中运行更新命令。这个命令同样具有交互模式-i来让你确认在不同workspace进行的具体操作。

自动发布关联的workspaces

有参与过monorepo开发的同学们一定会遇到过这样的问题:当某个包(workspace)发布了新的版本之后,发布其它相关联的包十分麻烦。如果你在项目中使用的是Lerna,当你发布一个包的新版本的时候,你要么所有的包都要发布新的版本,要么你得自己手动来管理其他包的版本发布。虽然自己来管理其它包的发布也是可以的,可是人为的东西肯定会存在疏忽,而且多人协作的项目会让人很头疼。为了解决这个问题,Yarn v2版本采取了和Lerna以及其他类似工具完全不同的解决方案,它把这部分逻辑放在了一个单独的叫做version的插件中。version插件允许你将一部分包版本管理工作分发给你的代码贡献者,而且它还提供了一个友好的交互界面来让你十分容易地管理关联包的发布:

在多个workspaces中运行相同的命令

在同一个项目的不同workspaces中运行同一个命令是很常见的情形,Yarn v2版本提供了一个新的yarn workspaces foreach命令来让你在多个workspaces中运行同一个命令,这个命令是由它内置的workspace-tools插件支持的,例如以下命令会在所有的workspaces中运行build命令:

yarn workspaces foreach run build

给所有workspaces添加约束(contraints)

有时候你希望同一个项目的所有workspaces都要遵循某些规则,例如所有的workspaces都不能使用underscore作为依赖又或者所有workspaces依赖的某个包版本要互相兼容等。v2版本有一个新的概念叫做约束(Constraints),这里的约束是对项目内各个workspaces的package.json进行的约束,就像ESLint对JS文件进行约束一样,它会在workspaces的package.json破坏了某些规则之后给你错误提示并且可以帮你修复其中一部分错误。

Yarn的约束规则是用Prolog语法来编写的。想要为你的workspaces添加约束,你首先得引入constraints插件:

yarn plugin import constraints

然后在项目的根目录定义一个存放约束规则的constraints.pro文件,最后在这个文件中定义你想要的约束条件,例如以下的约束条件会禁止所有的workspaces将underscore作为依赖:

gen_enforced_dependency(WorkspaceCwd, 'underscore', null, DependencyType) :-
  workspace_has_dependency(WorkspaceCwd, 'underscore', _, DependencyType).

约束规则定义完后可以使用yarn constraints check命令来校验项目的workspaces是否满足定义的约束规则,当有错误发生时,可以使用yarn constraints fix命令自动修复那些可以被自动修复的错误。

像搜索数据库一样查询workspaces的依赖信息

yarn constraints query命令可以查询项目中的workspaces用到的依赖信息,例如以下命令会输出各个workspace使用到的lodash版本信息:

$my-project: yarn constraints query "workspace_has_dependency(Cwd, 'lodash', Range, _)."
➤ YN0000: ┌ Cwd   = 'packages/backend'
➤ YN0000: └ Range = '4.17.0'
➤ YN0000: ┌ Cwd   = 'packages/frontend'
➤ YN0000: └ Range = '4.17.0'
➤ YN0000: Done with warnings in 0.03s

个人感觉上面的依赖查询很像在MySQL数据库里面用SELECT语法查询数据库,是一个十分强大而且有用的功能。

依赖零安装 (Zero-Installs)

依赖零安装更像是一个理念而不是一个功能,它的思路是希望我们每次在使用git更新完代码后,不需要再次使用yarn install命令来更新本地仓库的依赖来提高开发效率和避免一些问题的发生。它的具体做法是让开发者将本地的依赖包也提交到远端的git仓库中,看到这里你可能会想:“不就是将node_modules也提交吗?这个做法很蠢吧!”。确实如果直接将node_modules提交到远端仓库的话,每次提交都是一个噩梦,因为node_modules的文件很多(几万个文件很常见),首先你上传和下载代码的速度会变得很慢,其次很影响别人对你的代码进行review。为了解决这个问题,v2版本默认开启了Plug'n'Play + zip loading的功能,这个功能开启后你的项目将不再存在node_modules文件夹,所有的依赖都会被压缩成一个文件放在特定的地方,由于压缩后的包体积很小,而且包的数量不会很多,所以就不会存在以上说到的node_modules存在的问题。

可是为什么要做到依赖零安装呢?这是因为它有以下的好处:

  • 更好的开发体验

    • 你每次使用git pull, git checkout, git rebase这些命令更新完你的代码后无需使用yarn install进行依赖的安装,这样可以避免一些问题的出现,例如别人更新了某个依赖的版本后,如果你没有进行对应的更新的话,你的代码会挂。
    • 代码review的时候可以更清楚哪些依赖发生了改变。
  • 更快,更简单,更稳定的CI部署

    • 由于每次部署代码的时候,yarn install占用的时间都是一个大头,去掉这个步骤后部署速度将会大大提升。
    • 不会存在本地运行没问题,发布线上环境的时候挂掉了的问题。
    • 不用你在CI文件里面进行一些安装依赖的配置。

想要看一下pnp + zip loading实际效果的同学可以看一下yarn v2版本的[代码
](https://github.com/yarnpkg/be...,你可以看到它就是在自己仓库的.yarn/cache目录下存放了它所有的依赖:

新协议

Yarn v2版本添加了两个新的协议:patchportal协议。不知道什么是协议的同学可以看一下官网介绍,它大概是用来告诉yarn,定义在package.json文件里面的依赖是如何解析的。

Patch协议

我们日常开发中有时候会需要更改某个依赖的原代码来做一些试验性的东西,这个时候就可以使用这个patch协议了。我们先来看一下怎么使用:

{
  "dependencies": {
    "left-pad": "patch:left-pad@1.3.0#./my-patch.patch"
  }
}

上面的package.json中定义了left-pad这个依赖是如何解析的,我们可以看到left-pad的解析其实就用到了patch协议,它表示项目中用到的left-pad代码是1.3.0这个版本的代码叠加上./my-patch.patch这个补丁,所谓的补丁就是我们自己对left-pad这个库的代码的更改,和git的diff文件类似。

Portal协议

Portal协议和原有的link协议类似。它的作用是告诉yarn项目中的某个依赖指向本地文件系统的某个软链接(symlink),其实和yarn link的作用是差不多的。和link协议不同的是,portal指向的是一些包(package),也就是有package.json文件的那种文件夹,而且yarn会去解析这个包中的transitive dependencies。关于portal协议和link协议的更具体的区别可以看官方文档

范式化shell脚本(Normalized shell)

v2版本对Windows开发环境有了更好的兼容。你之前可能会遇到这样一个问题:你在package.json定义的script命令在OSX系统中可以运行,可是在windows电脑上却会报错。出现这个问题的原因是你在package.json中定义的script最终是通过Yarn创建一个子进程来执行的,而子进程的shell环境在Windows和OSX环境是不一样的(例如文件路径的写法就不一样)。为了解决这个问题,Yarn v2自带一个简单shell解析器(interpreter),这个解析器是用来兼容Windows和OSX shell环境的区别的,它覆盖了90%常用的shell脚本写法,所以正常来说你定义的shell脚本在Windows环境和OSX环境在这个解析器的兼容下都可以正常运行:

{
  "scripts": {
    "redirect": "node ./something.js > hello.md",
    "no-cross-env": "NODE_ENV=prod webpack"
  }
}

模块化代码架构

在前面已经提到Yarn v2版本已经转变为一个模块化的架构,并且它支持用户自定义Plugin去增强它的功能。用户自定义的插件可以获取到Yarn解析出的dependency tree信息以及一些其他的上下文信息,因此很容易就可以实现一些诸如LernaFemotoPatch-Package的库。

想要感受下Yarn的插件是怎么实现的同学可以看一下官方实现的typescript插件。这个typescript插件对于用Typescript开发的同学来说十分有用,它可以在你使用yarn add命令添加依赖的时候同时也添加这个依赖对应的@types/包,这样你就可以避免很多手动的工作了。更多和插件的相关的内容可以查看这个教程

其他更新

除了上面的提到的新的属性外,v2版本还有以下这些更新:

  • Peer dependencies也可以在yarn link里面使用了
  • Lockfile的格式变为了标准的YAML格式
  • 包只能依赖那些在package.json声明的依赖,不允许require那些没有声明的依赖
  • 范式化了配置文件
  • ...

想要查看v2版本所有更新内容的朋友可以看Maël Nison的文章 - Introducing Yarn 2或者直接查看它的change log

Yarn的未来计划

  • v1最后一个版本v1.22已经发布,作者从此不会再在v1的代码上添加任何新的功能了。Yarn所有的新功能都只会在v2版本的代码库上开发。
  • v1的代码仓库将会被从yarnpkg/yarn迁移到yarnpkg/legacy,这个仓库会继续开放一定的时间用来修复一些bug,然后会在一两年后achieve掉。v2版本的代码由于历史遗留问题不会迁移到yarnpkg/yarn,而且会在未来很长的一段时间保留在yarnpkg/berry
  • v1的官方网站会被搬到legacy.yarnpkg.com,yarnpkg.com官网的内容已经是v2版本next.yarnpkg.com的内容了。
  • npm仓库中,legacy标签指向的是最新的v1版本代码,latest标签会继续指向v1的最新版本的代码几周,然后指向v2的代码。berry标签将会一直指向v2版本的最新版本。
  • 大概在今年4月的时候,Node 14版本的Docker镜像可能会默认自带v2版本,这样你就可以直接在容器里使用v2的功能了。

参考文献

  • Yarn berry官方文档
  • [Yarn 2 - Reinventing package management - Maël Nison aka @arcanis at @ReactEurope 2019

](https://www.youtube.com/watch...

个人技术动态

文章首发于我的个人博客

欢迎关注公众号进击的大葱一起学习成长

查看原文

赞 1 收藏 0 评论 0

伍陆 赞了文章 · 1月7日

使用RxJS管理React应用状态的实践分享

随着前端应用的复杂度越来越高,如何管理应用的数据已经是一个不可回避的问题。当你面对的是业务场景复杂、需求变动频繁、各种应用数据互相关联依赖的大型前端应用时,你会如何去管理应用的状态数据呢?

我们认为应用的数据大体上可以分为四类:

  • 事件:瞬间产生的数据,数据被消费后立即销毁,不存储。
  • 异步:异步获取的数据;类似于事件,是瞬间数据,不存储。
  • 状态:随着时间空间变化的数据,始终会存储一个当前值/最新值。
  • 常量:固定不变的数据。

RxJS天生就适合编写异步和基于事件的程序,那么状态数据用什么去管理呢?还是用RxJS吗? 合不合适呢?

我们去调研和学习了前端社区已有的优秀的状态管理解决方案,也从一些大牛分享的关于用RxJS设计数据层的构想和实践中得到了启发:

  1. 使用RxJS完全可以实现诸如Redux,Mobx等管理状态数据的功能。
  2. 应用的数据不是只有状态的,还有事件、异步、常量等等。如果整个应用都由observable来表达,则可以借助RxJS基于序列且可响应的的特性,以流的方式自由地拼接和组合各种类型的数据,能够更优雅更高效地抽象出可复用可扩展的业务模型。

出于以上两点原因,最终决定基于RxJS来设计一套管理应用的状态的解决方案。

原理介绍

对于状态的定义,通常认为状态需要满足以下3个条件:

  1. 是一个具有多个值的集合。
  2. 能够通过event或者action对值进行转换,从而得到新的值。
  3. 有“当前值”的概念,对外一般只暴露当前值,即最新值。

那么,RxJS适合用来管理状态数据吗?答案是肯定的!

首先,因为Observable本身就是多个值的推送集合,所以第一个条件是满足的!

其次,我们可以实现一个使用dispatch action模式来推送数据的observable来满足第二个条件!

众所周知,RxJS中的observable可以分为两种类型:

  1. cold observable: 推送值的生产者(producer)来自observable内部。

    • 将会推送几个值以及推送什么样的值已在observable创建时被定义下来,不可改变。
    • producer与观察者(observer) 是一对一的关系,即是单播的。
    • 每当有observer订阅时,producer都会把预先定义好的若干个值依次推送给observer
  2. hot observable: 推送值的producer来自observable外部。

    • 将会推送几个值、推送什么样的值以及何时推送在创建时都是未知的。
    • producerobserver是一对多的关系,即是多播的。
    • 每当有observer订阅时,会将observer注册到观察者列表中,类似于其他库或语言中的addListener的工作方式。
    • 当外部的producer被触发或执行时,会将值同时推送给所有的observer;也就是说,所有的observer共享了hot observable推送的值。

RxJS提供的BehaviorSubject就是一种特殊的hot observable,它向外暴露了推送数据的接口next函数;并且有“当前值”的概念,它保存了发送给observer的最新值,当有新的观察者订阅时,会立即从BehaviorSubject那接收到“当前值”。

那么这说明使用BehaviorSubject来更新状态并保存状态的当前值是可行的,第三个条件也满足了。

简单实现

请看以下的代码:

import { BehaviorSubject } from 'rxjs';

// 数据推送的生产者
class StateMachine {
  constructor(subject, value) {
    this.subject = subject;
    this.value = value;
  }

  producer(action) {
    let oldValue = this.value;
    let newValue;
    switch (action.type) {
      case 'plus':
        newValue = ++oldValue;
        this.value = newValue;
        this.subject.next(newValue);
        break;
      case 'toDouble':
        newValue = oldValue * 2;
        this.value = newValue;
        this.subject.next(newValue);
        break;
    }
  }
}

const value = 1;  // 状态的初始值
const count$ = new BehaviorSubject(value);
const stateMachine = new StateMachine(count$, value);

// 派遣action
function dispatch(action) {
  stateMachine.producer(action);
}

count$.subscribe(val => {
  console.log(val);
});

setTimeout(() => {
  dispatch({
    type: "plus"
  });
}, 1000);

setTimeout(() => {
  dispatch({
    type: "toDouble"
  });
}, 2000);

执行代码控制台会打印出三个值:

Console

 1
 2
 4

上面的代码简单实现了一个简单管理状态的例子:

  • 状态的初始值: 1
  • 执行plus之后的状态值: 2
  • 执行toDouble之后的状态值: 4

实现方法挺简单的,就是使用BehaviorSubject来表达状态的当前值:

  • 第一步,通过调用dispatch函数使producer函数执行
  • 第二部,producer函数在内部调用了BehaviorSubjectnext函数,推送了新数据,BehaviorSubject的当前值更新了,也就是状态更新了。

不过写起来略微繁琐,我们对其进行了封装,优化后写法见下文。

使用操作符来创建状态数据

我们自定义了一个操作符state用来创建一个能够通过dispatch action模式推送新数据的BehaviorSubject,我们称她为stateObservable

const count$ = state({
  // 状态的唯一标识名称
  name: "count",
    
  // 状态的默认值
  defaultValue: 1,
    
  // 数据推送的生产者函数
  producer(next, value, action) {
    switch (action.type) {
      case "plus":
        next(value + 1);
        break;
      case "toDouble":
        next(value * 2);
        break;
    }
  }
});

更新状态

在你想要的任意位置使用函数dispatch派遣action即可更新状态!

dispatch("count", {
  type: "plus"
})

异步数据

RxJS的一大优势就在于能够统一同步和异步,使用observable处理数据你不需要关注同步还是异步。

下面的例子我们使用操作符frompromise转换为observable

指定observable作为状态的初始值(首次推送数据)

const todos$ = state({
  name: "todos",
    
  // `observable`推送的数据将作为状态的初始值
  initial: from(getAsyncData())
    
  //...
  
});

producer推送observable

const todos$ = state({
  name: "todos",
    
  defaultValue: []
    
  // 数据推送的生产者函数
  producer(next, value, action) {
    switch (action.type) {
      case "getAsyncData":
        next(
          from(getAsyncData())
        );
        break;
    }
  }
});

执行getAsyncData之后,from(getAsyncData())的推送数据将成为状态的最新值。

衍生状态

由于状态todos$是一个observable,所以可以很自然地使用RxJS操作符转换得到另一个新的observable。并且这个observable的推送来自todos$;也就是说只要todos$推送新数据,它也会推送;效果类似于Vue的计算属性。

// 未完成任务数量
const undoneCount$ = todos$.pipe(
  map(todos => {
    let _conut = 0;
    todos.forEach(item => {
      if (!item.check) ++_conut;
    });
    return _conut;
  })
);

React视图渲染

我们可能会在组件的生命周期内订阅observable得到数据渲染视图。

class Todos extends React.Component {
  componentWillMount() {
    todos$.subscribe(data => {
      this.setState({
        todos: data
      });
    });
  }
}

我们可以再优化下,利用高阶组件封装一个装饰器函数@subscription,顾名思义,就是为React组件订阅observable以响应推送数据的变化;它会将observable推送的数据转换为React组件的props

@subscription({
  todos: todos$
})
class TodoList extends React.Component {
  render() {
    return (
      <div className="todolist">
        <h1 className="header">任务列表</h1>
        {this.props.todos.map((item, n) => {
          return <TodoItem item={item} key={item.desc} />;
        })}
      </div>
    );
  }
}

总结

使用RxJS越久,越令人受益匪浅。

  • 因为它基于observable序列提供了较高层次的抽象,并且是观察者模式,可以尽可能地减少各组件各模块之间的耦合度,大大减轻了定位BUG和重构的负担。
  • 因为是基于observable序列来编写代码的,所以遇到复杂的业务场景,总能按照一定的顺序使用observable描述出来,代码的可读性很强。并且当需求变动时,我可能只需要调整下observable的顺序,或者加个操作符就行了。再也不必因为一个复杂的业务流程改动了,需要去改好几个地方的代码(而且还容易改出BUG,笑~)。

所以,以上基于RxJS的状态管理方案,对我们来说是一个必需品,因为我们项目中大量使用了RxJS,如果状态数据也是observable,对我们抽象可复用可扩展的业务模型是一个非常大的助力。当然了,如果你的项目中没有使用RxJS,也许ReduxMobx是更合适的选择。

这套基于RxJS的状态管理方案,我们已经用于开发公司的商用项目,反馈还不错。所以我们决定把这套方案整理成一个js lib,取名为:Floway,并在github上开源:

欢迎大家star,更欢迎大家来共同交流和分享RxJS的使用心得!




参考文章:

查看原文

赞 13 收藏 6 评论 2

伍陆 回答了问题 · 2020-11-27

树形结构数据如何删除空的子级节点?

function deleteEmpty(list) {
  for (let i = list.length - 1; i >= 0; i--) {
    const item = list[i];
    if (!item.hasOwnProperty('children')) continue;
    if (item.children.length === 0) {
      list.splice(i, 1);
      continue;
    }
    deleteEmpty(item.children);
  }
}

关注 6 回答 4

伍陆 赞了文章 · 2020-11-26

计数排序,桶排序与基数排序

一般算法能做到O(logn),已经非常不错,如果我们排序的对象是纯数字,还可以做到惊人的O(n)。涉及的算法有计数排序、基数排序、桶排序,它们被归类为非比较排序。

非比较排序只要确定每个元素之前的已有的元素个数即可,遍历一次就能求解。算法时间复杂度O(n)。

非比较排序时间复杂度低,但由于非比较排序需要占用空间来确定唯一位置。所以对数据规模和数据分布有一定的要求。

计数排序

计数排序需要占用大量空间,它仅适用于数据比较集中的情况。比如 [0~100],[10000~19999] 这样的数据。

我们看一下计数排序是怎么运作,假设我们有[1,2,3,1,0,4]这六个数,这里面最大的值为4,那么我们创建一个长度为4的数组,每个元素默认为0。这相当于选举排序,一共有6个投票箱,1就投1号箱,0就投入0号箱。注意,这些箱本来就是已经排好序,并且箱的编号就是代表原数组的元素。当全部投完时,0号箱有1个,1号箱有2个,2号箱有1个,3号箱有1,4号箱有1个。然后我们从这些箱的所有数依次出来,放到新数组,就神奇地排好序了。

计数排序没有对元素进行比较,只是利用了箱与元素的一一对应关系,根据箱已经排好序的先决条件,解决排序。

//by 司徒正美
function countSort(arr){
   var max = Math.max.apply(0, arr);
   var buckets = []
   for(var i = 0; i < n; i++){
      var el = arr[i]
      if(buckets[el]){//子桶里不实际存在
         buckets[el]++ 
      }else{
         buckets[el] = 1
      }
   }
   var index = 0
   for(var i = 0; i < n; i++){
       var m = buckets[i].length;
       while(m){
          arr[index] = i;
          index++
          m--
       }
   }
   return arr
}

但数组有一个问题就是它的索引值是从0开始,但我们的元素也要大于或等于0。我们可以通过一个数学技巧让它支持负数。

//by 司徒正美
function countSort(arr){
   var max = arr[0]
   var min = arr[0]
   for(var i = 0; i < n; i++){
      if(arr[i] > max){
         max = arr[i]
      }
      if(arr[i] < min){
         max = arr[i]
      }
   }
 
   var buckets = new Array(max-min+1).fill(0);
   for(var i = 0; i < n; i++){
      buckets[ arr[i]-min ]++     //减去最小值,确保索引大于负数
   }
   var index = 0, bucketCount = max-min+1
   for(var i = 0; i < bucketCount; i++){
       var m = buckets[i].length;
       while(m){
        //将桶的编号加上最小值,变回原来的元素
        arr[index] = i+min;
        index++
        m--
       }
   }
   return arr
}

桶排序

桶排序与计数排序很相似,不过现在的桶不单计数,是实实在在地放入元素。举个例子,学校要对所有老师按年龄进行排序,这么多老师很难操作,那么先让他们按年龄段进行分组,20-30岁的一组,30-40岁一组,50-60岁一组,然后组内再排序。这样效率就大大提高了。桶排序也是于这种思想。

操作步骤:

  1. 确认范围,亦即求取原数组的最大值与最小值。
  2. 确认需要多少个桶(这个通常作为参数传入,不能大于原数组长度),然后最大值减最小值,除以桶的数量,但得每个桶最多能放多个元素,我们称这个数为桶的最大容量。
  3. 遍历原数组的所有元素,除以这个最大容量,就能得到它要放入的桶的编号了。在放入时可以使用插入排序,也可以在合并时才使用快速排序。
  4. 对所有桶进行遍历,如果桶内的元素已经排好序,直接一个个取出来,放到结果数组就行了。
//by 司徒正美
var arr = [2,5,3,0,2,8,0,3,4,3]
   function bucketSort(array, num){
    if(array.length <= 1){
      return array
    }
    var n = array.length;
    var min = Math.min.apply(0, array)
    var max = Math.max.apply(0, array)
    if(max === min){
       return array
    }
    var capacity = (max - min + 1) /num;
    var buckets = new Array(max - min + 1)
    for(var i = 0; i < n; i++){
      var el = array[i];//el可能是负数
      var index = Math.floor((el - min) / capacity)
      var bucket = buckets[index]
      if(bucket){
         var jn = bucket.length;
         if(el >= bucket[jn-1]){
            bucket[jn] = el
         }else{
            insertSort: 
            for(var j = 0; j < jn; j++){
                if(bucket[j] > el){
                    while(jn > j){ //全部向后挪一位
                        bucket[jn] = bucket[jn-1]
                        jn--
                    }
                    bucket[j] = el //让el占据bucket[j]的位置
                    break insertSort;
                }
            }
         }
      }else{
         buckets[index] = [el]
      }
    }
    var index = 0
    for(var i = 0; i < num; i++){
        var bucket = buckets[i]
        for(var k = 0, kn = bucket.length; k < kn; k++){
            array[index++] = bucket[k]
        }
    }
    return array;
 }
 console.log(  bucketSort(arr,4) )
 //[ 0, 0, 2, 2, 3, 3, 3, 4, 5, 8 ]

基数排序

基数排序是一种非比较型的整数排序算法。其基本原理是,按照整数的每个位数分组。在分组过程中,对于不足位的数据用0补位。

基数排序按照对位数分组的顺序的不同,可以分为LSD(Least significant digit)基数排序和MSD(Most significant digit)基数排序。

LSD基数排序,是按照从低位到高位的顺序进行分组排序。MSD基数排序,是按照从高位到低位的顺序进行分组排序。上述两种方式不仅仅是对位数分组顺序不同,其实现原理也是不同的。

LSD基数排序

对于序列中的每个整数的每一位都可以看成是一个桶,而该位上的数字就可以认为是这个桶的键值。比如下面数组

[170, 45, 75, 90, 802, 2, 24, 66]

首先我们要确认最大值,一个for循环得最大数,因为最大数的位数最长。

然后,建立10个桶,亦即10个数组。

然后再遍历所有元素,取其个位数,个位数是什么就放进对应编号的数组,1放进1号桶。

 0号桶: 170,90
 1号桶: 无
 2号桶: 802,2
 3号桶: 无
 4号桶: 24
 5号桶: 45, 75
 6号桶: 66
 7-9号桶: 无

然后再依次将元素从桶里最出来,覆盖原数组,或放到一个新数组,我们把这个经过第一次排序的数组叫sorted。

sorted = [170,90,802,2,24,45,75,66]

然后我们再一次遍历sorted数组的元素,这次取十位的值。这时要注意,2不存在十位,那么默认为0

 0号桶: 2,802
 1号桶: 无
 2号桶: 24
 3号桶: 无
 4号桶: 45
 5号桶: 无
 6号桶: 66
 7号桶: 170, 75
 8号桶: 无
 9号桶: 90

再全部取出来

sorted = [2,802,24,45,66,170,75,90]

开始百位上的入桶操作,没有百位就默认为0:

 0号桶: 2,24,45,66,75,90
 1号桶: 170
 2-7号桶:无
 8号桶: 802
 9号桶: 无

再全部取出来

sorted = [2,24,45,66,75,90,170,802]

没有千位数,那么循环结束,返回结果桶sorted

从程序描述如下:

image_1c44h9tg4j23p1l18e1ojue8i20.png-122.3kB

//by 司徒正美
function radixSort(array) {
    var max = Math.max.apply(0, array);
    var times = getLoopTimes(max),
        len = array.length;
    var buckets = [];
    for (let i = 0; i < 10; i++) {
        buckets[i] = []; //初始化10个桶
    }
    for (var radix = 1; radix <= times; radix++) {
        //个位,十位,百位,千位这样循环
        lsdRadixSort(array, buckets, len, radix);
    }
    return array;
}
// 根据数字某个位数上的值得到桶的编号
function getBucketNumer(num, d) {
    return (num + "").reverse()[d];
}
//或者这个
function getBucketNumer(num, i) {
    return Math.floor((num / Math.pow(10, i)) % 10);
}
//获取数字的位数
function getLoopTimes(num) {
    var digits = 0;
    do {
        if (num > 1) {
            digits++;
        } else {
            break;
        }
    } while ((num = num / 10));
    return digits;
}
function lsdRadixSort(array, buckets, len, radix) {
    //入桶
    for (let i = 0; i < len; i++) {
        let el = array[i];
        let index = getBucketNumer(el, radix);
        buckets[index].push(el);
    }
    var k = 0;
    //重写原桶
    for (let i = 0; i < 10; i++) {
        let bucket = buckets[i];
        for (let j = 0; j < bucket.length; j++) {
            array[k++] = bucket[j];
        }
        bucket.length = 0;
    }
}
// test
var arr = [278, 109, 63, 930, 589, 184, 505, 269, 8, 83];
console.log(radixSort(arr));

MSD基数排序

接下来讲MSD基数排序.

最开始时也是遍历所有元素,取最大值,得到最大位数,建立10个桶。这时从百位取起。不足三位,对应位置为0.

 0号桶: 45, 75, 90, 2, 24, 66
 1号桶: 107
 2-7号桶: 无
 8号桶: 802
 9号桶: 无

接下来就与LSD不一样。我们对每个长度大于1的桶进行内部排序。内部排序也是用基数排序。我们需要建立另10个桶,对0号桶的元素进行入桶操作,这时比原来少一位,亦即十位。

 0号桶: 2
 1号桶: 无
 2号桶: 24
 3号桶: 无
 4号桶: 45
 5号桶: 无
 6号桶: 66
 7号桶: 75
 8号桶: 无
 9号桶: 90

然后继续递归上一步,因此每个桶的长度,都没有超过1,于是开始0号桶的收集工作:

 0号桶: 2,24,45,66,75,90
 1号桶: 107
 2-7号桶: 无
 8号桶: 802
 9号桶: 无

将这步骤应用其他桶,最后就排序完毕。

//by 司徒正美
function radixSort(array) {
    var max = Math.max.apply(0, array),
        times = getLoopTimes(max),
        len = array.length;
    msdRadixSort(array, len, times);
    return array;
}

//或者这个
function getBucketNumer(num, i) {
    return Math.floor((num / Math.pow(10, i)) % 10);
}
//获取数字的位数
function getLoopTimes(num) {
    var digits = 0;
    do {
        if (num > 1) {
            digits++;
        } else {
            break;
        }
    } while ((num = num / 10));
    return digits;
}
function msdRadixSort(array, len, radix) {
    var buckets = [[], [], [], [], [], [], [], [], [], []];
    //入桶
    for (let i = 0; i < len; i++) {
        let el = array[i];
        let index = getBucketNumer(el, radix);
        buckets[index].push(el);
    }
    //递归子桶
    for (let i = 0; i < 10; i++) {
        let el = buckets[i];
        if (el.length > 1 && radix - 1) {
            msdRadixSort(el, el.length, radix - 1);
        }
    }
    var k = 0;
    //重写原桶
    for (let i = 0; i < 10; i++) {
        let bucket = buckets[i];
        for (let j = 0; j < bucket.length; j++) {
            array[k++] = bucket[j];
        }
        bucket.length = 0;
    }
}
var arr = radixSort([170, 45, 75, 90, 802, 2, 24, 66]);
console.log(arr);

字符串使用基数排序实现字典排序

此外,基数排序不局限于数字,可以稍作变换,就能应用于字符串的字典排序中。我们先来一个简单的例子,只对都是小写字母的字符串数组进行排序。

小写字母一共26个,考虑到长度不一样的情况,我们需要对够短的字符串进行补充,这时补上什么好呢?我们不能直接上0,而是补空白。然后根据字母与数字的对应关系,弄27个桶,空字符串对应0,a对应1,b对应2.... 字典排序是从左边开始比较, 因此我们需要用到MST基数排序。

//by 司徒正美
var character = {};
"abcdefghijklmnopqrstuvwxyz".split("").forEach(function(el, i) {
    character[el] = i + 1;
});
function toNum(c, length) {
    var arr = [];
    arr.c = c;
    for (var i = 0; i < length; i++) {
        arr[i] = character[c[i]] || 0;
    }
    return arr;
}
function getBucketNumer(arr, i) {
    return arr[i];
}

function radixSort(array) {
    var len = array.length;
    var loopTimes = 0;

    //求出最长的字符串,并得它的长度,那也是最高位
    for (let i = 0; i < len; i++) {
        let el = array[i];
        var charLen = el.length;
        if (charLen > loopTimes) {
            loopTimes = charLen;
        }
    }

    //将字符串转换为数字数组
    var nums = [];
    for (let i = 0; i < len; i++) {
        nums.push(toNum(array[i], loopTimes));
    }
    //开始多关键字排序
    msdRadixSort(nums, len, 0, loopTimes);
    //变回字符串
    for (let i = 0; i < len; i++) {
        array[i] = nums[i].c;
    }
    return array;
}

function msdRadixSort(array, len, radix, radixs) {
    var buckets = [];
    for (var i = 0; i <= 26; i++) {
        buckets[i] = [];
    }
    //入桶
    for (let i = 0; i < len; i++) {
        let el = array[i];
        let index = getBucketNumer(el, radix);
        buckets[index].push(el);
    }
    //递归子桶
    for (let i = 0; i <= 26; i++) {
        let el = buckets[i];
        //el.c是用来识别是桶还是我们临时创建的数字字符串
        if (el.length > 1 && !el.c && radix < radixs) {
            msdRadixSort(el, el.length, radix + 1, radixs);
        }
    }
    var k = 0;
    //重写原桶
    for (let i = 0; i <= 26; i++) {
        let bucket = buckets[i];
        for (let j = 0; j < bucket.length; j++) {
            array[k++] = bucket[j];
        }
        bucket.length = 0;
    }
}
var array = ["ac", "ee", "ef", "b", "z", "f", "ep", "gaaa", "azh", "az", "r"];

var a = radixSort(array);
console.log(a);

参考链接

查看原文

赞 9 收藏 23 评论 9

伍陆 发布了文章 · 2020-11-23

Markdown常用语法

Markdown常用语法

[TOC]

1、斜体和粗体

*斜体*或_斜体_
**粗体**
***加粗斜体***
~~删除线~~

斜体或_斜体_
粗体
加粗斜体
删除线

2、分级标题{#2}

# 一级标题
## 二级标题
### 三级标题

使用了[TOC]就会把所有的标题写入到目录大纲中,当前目录就是如此生成

3、超链接

3-1、行内式

这是[baidu](https://www.baidu.com/)  
这是[Google](https://www.google.com/)  

这是baidu
这是Google

3-2、参考式

一般用在学术论文上面,或者某个链接有多处使用

面向[Google][1]编程
或者面向[百度][2]编程

面向[Google][]的话有用的信息更多一点

[1]:https://www.google.com/
[2]:https://www.baidu.com/
[Google]:https://www.google.com/

面向Google编程
或者面向百度编程

面向[Google][]的话有用的信息更多一点

3-3、自动链接

<http://example.com>
<address@example.com>

http://example.com
<address@example.com>

4、锚点

锚点也就是链接文档内部的某些元素,实现当前页面中的跳转

## 0、跳转测试{#index}

跳转到[跳转测试](#index)

5、列表

5-1、无序列表

使用* + -表示无序列表

- 无序列表项1
- 无序列表项2
  • 无序列表项1
  • 无序列表项2

5-2、有序列表

1. 有序列表项1
2. 有序列表项2
  1. 有序列表项1
  2. 有序列表项2

5-3、定义型列表

由名词和解释组成。一行写上定义,紧跟一行写上解释。解释的写法:紧跟一个缩进(Tab)

代码块1 Markdown
:   轻量级文本标记语言,可以转成 HTML,PDF 等格式(左侧有一个可见的冒号和四个不可见的空格)

代码块2
:   这是代码块的定义(左侧有一个可见的冒号和四个不可见的空格)

        代码块(左侧有八个不可见的空格)

代码块1 Markdown
: 轻量级文本标记语言,可以转成 HTML,PDF 等格式(左侧有一个可见的冒号和四个不可见的空格)

代码块2
: 这是代码块的定义(左侧有一个可见的冒号和四个不可见的空格)

    代码块(左侧有八个不可见的空格)

5-3-1、列表缩进

列表项目标记通常放在最左边,但其实也可以缩进,最多三个空格,项目标记后面则一定要接着至少一个空格或制表符

*    轻轻的我走了,正如我轻轻的来;我轻轻的挥手,作别西天的云彩。那河畔的金柳,是夕阳中的新娘;波光里的艳影,在我心头荡漾。软泥上的靑荇,油油的在水底招摇;在康河的柔波里,我甘心做一条水草!
*         那榆萌下的一潭,不是清泉,是天上虹;揉碎在浮藻间,沉淀着彩虹似的梦。寻梦?撑一只长篙,向青草更深处漫溯;满载一船星辉,在星辉斑斓里放歌。但我不能放歌,悄悄是别离的笙箫;夏虫也为我沉默,沉默是今晚的康桥!悄悄的我走了,正如我悄悄的来;我挥一挥衣袖,不带走一片云彩。
  • 轻轻的我走了,正如我轻轻的来;我轻轻的挥手,作别西天的云彩。那河畔的金柳,是夕阳中的新娘;波光里的艳影,在我心头荡漾。软泥上的靑荇,油油的在水底招摇;在康河的柔波里,我甘心做一条水草!
  • 那榆萌下的一潭,不是清泉,是天上虹;揉碎在浮藻间,沉淀着彩虹似的梦。寻梦?撑一只长篙,向青草更深处漫溯;满载一船星辉,在星辉斑斓里放歌。但我不能放歌,悄悄是别离的笙箫;夏虫也为我沉默,沉默是今晚的康桥!悄悄的我走了,正如我悄悄的来;我挥一挥衣袖,不带走一片云彩。

5-4、包含段落的列表

列表项目可以包含多个段落,每个项目下的段落都必须缩进 4 个空格或是 1 个制表符(显示效果与代码一致

*   轻轻的我走了,正如我轻轻的来;我轻轻的招手,作别西天的云彩。
那河畔的金柳,是夕阳中的新娘;波光里的艳影,在我心头荡漾。
软泥上的靑荇,油油的在水底招摇;在康河的柔波里,我甘心做一条水草!

    那榆荫下的一潭,不是清泉,是天上虹;揉碎在浮藻间,沉淀着彩虹似的梦。
寻梦?撑一支长篙,向青草更青处漫溯;满载一船星辉,在星辉斑斓里放歌。
但我不能放歌,悄悄是别离的笙箫;夏虫也为我沉默,沉默是今晚的康桥!

*   悄悄的我走了,正如我悄悄的来;我挥一挥衣袖,不带走一片云彩。
  • 轻轻的我走了,正如我轻轻的来;我轻轻的招手,作别西天的云彩。

那河畔的金柳,是夕阳中的新娘;波光里的艳影,在我心头荡漾。
软泥上的靑荇,油油的在水底招摇;在康河的柔波里,我甘心做一条水草!

那榆荫下的一潭,不是清泉,是天上虹;揉碎在浮藻间,沉淀着彩虹似的梦。

寻梦?撑一支长篙,向青草更青处漫溯;满载一船星辉,在星辉斑斓里放歌。
但我不能放歌,悄悄是别离的笙箫;夏虫也为我沉默,沉默是今晚的康桥!

  • 悄悄的我走了,正如我悄悄的来;我挥一挥衣袖,不带走一片云彩。

5-5、包含引用的列表

* 阅读的方法:
  > 打开书本
  > 打开电灯
  • 阅读的方法:

    打开书本
    打开电灯

5-6、包含代码块的引用

如果要放代码区块的话,该区域就要缩进两次,也就是 8 个空格或是 2 个制表符

* 下面是代码块
        const URL = "google.com";
  • 下面是代码块
    const URL = "google.com";

5-7、一个特殊的情况

在特殊情况下,列表项目很可能会不小心产生,向下面这样的写法:

1892. 这是第 1892 个

可能会显示成

1. 这是第 1892 个

也就是首行出现数字-句点-空白,要避免这样的情况,可以在句点前面加上反斜杠:

1892\. 这是第 1892 个

才会正常显示成:

1892. 这是第 1892 个

6、引用

> 这是一个有两段文字的引用
无意义的占行文字1
无意义的占行文字2

> 无意义的占行文字3
无意义的占行文字4
这是一个有两段文字的引用
无意义的占行文字1
无意义的占行文字2

无意义的占行文字3
无意义的占行文字4

6-1、引用的多层嵌套

>>> 请问 Mardown 怎么用 - 小白

>> 自己看教程! - 愤青

> 教程在哪里? - 小白
请问 Mardown 怎么用 - 小白

自己看教程! - 愤青

教程在哪里? - 小白

6-2、引用其它要素

引用的区块内也可以使用其它的 Markdown 语法,包含标题、列表、代码块等

> 1. 这是第一行列表项
> 2. 这是第二行列表项
>
> 给出一些例子代码:
>
>     const url = 'baidu.com';
  1. 这是第一行列表项
  2. 这是第二行列表项

给出一些例子代码:

const url = 'baidu.com';

7、内容目录

在段落中填写[TOC]以显示全文内荣的目录结构

效果看当前文档最上方的目录

8、注脚

使用 Markdown[^1] 可以效率的书写文档,直接转换成 HTML[^2] 或 PDF,你可以使用 Typora[^Ty] 编辑器进行书写

[^1]: Markdown 是一种纯文本标记语言

[^2]: HyperTextMarkupLanguage 超文本标记语言

[^Ty]: 开源的 Markdown 编辑器

使用 Markdown1 可以效率的书写文档,直接转换成 HTML2 或 PDF,你可以使用 Typora3 编辑器进行书写

注:注脚自动被搬运到最后面,请到文章末尾查看,并且注脚后方的链接可以直接跳转回到加注的地方。

9、LaTeX 公式

使用较少 访问 MathJax 参考更多使用方法。

9-1、 $ 表示行内公式:

质能守恒方程式可以用一个很简洁的方程式 $E=mc^2$ 来表示。

质能守恒方程式可以用一个很简洁的方程式 $E=mc^2$ 来表示。

10、表格

第一行为表头,第二行为分隔表头和主体部分,第三行开始每一行是一个表格行。

列与列之间用管道符|隔开。原生方式的表格每一行的两边也要有管道符。第二行还可以为不同的列指定对齐方式。默认为左对齐,:在哪边就是哪边对齐。

1. 简单方式写表格:
学号|姓名|分数
-|-|-
小明|男|66
小红|女|88
小鹿|男|99
2. 原生方式写表格:
|学号|姓名|分数|
|-|-|-|
|小明|男|66|
|小红|女|88|
|小鹿|男|99|
3. 第二列指定方向(右)
姓名|备注
-|-:
小明|短备注
小红|长长长长长长长长长长备注
  1. 简单方式写表格:
学号姓名分数
小明66
小红88
小鹿99
  1. 原生方式写表格:
学号姓名分数
小明66
小红88
小鹿99
  1. 第二列指定方向(右)
姓名备注
小明短备注
小红长长长长长长长长长长备注

11、分割线

* * *
***
******
- - -
------

显示效果都一样

12、代码

  1. 行内
  2. 多行

    • \``\`
    • 缩进

12-1、使用 Diff

  • const TYPE = 1;
  • const TyPE = 2;

显示效果:

- const TYPE = 1;
+ const TyPE = 2;

See Also


  1. Markdown 是一种纯文本标记语言
  2. HyperTextMarkupLanguage 超文本标记语言
  3. 开源的 Markdown 编辑器
查看原文

赞 0 收藏 0 评论 0

伍陆 回答了问题 · 2020-10-19

解决VUE如何同时监听上下两个布局的点击事情?

语法写错了吧

@click="addGoodsHandler"

关注 3 回答 2

伍陆 回答了问题 · 2020-10-14

chrome浏览器F12查看到的网站js文件是加密压缩过的,可以还原吗?

这个看起来是webpack打包的,打包的时候打开 sourceMap 就可以看到了

关注 3 回答 3

伍陆 回答了问题 · 2020-09-29

解决后端提供的接口都是分过页的数据,我导出数据导不了全部。

每页数据999999

关注 5 回答 5

伍陆 回答了问题 · 2020-09-29

请教一个locaStorage的自增方法

广告页进入前

let count = locaStorage.getItem('count');

if (!count) {
    locaStorage.setItem('count', 1);
} else if (count >= 5) {
    进入其他
    return
} else {
    locaStorage.setItem('count', count + 1);
}
进入广告页

关注 4 回答 3

认证与成就

  • 获得 151 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-12-16
个人主页被 2.8k 人浏览