0%

React Fiber现在应该是一个应知必会的内容了。

什么是Fiber呢?

官方的话说“React Fiber是对核心算法的一次重新实现。”
首先我们要理解重新实现,就得知道之前有什么问题,我们知道之前React的更新是同步的。React在加载或者更新组件的时候,会进行一系列的操作如调用生命周期,计算和对比Virtual DOM,最后更新DOM这整个过程都是同步的。在这个过程中主线程只负责更新操作,无法做任何事情,这就会导致页面的卡顿。

React Fiber的特点

通过分片的方式来破解同步时间过长。他的特点:

  1. 增量渲染(把渲染任务拆分成块,匀到多帧)
  2. 更新时能够暂停,终止,复用渲染任务
  3. 给不同类型的更新赋予优先级。
  4. 并发方面新的基础能力。

Fiber Tree

维护每一个分片需要一个数据结构。就是Fiber树,长下面这个样子。
scxggJ.png
可以看出来这就是一个链表树。

在render的过程中,我们创建root fiber并设为nextUnitOfWork放在performUnitOfWork中进行,在里面我们要做三件事。

  1. 将元素加到DOM上
  2. 创建该元素的子fiber元素。
  3. 选择下一个工作单元

这个fiber树的目标之一就是更方便的找到下一个工作单元。我们可以看到每一个fiber都廉洁着它的子元素,兄弟元素和父元素。

我们来看一下它的顺序。

当我们完成一个fiber的渲染工作。如果有子孩子,子孩子将会成为下一个工作单元。如果没有子孩子,它的兄弟孩子将会成为下一个工作单元。如果既没有子也没有兄弟,它的叔叔节点将会成为下一个工作单元(也就是父节点的兄弟节点)。直到回到root节点,整个render过程结束。

我们来用代码实现一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function createDom(fiber) {
const dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)

const isProperty = key => key !== "children"

Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})

return dom;
}

function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}

let nextUnitOfWork = null;

在render函数中我们设置root为nextUnitOfWork,之后浏览器准备好了之后,将会调用我们的workLoop

我们来实现performUnitOfWork函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}

if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}

const elements = fiber.props.children
let index = 0;
let prevSibling = null;

while (index < elements.length) {
const element = elements[index];

const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}

if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}

prevSibling = newFiber
index++
}

if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}

遍历每一个child创建新的fiber。然后再为他添加孩子或者兄弟节点,是否为兄弟取决于是否是第一个孩子。最后我们搜索下一个工作单元,先尝试是否有孩子节点,然后再试兄弟,最后是叔叔这样的顺序。
这就是整个performUnitOfWork的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { useRef, useEffect } from 'react';

const usePrevious = value => {

const ref = useRef();

useEffect(() => {
ref.current = value;
}, [value]);

return ref.current;

}

为什么用useRef能拿到上一次的值。

  1. useRef保持引用不变;
  2. 函数式组件的生命周期决定,JSX的渲染比useEffect早。
  3. 手动修改ref.current并不会触发组件的重新渲染。

Context的概念比较容易理解,它的目的就是做组件间数据共享。让嵌套很深的组件不需要一层一层的把props传下来。
我们先看一下如何使用Context

创建Context

我们用React.createContext来创建Context对象。可以给他传默认值。我们就用React官网的例子

1
const MyContext = React.createContext(defaultValue);

当React渲染订阅了这个Context对象的组件,这个组件会从组件树中离自身最近的那个匹配的Provider中读取到当前的context值。

如果他没有匹配到Provider也就是没有被Provider包裹的情况下。将使用defaultValue。

这有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

Context.Provider

1
<MyContext.Provider value={/* 某个值 */}>

每个Context对象都会返回一个Provider React组件,它允许Consumer订阅context的变化。
Provider接收一个value属性,传递给Consumer组件。一个Provider可以和多个Consumer组件有对应关系。多个Provider也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

通过新旧值检测来确定变化,使用了与 Object.is 相同的算法。

