伍陆

伍陆 查看完整档案

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

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

个人动态

伍陆 赞了文章 · 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的使用心得!




参考文章:

查看原文

赞 11 收藏 5 评论 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

伍陆 关注了用户 · 2020-09-27

Jokcy @jokcy

前端码农一枚

关注 62

伍陆 赞了文章 · 2020-09-27

React源码阅读之:复合类型方案设计

关于React中一些代码设计

最近在看React的源码,注意到了一些有意思的细节,比如经常会出现的一下比较和赋值代码

workInProgress.effectTag |= Ref
(workInProgress.effectTag & DidCapture) !== NoEffect

对于平时基本上没怎么用到过移位运算的我一开始表示这是啥?为啥要这么设计?

我们先来看一下,这个effectTag具体会有那些值

// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /*              */ 0b00000000000;
export const PerformedWork = /*         */ 0b00000000001;

// You can change the rest (and add more).
export const Placement = /*             */ 0b00000000010;
export const Update = /*                */ 0b00000000100;
export const PlacementAndUpdate = /*    */ 0b00000000110;
export const Deletion = /*              */ 0b00000001000;
export const ContentReset = /*          */ 0b00000010000;
export const Callback = /*              */ 0b00000100000;
export const DidCapture = /*            */ 0b00001000000;
export const Ref = /*                   */ 0b00010000000;
export const Snapshot = /*              */ 0b00100000000;

// Union of all host effects
export const HostEffectMask = /*        */ 0b00111111111;

export const Incomplete = /*            */ 0b01000000000;
export const ShouldCapture = /*         */ 0b10000000000;

这么一看貌似好像有点意思,可以看到大部分的值都只有一位是1,其他位都是00bxxx是原生二进制字面量的表示方法

那么回过头去我们再看上面两句表达式

workInProgress.effectTag |= Ref
// 也就是
workInProgress.effectTag = workInProgress.effectTag | RefRef

我们随便拿两个值来举例,比如PlacementUpdate,也就是0b00000000010 | 0b00000000100那么得到的结果是什么呢?0b00000000110,也就等于PlacementAndUpdate。所以这时候大家知道为什么大部分的值1所在的位置不一样了吧,因为其实每一位的1代表一种属性,把他们结合在一起就代表有多种属性,不会有重复。

同样的对于第二个表达式

(workInProgress.effectTag & DidCapture) !== NoEffect

我们拿UpdateDidCapture来进行&操作,那么得到的结果就很明显了,所有位都是0,所以后期的&操作是用来判断在某个变量中是否含有某个属性的。比如这里就是判断workInProgress.effectTag中是否含有DidCapture这个属性。

这种设计方式我觉得挺有参考意义的,可以用在类似权限系统上。大概现在很多权限系统已经这么做了吧,只是我不知道。。。

React源码正在阅读中,有望一两个月把所有成果放出来,有兴趣的可以关注我

查看原文

赞 9 收藏 5 评论 1

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

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