Class.contextType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass extends React.Component {
componentDidMount() {
let value = this.context;
/* 在组件挂载完成后,使用 MyContext 组件的值来执行一些有副作用的操作 */
}
componentDidUpdate() {
let value = this.context;
/* ... */
}
componentWillUnmount() {
let value = this.context;
/* ... */
}
render() {
let value = this.context;
/* 基于 MyContext 组件的值进行渲染 */
}

// 或者 static contextType = MyContext;
}
MyClass.contextType = MyContext;

挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
当然使用这种方法,你只能挂在一个context。

Context.Consumer

用来订阅context的变更,它让函数式组件可以订阅context。
这种方法,子元素必须是一个函数,这个函数接收context的值,并返回一个React节点。传递给函数的value值等价于组件树上方离这个context最近的Provider提供的value值。如果没有对应的Provider,value参数等同于传递给createContext()的defaultValue。

Context.displayName

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

注意

官网中的例子,这个一定要注意:
因为 context 会使用参考标识(reference identity)来决定何时进行渲染,这里可能会有一些陷阱,当 provider 的父组件进行重渲染时,可能会在 consumers 组件中触发意外的渲染。举个例子,当每一次 Provider 重渲染时,以下的代码会重渲染所有下面的 consumers 组件,因为 value 属性总是被赋值为新的对象:

1
2
3
4
5
6
7
8
9
class App extends React.Component {
render() {
return (
<MyContext.Provider value={{something: 'something'}}>
<Toolbar />
</MyContext.Provider>
);
}
}

为了防止这种情况,将 value 状态提升到父节点的 state 里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}

render() {
return (
<Provider value={this.state.value}>
<Toolbar />
</Provider>
);
}
}

对比新老Context

在我最初用Context的时候,官方还是不推荐使用的。说是一般程序没有必要用Context而且Context还在实验阶段将在之后的版本里删除。在新的Context出来之后,现在可以频繁的见到Context的使用。而且在hook出现之后,useContext和useReducer配合还可以代替Redux。

老Context存在的问题。

在16.3之前Context这样用。

1
2
3
4
5
6
7
8
9
class Parent extends React.Component {
getChildContext() {
return {color: "purple"};
}
}

Parent.childContextTypes = {
color: PropTypes.string
};

然后就可以在任意一级子组件上访问 Context 里的内容了:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Child extends React.Component {
render() {
return (
<p>
{this.context.color}
</p>
);
}
}

Child.contextTypes = {
color: PropTypes.string
};

用起来也不太复杂。但是和shouldComponentUpdate搭配起来就有问题。

1
2
3
4
5
<a>
<b>
<c>
</b>
</a>

其中组件a会通过 getChildContext 设置 Context,组件c通过 this.context 读取 Context。
当a设置新的Context,会触发子组件rerender,然后b会rerender。最终导致c的rerender,并且在 c render的时候会取到新的context。
这一切都很正常,如果这是b中使用了shouldComponentUpdate的时候,由于b中的state和props没有变,不会导致b的rerender,导致c也不会rerender这时,c就取不到新的Context的值。

新的Context

  1. Provider 和 Consumer 必须来自同一次 React.createContext 调用。也就是说 A.Provider 和 B.Consumer 是无法搭配使用的。
  2. React.createContext 方法接收一个默认值作为参数。当 Consumer 外层没有对应的 Provider 时就会使用该默认值。
  3. Provider 组件的 value prop 值发生变更时,其内部组件树中对应的 Consumer 组件会接收到新值并重新执行 children 函数。此过程不受 shouldComponentUpdete 方法的影响。
  4. Provider 组件利用 Object.is 检测 value prop 的值是否有更新。注意 Object.is 和 === 的行为不完全相同。
  5. Consumer 组件接收一个函数作为 children prop 并利用该函数的返回值生成组件树的模式被称为 Render Props 模式。

由于最近要写一个系列文章,为了保证尽可能的严谨。我将其中设计的到的知识点再重新复习一遍。过一遍官方文档,查缺补漏,也能带大家入门。

SCSS是一个CSS预处理器,它可以让我们使用一些CSS没有的功能,比如变量(现在CSS已经支持变量)嵌套,mixins,继承等。让样式变得好维护。

当我们安装了SCSS之后,可以在控制台直接运行

1
sass input.scss out.css

这样就把scss文件直接编译为css文件,就可以在浏览器中直接使用。
你还可以对scss文件的变化进行监听。加上--watch

1
sass --watch input.scss output.css

这样每当你保存的时候,它都会重新build。
你也可以监听整个目录,input和output路径通过冒号分割

1
sass --watch app/sass:public/stylesheets

Sass会监听app/sass目录下所有scss文件的变化。

变量

$来声明变量,变量可以来保存你想要重用的。比如颜色、字体样式、宽高之类的。

1
2
3
4
5
6
7
$font-stack:    Helvetica, sans-serif;
$primary-color: #333;

body {
font: 100% $font-stack;
color: $primary-color;
}

嵌套

在写HTML的时候,我们有很好的层次结构。现在Scss也可以让我们坐到。但主要不要嵌套太多层,直接上例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}

li { display: inline-block; }

a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
}

Partials

这个很好理解,这个就是放一些公共样式,只要在你的scss文件名前面加上_,这个scss文件中的内容就不会被build到css文件中。这个写在写组件的时候会很有用。

模块化 Modules

你不可可能将所有的样式都写在一个文件内。只要用@use不包含扩展名就能将别的文件模块引入。

1
2
3
4
5
6
7
8
// _base.scss
$font-stack: Helvetica, sans-serif;
$primary-color: #333;

body {
font: 100% $font-stack;
color: $primary-color;
}

1
2
3
4
5
6
7
// styles.scss
@use 'base';

.inverse {
background-color: base.$primary-color;
color: white;
}

生成的CSS文件。

1
2
3
4
5
6
7
8
9
body {
font: 100% Helvetica, sans-serif;
color: #333;
}

.inverse {
background-color: #333;
color: white;
}

Mixins

在写css的时候经常会写一些重复性的代码。Mixins的出现就能很好的解决这个问题。我们用@mixin来定义一个mixin然后用@include来使用它。直接看例子

1
2
3
4
5
6
@mixin transform($property) {
-webkit-transform: $property;
-ms-transform: $property;
transform: $property;
}
.box { @include transform(rotate(30deg)); }

build的css

1
2
3
4
5
.box {
-webkit-transform: rotate(30deg);
-ms-transform: rotate(30deg);
transform: rotate(30deg);
}

扩展和继承

这个非常有用,它可以让不同的算选择器公用一部分样式,还能保持自己的样式干净。这块看例子最清晰明了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
%message-shared {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}

// This CSS won't print because %equal-heights is never extended.
%equal-heights {
display: flex;
flex-wrap: wrap;
}

.message {
@extend %message-shared;
}

.success {
@extend %message-shared;
border-color: green;
}

.error {
@extend %message-shared;
border-color: red;
}

.warning {
@extend %message-shared;
border-color: yellow;
}

我们看它生成的样式

.message, .success, .error, .warning {
  border: 1px solid #ccc;
  padding: 10px;
  color: #333;
}

.success {
  border-color: green;
}

.error {
  border-color: red;
}

.warning {
  border-color: yellow;
}

感觉非常的棒。

运算符

scss支持常见的运算符。如加、减、乘、除、取余之类的。常运用宽高的计算。

判断奇偶

我们看整数转为二进制数的时候,最后一位,如果是1必为奇数,如果是0则为偶数。所以我们用该数与1做与运算。结果为0则为偶,结果为1则为奇。

1
2
偶数 & 1 = 0
奇数 & 1 = 1

交换两个数

这是用到了异或的性质。a^b^a = 0^b = b

1
2
3
4
5
6
let a = 1;
let b = 2;

a = a^b;
b = a^b; // a^b^b = a;
a = a^b; // a^a^b = b;

取整

由于js在做位运算的时候,是吧浮点数转为整数来计算。 我们可以用这个原理通过左移0位来进行整数转换。

1
0.1 << 0  // 0

同理用>>0也行但是>>>0就不行。
还有根据a|0=a的原理|0也可以进行取整。

权限验证

这块就是最近工作中用到的。
位运算在权限系统中的使用。的保证每个权限码都是唯一的并且权限码的二进制数形式有且只有一位为1剩余的位为0(2^n)。

接下来我们就可以用

  1. | 来赋予权限
  2. & 来验证权限
1
2
3
4
5
6
7
8
9
10
11
12
let r = 0b100
let w = 0b010
let x = 0b001

// 给用户赋 r w 两个权限
let user = r | w
// user = 6
// user = 0b110 (二进制)

console.log((user & r) === r) // true 有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限

如果想删除一个权限的话,最简单的方式就是用异或^。但是异或是一个有则减,无则增的一个操作。

那么如果单纯的想删除权限(而不是无则增,有则减)怎么办呢?答案是执行 &(~code),先取反,再执行与操作

但是这种方法也有局限性。因为js操作最多只有32位。有一篇文章很好地解决了这个问题局限性和解决办法

做字符串的加密

这部分参考这篇文章深入研究js中的位运算及用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const key = 313;
function encryption(str) {
let s = '';
str.split('').map(item => {
s += handle(item);
})
return s;
}

function decryption(str) {
let s = '';
str.split('').map(item => {
s += handle(item);
})
return s;
}

function handle(str) {
if (/\d/.test(str)) {
return str ^ key;
} else {
let code = str.charCodeAt();
let newCode = code ^ key;
return String.fromCharCode(newCode);
}
}

let init = 'hello world 位运算';
let result = encryption(init); // őŜŕŕŖęŎŖŋŕŝę乴軩窮
let decodeResult = decryption(result); // hello world 位运算

用~~来取整

我们先看效果

1
2
3
4
5
6
7
8
9
10
11
12
13
~~1.1 // 1

首先在js中位运算要转为整数,所以先对1取反, 简单地用八位
1的源码 0000 0001
然后取反 ~ 0000 0001 = 1111 1110
发现符号位为负数。这里会将负数变为补码的形式,为了统一加法和减法,减法会变成加一个负数,而负数会以补码的形式存储。
1111 1110 的 补码为 反码 加 1
1000 0010
转为十进制,就成了-2,然后再对-2取反
这时就将负数转为补码的形式
1111 1110
然后在取反。就为
0000 0001 转为十进制 就为 1

我们其实可以看到,都是用补码来进行取反。(正数的补码是它本身,负数的补码为反码+1)

判断边界

做边界判断,总共有3种超出情况:右、上、左,并且可能会叠加,如鼠标在左上角的时候会导致左边和上面同时超出。需要记录超出的情况进行调整,用001表示右边超出,010表示上方超出,100表示左边超出,如下代码计算:

let postFlag = 0;
//右边超出
if(pos.right < maxLen) posFlag |= 1;
//上面超出
if(pos.top < maxLen) posFlag |= 2;
//左边超出
if(pos.left < maxLeftLen) posFlag |= 4;
//对超出的情况进行处理,代码略
switch(posFlag){
      case 1: //右
      case 2: //上
      case 3: //右上
      case 4: //左
      case 6: //左上
}

保留高位/低位

如想要保留后面四位 只需要用数跟 &0000 1111 这样前四位都变为了0。保留低位同理。

在开始增加其它的代码,我们需要重构。

我们看之前的render函数。我们会发现,在递归调用的时候会有问题。当开始render的时候我们就只能等到element tree全部render完成。如果element tree太大的话。它就会中断主线程很久。如果浏览器需要做一些高权限的事情,比如处理用户输入,动画处理这些就必须的等到render结束。

所以我们要将这个任务分成小的单元。我们在完成每个单元之后,如果要有别的事情需要做。浏览器就将会中断渲染。
我们来看看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let nextUnitOfWork = null;

function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

function performUnitOfWork(nextUnitOfWork) {

}

我们用requestIdleCallback来做循环。它就是在浏览器主线程空闲的时候才调用这个方法。
React已经不再用requestIdleCallback了。现在用scheduler package但是不影响这个例子。

requestIdleCallback的回调会传入deadline参数。我们可以用这个来检测还剩多长时间。浏览器将会夺回控制权。
截止2019年11月,并发模型在React还不稳定,稳定的版本的循环像这样:

1
2
3
4
5
while (nextUnitOfWork) {    
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
}

在开始循环,我们需要设置一个工作单元。我们需要编写一个performUnitOfWork函数。它不仅运行当前work。还会将下次要运行的work返回。

我们现在来实现一下。ReactDOM.render方法。

我们只做把将元素添加到DOM节点上。

1
2
3
4
5
function render(element, container) {
const dom = document.createElement(element.type)

container.appendChild(dom);
}

我们还需要递归的遍历,将子元素也添加上。

1
2
3
4
5
6
7
8
9
function render(element, container) {
const dom = document.createElement(element.type)

element.props.children.forEach(child => {
return render(child, dom);
})

container.appendChild(dom);
}

还需要处理纯文本元素,如果element的type是TEXT_ELEMENT我们将创建一个text node。

1
2
3
4
5
6
7
8
9
10
11
function render(element, container) {
const dom = element.type === "TEXT_ELEMENT" ?
document.createTextNode("")
: document.createElement(element.type)

element.props.children.forEach(child => {
return render(child, dom);
})

container.appendChild(dom);
}

最后我们将元素的props添加到元素上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function render(element, container) {
const dom = element.type === "TEXT_ELEMENT" ?
document.createTextNode("")
: document.createElement(element.type)

const isProperty = key => key !== "children";
Object.keys(element.props).filter(isProperty).forEach(name => {
dom[name] = element.props[name]
})

element.props.children.forEach(child => {
return render(child, dom);
})

container.appendChild(dom);
}

至此我们就完成了全部,JSX渲染到DOM上的整个过程。

我们来实现一下createElement这个方法。
根据之前的文章,我们也介绍了。通过调用React.createElement这个方法,最终返回的是一个包含type和props属性的对象。
我们需要接受一些参数,第一个就是要创建element的类型。第二个为这个这个element属性对象,之后就是这个element的子元素。

1
2
3
4
5
6
7
8
9
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children
}
}
}

我们使用解构参数的形式,来获取children参数。这样能保证children永远是一个数组。

1
2
3
4
5
6
7
我们调用
createElement("div", null, a, b)
返回
{
"type": "div",
"props": { "children": [a, b] }
}

有可能在children数组中包含基本数据类型,而并非一个对象,如string或者number。我们需要将这些转换成一个对象。我们改写一下上面的代码。

function createElement(type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => {
                return typeof child === 'object'
                ? child
                :  createTextElement(child)
            })
        }
    }
}

function createTextElement(text) {
    return {
        type: 'TEXT_ELEMENT',
        props: {
            nodeValue: text,
            children: [],
        },
    }
}

至此我们就实现了createElement方法。

之前涉及到正则的时候全靠搜。现在记录一下,加深一下印象。
正则的语法就是两条斜线//中间是正则的主体。比如

1
/aaaa/ 匹配 aaaa

转义字符

转义字符主要作用就是

  1. 匹配不方便显示的字符如\n
  2. 在正则中预设了一些特殊意义的字符。
  3. 让特殊意义的字符显示本身。

举一些常见的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
\n  换行
\r 回车
\t 制表符也就是tab
这些属于第一种,但感觉也不常用。平时开发没怎么用到

\w 匹配任何一个字母或者数字或者下划线
\W 除了\w以外的东西
\s 匹配空白字符,如空格,tab等。
\S 除了\s以外的东西
\d 0-9
\D 非数字字符
\b 匹配单词的边界
\b 匹配非单词的边界
这些就是一些常见的预设特殊意义的字符

如果你就想匹配\的话。就需要这样写
\\

字符集合

用一个中括号。表示里面的字符匹配都是或的关系。

1
2
3
[abc] a || b || c
[0-9] 可以用-表示一个范围。 表示0-9中间任意一个
[^abc] ^表示非 就是匹配abc之外的任意字符

量词

正则中量词有多个。

  1. ?匹配0次或者1次(也就是最多出现一次)
  2. + 匹配1-n次(至少出现一次)
  3. * 匹配0-n次,优先匹配n次。
  4. {n} 匹配n次。
  5. {m, n} 匹配m-n次。
  6. {m, } 匹配m到无穷次。优先匹配无穷次。

正则默认是贪婪模式,就是凡事表示范围的,优先匹配上限而不是下限。就是说它会尽可能的往多里匹配。

字符边界

  1. ^ 表示匹配开头
  2. $ 表示匹配结尾

选择表达式

123|456|789 //匹配123或456或789

分组与引用

分组的功能就是匹配一组字符,如
(abc) 就是匹配abc这个整体

捕获分组和非捕获分组

这块,知乎上有一个答案说的非常清楚。在此引用一下。

最近正在学习正则,也发现了捕获性分组和非捕获性分组,也在寻找答案。终于通过努力,研究懂了他们的区别到底在哪。我就不说术语了,直接用例子会表述的更清楚: 要在一篇文章中查找”program”和”project”两个单词,正则表达式可表示为/program|project/,也可表示为/pro(gram|ject)/,但是缓存子匹配(gramject)没有意义,就可以用/pro(?:gram|ject)/进行非捕获性匹配这样既可以简洁匹配又可不缓存无实际意义的字匹配
作者:冰冻三寸
链接:https://www.zhihu.com/question/19853431/answer/160306020
来源:知乎

预搜索

如果你想匹配xxx前不能是yyy,或者xxx后不能是yyy,那就要用到预搜索

js只支持先行预搜索,也就是xxx前面必须是yyy,或者xxx前面不能是yyy

1
2
(?=1)2 // 可以匹配12,不能匹配22
(?!1)2 // 可有匹配22,不能匹配12

修饰符

  1. g 全局修饰符,一直匹配到结束
  2. i 忽略大小写
  3. m 匹配多行文本

最近在工作中很邪门,两次遇到了跟位运算相关的内容。所以想把这块整理成文章。
在写这篇文章的时候,默认已经掌握了二进制转换,原码、反码、补码等等这类基础知识。这部分内容也会出现在。组成原理系列文章中(已经很久没有更新的系列,哎)。

根据IEEE754标准,JavaScript在存储数字时是始终以双精度浮点数来存储的。也就是64位二进制数,64bit,而1Byte = 8bit,相当于8Byte。 其中0到51位存储数字,52到62存储指数,63存储符号。
而在JS位运算中,并不用64位来计算,它会把值转为32位数值再进行位运算。位运算结束后,再将32位转为64位储存。所以我们在位运算时只需要关注32位整数就可以

按位与&

两位同为1则为1,否则为0
例:

1
2
3
4
  0011    
& 0101
------
0001

按位或|

只要其中一位是1,结果就为1
例:

1
2
3
4
  0011
| 0101
------
0111

按位异或^

相同的位为0,不同的位为1。我们可以理解为就是做无进位加法:

1
2
3
4
  0011
^ 0101
------
0110

异或有几条性质:

  1. 交换律 a^b = b^a
  2. 结合律(a^b)^c = a^(c^b)
  3. 对于任何数,都有x^x=0, x^0=x
  4. 自反性:a^b^b=a^0=a;

按位取反~

0变1,1变0

1
~0011 = 1100

按位左移 <<

各二进位全部左移若干位,高位丢弃,低位补 0

按位右移

有符号右移>>

将数值的二进制码按照指定的位数向右移动,符号位不变,高位补符号位.

无符号右移>>>

将数值的所有 32 位字符都右移,然后将高位补0。