My Little World

learn and share


  • 首页

  • 分类

  • 标签

  • 归档

  • 关于
My Little World

hook 产生原因

发表于 2021-11-10

数据与视图间关系

在过去,我们需要处理当 Model 变化时,DOM 节点应该如何变化的细节问题。
而现在,我们只需要通过 JSX,根据 Model 的数据用声明的方式去描述 UI 的最终展现就可以了,
因为 React 会帮助你处理所有 DOM 变化的细节。而且,当 Model 中的状态发生变化时,UI 会自动变化,即所谓的数据绑定。

所以呢,我们可以把 UI 的展现看成一个函数的执行过程。
其中,Model 是输入参数,函数的执行结果是 DOM 树,也就是 View。
而 React 要保证的,就是每当 Model 发生变化时,函数会重新执行,并且生成新的 DOM 树,然后 React 再把新的 DOM 树以最优的方式更新到浏览器。

class组件不合适的原因

一方面,React 组件之间是不会互相继承的。比如说,你不会创建一个 Button 组件,然后再创建一个 DropdownButton 来继承 Button。
所以说,React 中其实是没有利用到 Class 的继承特性的。

另一方面,因为所有 UI 都是由状态驱动的,因此很少会在外部去调用一个类实例(即组件)的方法。
要知道,组件的所有方法都是在内部调用,或者作为生命周期方法被自动调用的。

所以你看,这两个 Class 最重要的特性其实都没有用到。

函数组件的缺陷/hooks产生的背景

只是当时有一个局限是,函数组件无法存在内部状态,必须是纯函数,而且也无法提供完整的生命周期机制。这就极大限制了函数组件的大规模使用。
那么我们自然就知道了,Class 作为 React 组件的载体,也许并不是最适合,
反而函数是更适合去描述 State => View 这样的一个映射,但是函数组件又没有 State ,也没有生命周期方法。
以此来看,我们应该如何去改进呢?

hooks的产生

简单想一下,函数和对象不同,并没有一个实例的对象能够在多次执行之间保存状态,那势必需要一个函数之外的空间来保存这个状态,而且要能够检测其变化,从而能够触发函数组件的重新渲染。
再进一步想,那我们是不是就是需要这样一个机制,能够把一个外部的数据绑定到函数的执行。当数据变化时,函数能够自动重新执行。
这样的话,任何会影响 UI 展现的外部数据,都可以通过这个机制绑定到 React 的函数组件。
在 React 中,这个机制就是 Hooks。
所以我们现在也能够理解这个机制为什么叫 Hooks 了。
顾名思义,Hook 就是“钩子”的意思。

在 React 中,Hooks 就是把某个目标结果钩到某个可能会变化的数据源或者事件源上,那么当被钩到的数据或事件发生变化时,产生这个目标结果的代码会重新执行,产生更新后的结果。

对于函数组件,这个结果是最终的 DOM 树;对于 useCallback、useMemo 这样与缓存相关的组件,则是在依赖项发生变化时去更新缓存。

hooks 好处一 逻辑复用

Hooks 中被钩的对象,不仅可以是某个独立的数据源,也可以是另一个 Hook 执行的结果,这就带来了 Hooks 的最大好处:逻辑的复用

class高阶组件缺陷

更为糟糕的是,高阶组件几乎是 Class 组件中实现代码逻辑复用的唯一方式,其缺点其实比较显然:
代码难理解,不直观,很多人甚至宁愿重复代码,也不愿用高阶组件;
会增加很多额外的组件节点。每一个高阶组件都会多一层节点,这就会给调试带来很大的负担。

使用hooks封装好处

通过 Hooks 的方式进行封装,从而将依赖变成一个可绑定的数据源。
这样当窗口大小发生变化时,使用这个 Hook 的组件就都会重新渲染。而且代码也更加简洁和直观,不会产生额外的组件节点。

hooks好处二 有助于关注分离

在 Class 组件中,代码是从技术角度组织在一起的,例如在 componentDidMount 中都去做一些初始化的事情。
而在函数组件中,代码是从业务角度组织在一起的,相关代码能够出现在集中的地方,从而更容易理解和维护。

My Little World

《js 语言精髓与编程实践》

发表于 2021-10-18

整本书看下来,稍微有些晦涩,很多理论概念被换了一种说法阐释,开阔思路,故整理笔记。

笔记按照篇章整理,没有固定逻辑。

第二章

1.语法关键字对语义逻辑的绑定结果,是对作用域的限定;
变量对位置性质的绑定结果,则是对变量生命周期的限定。

2.函数的6种声明标识符的方法(var,const,let,fucntion,class,import),他们声明的标识符在语法分析阶段就可以被识别。

3.关于值传递与引用传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var str = 'abcded';
var obj = new String(str);

function newToString() {
return 'hello world'
}

function func(val){
val.toString = newToString;
}

func(str);
console.log(str); // abcded

func(obj);
console.log(String(obj)) // hello world

func函数传入字符串时,是值传递,根据结果可知,不能直接修改其属性方法
传入obj对象时,是引用传递,传入后被修改了属性值,故打印出了hello world.

所以,到底是值传递还是引用,会根据传递的数据类型决定。

4.等值检测(==)运算规则

值类型与引用类型比较,将引用转换成值类型数据相同的数据,进行数据等值比较

两个值类型比较,将二者转换成相同数据类型,在进行数据等值比较

两个引用类型比较,仅比较引用地址

跟数字,布尔,字符串比较时
有任何一个是数字时,将另外一个转数字
有任何一个是布尔值时,转换成数字后比较
有任何一个是对象(或函数),调用valueOf方法来将其转换成值数据进行比较
按特定规则返回比较结果,如,undefined和null会返回true

等值检测一些特例

1
2
3
4
5
6
7
8
9
10
1. NaN不等于自身

NaN == (===/ != /!==) NaN // true

2. 符号可以转为true,但不等值于true

Boolean(Symbol()) ,!Symbol(), Symbol() ==(===) true // true false false

3. 即使字面量相同的引用类型,也是不严格相等的
{} === {}, /./ === /./, function() {} === function() {} // false false false

5.序列检测规则

两个值类型比较
直接比较数据在序列中的大小

值类型与引用类型进行比较
将引用类型转换成与值类型数据相同的的数据,再进行“序列大小”比较

两个引用类型进行比较
无意义,总是返回false

6.赋值
字符串赋值原理是复制地址,所以对于字符串的操作有了以下三种限制
a.不能直接修改字符串中的字符
b.字符串连接运算必然导致写复制,这将产生新的字符串
c.不能改变字符串的长度

7.函数隐式调用的几种情况
a.es6之后的模板处理函数
b.将函数用作属性读取器时,属性存取操作将隐世调用该函数,==>getter,setter
c.使用bind方法将原函数绑定为目标函数时,调用目标函数,就是隐世调用原函数
d.当使用Proxy() 创建原函数的代理对象时,调用代理对象也是隐式调用原函数
e.new运算符运算
f.当函数用作对象的符号属性,触发相应的行为时,就会隐式调用原函数

8.模块导入

1
2
3
4
5
import * as mynames from 'module'; // mynames.x是对象属性读取

import {x} from 'module'; // x是来自原模块的引用,得到的是引用值

let { x : x2 } = mynames // x2 是本地声明的变量可修改
  1. new 调用普通函数
    如果该普通函数返回对象,则返回该对象
    如果该函数返回值类型数据,则返回值忽略,返回this 引用,也就是普通函数本身

  2. arr[1,2,3] 会返回arr[3],中括号里面的1,2,3会形成逗号运算

  3. delete
    不能删除
    用var/let/const声明的变量与常量
    直接继承自原型的成员
    本质上用于删除对象自有属性表属性

  4. 关于对象
    所谓原型,就是构造器用于生成实例的模板

空白对象: 它的原型链上的所有自有属性表都为空
原型链:对象所有父类和祖先类的原型所形成的,可上溯访问的链表

函数和构造器并没有明显的界限,唯一区别只在于原型prototype是不是一个有意义的值

类是静态的声明,意味着类继承的构建过程也是静态的,是在语法分析期就决定了的

当在函数f中使用super.xxx时,无论该函数被用来作为那个实际对象的方法,
super都绑定在Object.getPrototypeOf(obj)上,f为obj的属性方法

在new 类的实例时,super()执行的目的就是回溯整个原型链,确保基类最先创建实例
没有在类中声明constructor()方法时,会默认添加并调用super()

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
function foo() {
var showInArrow = () => {
console.log(this.name);
}
showInArrow();
var obj = {
showInArrow,
name: 'aObject',
showThis: function() {
console.log(this.name)
}
}
with(obj) {
var showThis2 = () => { console.log(this.name)}
showThis();
showThis2();
showInArrow();
}
}
foo.call({name: 'outSide'})
==>
outSide
aObject
outSide
outSide

继承方式的选择上,

大型系统必须采用类继承的思路,继承关系的确定性和支持静态语法检测等特性
可以帮助开发者最终简化构建大型系统的开发和业务逻辑实现,并提供足够的系统稳定性

小型系统或者体系的局部使用原型继承的思路,既可以有优美的实现和高校的性质,
也可以更深入理解js中混合不同语言特性的精髓

1
2
3
4
5
6
function MyFunction() {}

MyFunction.prototype = new Function();
var myFunc = new MyFunction();

myFunc(); //触发异常无法执行

上面例子符合对象继承语义,但不能继承它的‘可执行’效果
内置对象的特殊效果不被对象系统继承
一方面这些效果被引擎绑定在特殊的构造器上,而不是他们的原型上
另一方面,系统只负责维护内部原型链,以确保instanceof运算能正确检测这种关系
而不负责这些特定效果的实现和传递

如果一个属性使用的是存取描述符,那么无论读写性质是什么,都不会新建属性描述符
子类中如果是继承来的这样的属性,那么在子类中对该属性的读写也会忠实地调用继承来的读写器

第四章

  1. 信息是对状态集合的解释,该集合的解释成本即是编程所应付的复杂性

编程的目的是使一个系统对外呈现可解释信息

  1. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var x = 'outer', y = 'outer';
    function foo() {
    console.log(1, [x,y]) // [undefined, undefined]
    if(true) {
    function x() {}
    } else {
    function y() {}
    }
    console.log(2, [x,y]) // [f, undefined]
    }
    foo()

第五章

  1. 关于函数参数
    默认参数都是有名字的形式参数,但是从第一个参数开始,后续所有参数不会载计入形参个数
    剩余参数不计入形参个数
    模板参数参与计数,但是无论一个模板参数被解构为多少标识符,都按一个计算
    1
    2
    3
    4
    5
    6
    7
    8
    const ff = (a,b=1,c=2) => {console.log(34)}
    ff.length ==>1

    const ff2 = (a=3,b,c) => {console.log(34)}
    ff2.length ==>0

    const ff3 = (a, [b,c]) => {console.log(34)}
    ff3.length ==>2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function foo(filename) {
var [filename2, ...args] = arguments;

// filename 会影响arguments
filename = 'new file name';
console.log(arguments[0]); // new file name
console.log(filename2); // test.txt

// arguments 也会影响filename
arguments[0] = filename2;
console.log(filename);// test.txt

// 使用filenam2时,没有影响
filename2 = 'update again';
console.log(arguments[0]); // test.txt
console.log(filename);// test.txt
}
foo('test.txt');

arguments 获取的参数,不被赋予初始默认值

1
2
3
4
5
function foo(a=1,b,c=2,d) {
console.log(...arguments); // undefined,100,200,300
console.log(a,b,c,d); // 1,100,200,300
}
foo(undefined,100,200,300);

惰性求值

1
2
3
4
5
6
function foo(msg) {
console.log(msg)
}
var i = 100;
foo(i+=20, i*=2, 'value:'+i); // 120
foo(i); // 240

1
2
3
4
5
6
7
var f = function func2() {
console.log(typeof func2);
}

f() // function

console.log(typeof func2); // undefined

16.
类可以赋予对象成员,但是不能进行函数调用,可以用new 来调用生成实例

17.
绑定函数特殊性质

  1. 内部原型被置为与targetFunc的原型一致
  2. 没有自有的prototype属性
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
class MyFunc {
static foo() {
console.log('prototype method in myFunc');
}
}

class MyFuncEx extends MyFunc {
static foo() {
console.log('own method in MyFuncEX');
}
callMe() {
console.log('call me in MyFuncEx');
}
}

var f = MyFuncEx.bind();
MyFuncEx.foo(); // own method in MyFuncEX
f.foo(); // prototype method in myFunc

console.log('prototype' in Object.getPrototypeOf(MyFunc)) // false

// MyFunc.bind()生成的函数没有prototype
// class MyFuncEX2 extends MyFunc.bind() {} // 会报错

// MyFuncEx原型有继承自MyFunc的prototype,可以声明,但实例不会被继承
class MyFuncEx3 extends MyFuncEx.bind() {}

// 继续继承MyFuncEx原型的prototype属性,实例可继承
class MyFuncEx4 extends MyFuncEx {}
(new MyFuncEx4).callMe() //call me in MyFuncEx

console.log('callMe' in Object.getPrototypeOf(MyFuncEx4.prototype)); // true
console.log('callMe' in Object.getPrototypeOf(MyFuncEx3.prototype)); // false
console.log('callMe' in new MyFuncEx3); // false

18.
函数与对象的区别在于,前者内部结构中初始化了[[call]] 和 [[contruct]] 这两个内部方法
绑定函数通过这两个内部方法实现绑定逻辑
重写这两个方法并使其分别指向一段特有的调用或构建逻辑(以处理暂存在内部槽中的thisArg和arg1…n参数)
代理类对象Proxy则侧重重写了对象的全部13个内部方法
借助代理类也可以实现与绑定函数完全相同的功能
需要在handlers上添加自己的陷阱,以处理[[call]] 和 [[construc]]行为

19.
用属性来替代方法,并在递归中维护this引用

1
2
3
4
5
6
7
8
9
var obj = {
get fact() {
const fact = x=> x && x*fact(x-1) || this.power || 1;
return fact
}
}

obj.power = 100;
obj.fact(9) // 36288000

20.

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
var obj = {
[Symbol.iterator] : function*() {
var obj = {
[Symbol.iterator] : function*() {
for(var i =0;i< 10; i++) yield i;
}
}

console.log(...obj) // 0 1 2 3 4 5 6 7 8 9
}
}

console.log(...obj) // 0 1 2 3 4 5 6 7 8 9

var obj = {
start: 3,
[Symbol.iterator] : function*(start = 5, end = 10) {
var {start, end} = {start, end, ...this};
for(var i=start;i<end; i++) yield i;
}
}

console.log(...obj) // 3 4 5 6 7 8 9
obj.end = 6;
console.log(...obj) // 3 4 5

delete obj.end
delete obj.start
console.log(...obj) // 5 6 7 8 9

21.

1
2
3
4
5
var msg = (function myFunc(num) {
return myFunc = typeof myFunc;
})(10) + ", and upvalue's type is: " + typeof myFunc;

console.log(msg); // function, and upvalue's type is: undefined

第六章

  1. with语句传入的值如果是基础类型数据,会转换成相应类型对象再构建闭包
  2. null 作为对象总是转换为确定的三种值类型0,’null’和false
  3. 对于值类型来说,包装类上的toString()和valueOf方法其实只会对显示方法调用有效
    而并不影响原始值的运算
  4. 类型转换分为两个阶段,其一是转换为原始值,其二是转换为尝试运算的值类型
    这两个阶段,一个受上述”隐式转换逻辑”控制,另一个受具体的运算操作控制
    会尽可能的转换成预期的类型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    x = {
    toString: () => '10',
    valueOf: () => -1
    }

    parseInt(x) ===> 10 因为规定参数是字符串类型

    Math.abs(x) ===> 1

    1 + x ===> 0

    "1" + x ===> 1

    delete x.valueOf

    1+x ===> '1-1'
1
2
3
var x = new Boolean(false)
console.log(x.valueOf()) ===> false
console.log(!!x) ===> false x此时是对象
  1. 一旦对象中声明过Symbol.toPrimitive属性,那么valueOf()与toString() 在值运算的隐式转换中就无效了
  2. switch() 语句对表达式求值,并用该值与case分支中的值进行比较运算时,会采用===操作符进行运算,优先进行类型检测而不会发生类型转换过程

  3. symbol只能转换成bool类型的true,尝试转换成其他值都会报错

  4. 补前缀并转大写

    1
    2
    3
    var x = 1234567

    x.toString(16).padStart(8, '0').toUpperCase() // 0012D687
  5. 数组当对象去结构时,只会保留有值的key

1
2
3
4
5
var arr = [1,2,'345',,12];
var { 0: x, 1:y, length} = arr;
console.log(x,y,length); // 1,2,5
var x = {length: 100, ...arr};
console.log(x.length + ' => ' + Object.keys(x)); // 100 => 0,1,2,4,length
  1. 计算一个字符串中不同字符个数
1
console.log(new Set('abcadf134oaafshjafgoi').size) ==> 14
  1. 是否可重写的限制主要是两个,可引用,可写

    1
    [100][0]++ // 100
  2. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 检测成员是否是重写的
    var isRewrited = function(obj,key) {
    return obj.hasOwnProperty(key) && (key in Object.getPrototypeOf(obj));
    }

    // 检测成员是否是继承来的
    var isInherited = function(obj,key) {
    return (key in obj) && !obj.hasOwnProperty(key);
    }

    // 在对象及其原型链上查找属性的属主对象
    var getPropertyOwner = function f(obj,key) {
    return !obj ? null \
    : obj.hasOwnProperty(key) ? obj
    : f(Object.getPrototypeOf(obj),key)
    }
  3. 给Object.prototype添加成员,与添加全局变量名“效果相当”

1
2
3
4
Object.prototype.x = 100;
console.log(x) // 100
Object.getPrototypeOf(Object.getPrototypeOf(global)) === Object.prototype // true
global.constructor === Object
  1. try-catch-finally中的代码会在try最后return时,先被挂起,执行完finally再return
    如果return的值是个值类型则对返回值没有影响
    但如果是对象,return只保留引用,在finally中对return值进行了修改,那么将会影响返回值
My Little World

TS 学习

发表于 2020-10-06

数组

元素为同一类型

1
2
3
let list:number[] = [1,2,3]
或者
let list Array<number> = [1,2,3]

声明不同类型的元素就要用元祖

元祖 Tuple

已知数组个数和每个元素类型下进行声明的变量类型
如,我想定义一个变量是一个数组,第一项是字符串类型,第二项是数字

1
let x:[string,number]

后续对于相应位置的元素的使用基于该位置上元素的类型
新添加的元素类型可以为之前元素的任一类型,不能是其他类型

1
2
3
4
5
6
7
8
9
10
11
//结合解构
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);

type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}

枚举 enum

对js数据类型补充,可以通过key值互相使用

1
2
3
4
5
6
7
8
enum Color {Red, Green, Blue} //默认从0开始给元素编号
let c: Color = Color.Red; //访问元素返回值
console.log(c) ===>0

enum Color {Red = 1, Green, Blue=4,Yellow} //未赋值的依据赋值的前一个,加1
let c: Color = Color.Green;
console.log(c) ===>2 color.Yellow ===>5
let colorName: string = Color[2]; ===>'Green' Color[6]===>undefined //没赋值的为undefined

Any

任何类型
用于不确定变量类型,仅知道部分元素类型的数组或者避免对这些变量进行校验
区别对象Object类型,Object类型只允许赋值,不允许调用值上面的方法,any可以

void

没有任何类型,通常用于函数没有返回值

1
2
3
function warnUser(): void {
console.log("This is my warning message");
}

Null 和 Undefined

是所有类型的子类型,即该类型变量可以赋值给其他类型
但在指定了–strictNullChecks标记,null和undefined只能赋值给void和它们各自

Never

表示永不存在的值的类型
会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型
变量也可能是 never类型,当它们被永不为真的类型保护所约束时

never类型是任何类型的子类型,也可以赋值给任何类型
然而,没有类型是never的子类型或可以赋值给never类型(除了never本身之外
即使 any也不可以赋值给never

1
2
3
4
// 返回never的函数必须存在无法达到的终点
function error(message: string): never {
throw new Error(message);
}

Object

除基本类型(number,string,boolean,symbol,null,undefined)之外的类型

1
2
3
4
5
6
7
8
9
declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

类型断言

防止ts编译时根据推断的变量类型进行判断
让ts根据设置的类型进行编译判断

1
2
3
4
5
6
7
8
9
10
11
12
const foo = {};
foo.bar = 123; // Error: 'bar' 属性不存在于 ‘{}’
foo.bas = 'hello'; // Error: 'bas' 属性不存在于 '{}'

interface Foo {
bar: number;
bas: string;
}

const foo = {} as Foo;
foo.bar = 123;
foo.bas = 'hello';

接口

一个描述对象用来描述需要的数据类型

描述参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface LabelledValue {
label: string;
color?: string;//在可选属性名字定义的后面加一个?符号,这个属性可传可不传
//可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误
readonly y: number;//属性名前用 readonly来指定只读属性
[propName: string]: any;//索引签名,表示LabelledValue可以有任意数量的属性,只要不是上面已经定义过的 ;结合as 可以避免额外属性检查
}

function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label属性且类型为string的对象。

描述函数类型

1
2
3
4
5
6
7
8
9
interface SearchFunc {
(source: string, subString: string): boolean; //输入参数名,类型和返回值类型
}

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) { //参数名可以和SearchFunc定义的不同,因为只会检查对应位置类型
let result = source.search(subString);
return result > -1;
}

索引签名

描述了对象索引的类型,还有相应的索引返回值类型

1
2
3
4
5
6
7
8
interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

TypeScript支持两种索引签名:字符串和数字。
可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。
这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。
也就是说用 100(一个number)去索引等同于使用”100”(一个string)去索引,因此两者需要保持一致。

继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

联合类型

联合类型表示一个值可以是几种类型之一。
我们用竖线( |)分隔每个类型,
所以 number | string | boolean表示一个值可以是 number, string,或 boolean。

type 和 interface 区别
函数写法
implements 和 extends

My Little World

hook API学习

发表于 2020-10-06
1
import React,{useState,useEffect}from 'react';

useState

用于生成state数据和其对应的替换函数

1
2
3
4
5
6
7
const [state,setState] = useState(initialState)
state 即为声明的变量
setState 即更新函数
initialState即state初始值,不传入时为undefined
使用:
const [count,setCount] = useState(0)
const [obj,setObj] = useState({a:1})

useEffect

render执行结束之后执行effect

配置多个effect时从上到下依次执行

如果effect函数返回了一个函数,这个函数将会在组件卸载时执行,负责清除副作用

如果想让effect仅在某些情况下执行,可以传入第二个参数,
第二参数内为effect依赖的值,当值变化时,才会执行,否则跳过不执行

如果想执行只运行一次的 effect(仅在组件挂载和卸载时执行),可以传递一个空数组([])作为第二个参数。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。这并不属于特殊情况 —— 它依然遵循依赖数组的工作方式。

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
function App() {
const [data,setData] = useState(3);
useEffect(()=>{
console.log('useEffect');
let id = setTimeout(()=>{
setData({a:1})
// clearTimeout(id)
},5000)
return ()=>{ //卸载时执行
clearTimeout(id)
}
},[]) //传入第二个参数,控制是否每次render完都执行

useEffect(()=>{
console.log('会从上到下执行')
})
return (
<div>
{
(()=>{
console.log('render')
return null
})()
}
<p>{JSON.stringify(data)}</p>
</div>
)
}

useContext

1
2
const MyContext = React.createContext();
const value = useContext(MyContext);

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定

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
45
46
47
48
49
50
51
52
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
const ThemeContext = React.createContext(); //创建context 对象,可传入value初始值

function App() {
let [thems,setThems] = useState(themes.dark)
useEffect(()=>{
console.log('useEffect');
let id = setTimeout(()=>{
setThems(themes.light)
},5000)
return ()=>{
clearTimeout(id)
}
},[])
return (
<ThemeContext.Provider value={thems}>
<Toolbar />
<Child2 />
</ThemeContext.Provider>
)
}
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}

function ThemedButton() {
//获取最近的ThemeContext.Provider 标签上value的绑定值
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
I am styled by theme context!
</button>
);
}
//子组件不依赖constext的数据时,使用React.memo可以避免在context有变化时进行的更新
const Child2 = React.memo((props)=>{
console.log('fire')
return <div>xxxxxx</div>
})

useRef

1
const refContainer = useRef(initialValue);

返回对象ref可以用于绑定dom对象
ref.current属性被初始化为传入的参数,绑到DOM对象后,指向绑定的dom
useRef会在每次渲染时返回同一个ref对象,在整个组件的生命周期内是唯一的

ref.current 可以存储那些不需要引起页面重新渲染的数据
如果刻意地想要从某些异步回调中读取最新的state,可以用一个ref来保存,读取,修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function TextInputWithFocusButton() {
const inputEl = useRef(null);//生成绑定对象
const onButtonClick = () => {
// `current` 指向已挂载到 DOM 上的文本输入元素
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
export default App;

当 ref 对象内容发生变化时,useRef 并不会通知你。
变更 .current 属性不会引发组件重新渲染。
如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MeasureExample() {
const [height, setHeight] = useState(0);
console.log('fire')
const measuredRef = useCallback(node => {
if (node !== null) {
setHeight(node.getBoundingClientRect().height);
}
},[]);
return (
<>
<h1 ref={measuredRef}>Hello, world</h1>
<h2>The above header is {Math.round(height)}px tall</h2>
</>
);
}

获取 DOM 节点的位置或是大小的基本方式是使用 callback ref
每当 ref 被附加到一个另一个节点,React 就会调用 callback

当 ref 是一个对象时它并不会把当前 ref 的值的 变化 通知到我们。
使用 callback ref 可以确保 即便子组件延迟显示被测量的节点 (比如为了响应一次点击),我们依然能够在父组件接收到相关的信息,以便更新测量结果

useCallback

1
2
3
4
5
6
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。

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 Child({event,data}){
console.log('child-render')
useEffect(()=>{
console.log('child-effect')
event()
},[event])
return(
<div>
<p>child</p>
<button onClick = {event}>调用父event</button>
</div>
)
}
const set = new Set()
function Test(){
const [count,setCount] = useState(0)
const [data,setData] = useState({})
const handle = useCallback(async ()=>{
function temp(){
setTimeout(()=>{
return 'xxxx'
},1000)
}
const res = await temp()
setData(res)
console.log('paent-useCallback',data)
},[count]) //count变化才会生成新的handle,才会引起child组件的useEffect的执行
set.add(handle)
console.log('parent-render====>',data)
return (
<div>
<button
onClick={e=>{
setCount(count + 1)
}}
>
count++
</button>
<p>set size:{set.size}</p>
<p>count:{count}</p>
<Child event={handle} />
</div>
)
}

useCallback(fn, deps) 相当于 useMemo(() => fn, deps)。

useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一个 memoized 值
会在render 前执行
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。

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
function Test(){
const [count,setCount] = useState(0)
const handle1 = useMemo(()=>{
console.log('handle1',count)
return count
},[])
console.log('render-parent')
return (
<div>
<p>
demo:{count}
<button onClick={()=>setCount(count+1)}>++COUNT</button>
</p>
<Child handle = {handle1} />
</div>
)
}
function Child({handle}){
console.log('render-child')
return (
<div>
<p>child</p>
<p>prop-data:{handle}</p>
</div>
)
}

打印结果:
handle1 0
render-parent
render-child

useReducer

1
const [state, dispatch] = useReducer(reducer, initialArg, init);

useState 的替代方案。
它接收一个形如 (state, action) => newState 的 reducer,
并返回当前的 state 以及与其配套的 dispatch 方法。

适用场景
state 逻辑较复杂且包含多个子值;
下一个 state 依赖于之前的 state
给那些会触发深更新的组件做性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const initialState = {count: 0};

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}

function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

初始化有两种方式
1.传入第二个参数指定初始值

1
2
3
4
const [state, dispatch] = useReducer(
reducer,
{count: initialCount}
);

2.惰性初始化
将 init 函数作为 useReducer 的第三个参数传入,这样初始 state 将被设置为 init(initialArg)

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
function init(initialCount) {
return {count: initialCount};
}

function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}

function Counter({initialCount}) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}

以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利

如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行

参考资料
参考资料

My Little World

一些移动端环境搭建

发表于 2020-08-22

移动端项目引入rem单位

最终解决方案

安装 amfe-flexible

npm install –save-dev amfe-flexible
在main.js中引入

1
import 'amfe-flexible'

在index.html头部插入

1
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">

安装 postcss-px2rem-exclude

npm install –save-dev postcss-px2rem-exclude
修改 .postcssrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = {
"plugins": {
"postcss-import": {},
"autoprefixer": {},
'postcss-px2rem-exclude':{
//rootValue: remConfig.remUnit,
remUnit: 75,
exclude: /node_modules/i,
mediaQuery: false,
minPixelValue: 3
}
}
}

弯路

引入了px2remLoader
引入amfe-flexible,然后安装px2remLoader,并配置如下后,
会造成vue文件内样式单位不会被转为rem,但会把第三方组件库里面的样式单位改成rem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
exports.cssLoaders = function (options) {
//新增
const px2remLoader = {
loader:'px2rem-loader',
options:{
remUnit:remConfig.remUnit
}
}
//修改
function generateLoaders (loader, loaderOptions) {
//const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
const loaders = options.usePostCSS ? [cssLoader, postcssLoader, px2remLoader] : [cssLoader, px2remLoader]
}
}

注意

如果想个别样式单位不参与转换
需要将样式写在普通style标签里面,后面加注释/ no /
如果写在lang = ‘less’ 标签的style里面,
会因为less-loader 将注释去掉,从而避免不了被转换

将svg转成icon

在src/components中新增SvgIcon文件夹
新增index.js 文件,构建SvgIcon组件

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
src/components/SvgIcon/index.js
<template>
<svg :class="svgClass" aria-hidden="true">
<use :xlink:href="iconName"/>
</svg>
</template>

<script>
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
}
}
}
</script>

<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>

在src文件夹下新增icons文件夹
icons文件夹下
新增svg文件夹,存放svg文件,例如user.svg
新增index.js

1
2
3
4
5
6
7
8
9
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'// svg组件

// 全局注册
Vue.component('svg-icon', SvgIcon)

const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)

在main.js中引入

1
import './icons'

安装loader
npm install svg-sprite-loader –save-dev

更改webpack.base.conf.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module: {
rules: [
{ //在url-loader配置前面新增
test: /\.svg$/,
loader: 'svg-sprite-loader',
include: [resolve('src/icons')],
options: {
symbolId: 'icon-[name]'
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
},
exclude: [resolve('src/icons')] //修改url-loader配置
},
]

使用
icon-class 值为svg文件名

1
<svg-icon icon-class="user"/>

My Little World

一些奇思妙想(二)

发表于 2020-08-03

Jquery 链式调用原理

例子

1
$('div').eq(0).show().end().eq(1).hide();

jquery 通过更替this指向实现链式调用
实现

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
45
46
47
48
49
50
51
52
53
54
55
56
jQuery.fn = jQuery.prototype = {
pushStack: function( elems ) {
// Build a new jQuery matched element set
var ret = jQuery.merge( this.constructor(), elems );
// Add the old object onto the stack (as a reference)
ret.prevObject = this;
// Return the newly-formed element set
return ret;
},
eq: function( i ) {
var len = this.length,
j = +i + ( i < 0 ? len : 0 );
return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] );
},
show: function() {
return showHide( this, true );
},
hide: function() {
return showHide( this );
},
end: function() {
return this.prevObject || this.constructor();
},

css: function( name, value ) {
return access( this, function( elem, name, value ) { 。。。。}, name, value, arguments.length > 1 );
}
}
function showHide( elements, show ) {
。。。。
return elements;
}
var access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
var i = 0,
len = elems.length,
bulk = key == null;
if ( toType( key ) === "object" ) {
chainable = true;
for ( i in key ) {。。。。}
} else if ( value !== undefined ) {
chainable = true;
if ( !isFunction( value ) ) { 。。。。}
if ( bulk ) { 。。。。}
if ( fn ) { 。。。。。}
}
//当是链式调用时,返回对象
if ( chainable ) {
return elems;
}
// Gets
if ( bulk ) {
return fn.call( elems );
}

return len ? fn( elems[ 0 ], key ) : emptyGet;
};

当showHide,access传入this的时候,返回的即this,接着可以实现链式

promise then 原理

利用promise 对象去实现一系列功能的过程,可以理解为一个链式结构组成的过程
then 和 catch 函数的调用其实是对后续功能的收集
返回新的promise对象,区别存放每个阶段的执行结果
resolve 和 rejec 函数的调用为后续then/catch收集的函数进行定调,决定到底执行什么
resolve和reject函数执行过程中根据返回值,实现链式结构的拼接和后续函数的执行顺序处理
相关链接
代码实现

async await 原理

借助generator,通过封装promise,利用递归可以实现相同的效果

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
function asyncToGenerator(generatorFunc) {
return function() {
const gen = generatorFunc.apply(this, arguments)
return new Promise((resolve, reject) => {
function step(key, arg) {
let generatorResult
try {
generatorResult = gen[key](arg)
} catch (error) {
return reject(error)
}
const { value, done } = generatorResult
if (done) {
return resolve(value)
} else {
return Promise.resolve(value).then(
val => step('next', val), //递归
err => step('throw', err)
)
}
}
step("next")
})
}
}

KOA app.use next 原理

主要思想是递归实现
详见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let index = -1
return dispatch(0) //从第一个中间件开始执行
function dispatch (i) {
//防止next 在一个中间件中被调用多次
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i] //拿到中间件
if (i === middleware.length) fn = next //如果中间件函数全部调用完,fn 赋值为next,next为undefined
if (!fn) return Promise.resolve() //fn为next=>undefined时成立,即执行到最后返回一个Promise.resolve() 可以继续写then
try {//try-catch 用于保证错误在promise的情况能够正常捕获
//执行中间件函数,传入ctx,next参数,next即dispatch.bind(null, i + 1),
//递归调用下一个中间件函数
//从这一步可以实现await next()时,先执行下一个中间件函数
//中间件有调用next,就利用await暂停当前中间件执行,开始执行下一个中间件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}

VUEROUTER next 原理

利用迭代器思想递归调用钩子函数
在vueRouter 的声明周期使用时,next函数会当做默认参数传递进去,如下

1
2
3
router.beforeEach((to, from, next) => {
next()
})

源码

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
function confirmTransition (route, onComplete, onAbort) {
var queue = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks, //beforeEach钩子函数传入的函数集合
extractUpdateHooks(updated),
activated.map(function (m) { return m.beforeEnter; }),
resolveAsyncComponents(activated)
);
this.pending = route;
var iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort()
}
try {
hook(route, current, function (to) { 。。。。。。});
} catch (e) {
abort(e);
}
};
runQueue(queue, iterator, function () {
var postEnterCbs = [];
var isValid = function () { return this$1.current === route; };
var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
var queue = enterGuards.concat(this$1.router.resolveHooks);
runQueue(queue, iterator, function () {。。。。});
});
}

可见 beforeEach 钩子函数被装在queue数组中,传递给了runQueue函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function runQueue (queue, fn, cb) {
var step = function (index) {
if (index >= queue.length) {
cb();
} else {
if (queue[index]) { //拿到生命周期函数传递给fn函数
fn(queue[index], function () {
step(index + 1);
});
} else {
step(index + 1);
}
}
};
step(0);
}

参数fn函数是 iterator

1
2
3
4
5
6
7
8
9
10
iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort()
}
try {
hook(route, current, function (to) { 。。。。。。});
} catch (e) {
abort(e);
}
};

参数hook即钩子函数,来看传递给它的三个参数

1
hook(route, current, function (to) { 。。。。。。});

第三个参数即next

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function (to) {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this$1.ensureURL(true);
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort();
if (typeof to === 'object' && to.replace) {
this$1.replace(to);
} else {
this$1.push(to);
}
} else {
// confirm transition and pass on the value
next(to);
}
}

这里面的next 指向参数

1
2
3
function () {
step(index + 1);
}

即当我们在钩子函数调用next不传递任何参数时,
只是在调用迭代器函数,递归进行下一个函数的执行

My Little World

一些奇思妙想(一)

发表于 2020-08-02

为什么typeof null –>object?

解析器在存值的时候会以二进制形式存入,
编译时,如果一个值的二进制的前三位均为0的话,
那用typeof检测该值时,拿到的就是object
在null转二进制时,所有位都是0,所以typeof null —->object

为什么NaN != NaN —>true

NaN这个东西在计算机数值里面,是个浮点数,小数部分的位连续多位是不确定的,即在一定范围内的这个浮点数在获取的时候我们都可以叫它NaN

V8引擎调用c++的类库limit 使用它的numeric_limit的quiet_NaN()产生NaN时,
明显生成的是浮点数据类型,即每次生成的NaN是不同的数值
所以不相等

为什么js的对象的key值不能是对象

当试图用非字符串类型做对象key值时,其值会被toString方法静默转为字符串类,
当非字符串类型为对象时,如果作为key的两个对象属于一个类的实例,
那么他们转化成字符串的结果将会是相同的,赋值时,就会导致后续赋值覆盖前者的值

My Little World

一些问题小结

发表于 2020-08-02

响应式两种方案

前后端通过判断userAgent来跳转不同站点

使用media query媒体查询让页面根据不同设备自动改变页面布局和显示

移动端端适配方案

zoom
vm/vh
rem (font-size Media Query)
flex
viewport(scale=1/dpr)
链接

1
2
3
4
5
6
7
屏幕尺寸、屏幕分辨率-->对角线分辨率/屏幕尺寸-->屏幕像素密度PPI
|
设备像素比dpr = 物理像素 / 设备独立像素dip(dp)
|
viewport: scale
|
CSS像素px

实现动画的6种方式

纯JS(利用setInterval,16ms)
SVG (利用标签进行配置)
canvas(利用js API进行配置)
css3-transition(属于过度动画,不能独立实现动画)
css3-animation(纯css实现动画)
requestAnimationFrame(类似setInteval,但性能消耗低)

前两种兼容性良好
后四种适合移动端开发

处理浏览器css样式兼容

reset:清除浏览器默认样式,针对具体元素标签重写样式通过清除保持一致
normalize:样式规范确定的情况下,直接重写样式,通过使用默认统一保持一致
neat:前两者结合针对确定的样式直接重写,不确定的清除

css 样式权重
!important
内联(1000)
id(100)
类(10)
元素(1)

根据浏览器设备屏幕宽度和分辨率加载不同大小的图片

使用Media Query 判断加载不同背景图
使用H5的Picture标签
使用模板语言进行判断,渲染不同图片
请求时带上相关参数,让服务器吐出不同大小图片

JS 可能出现内存泄露的常见场景

闭包函数
全局变量
对象属性循环引用
DOM结点删除时未解绑事件
Map和Set的属性直接删除

其他

1.
数据类型在内存中分布
常量池:boolean,number,string
栈:undefined
堆:object
函数定义区:function(其实在堆里面)
=== 比较变量地址;==会通过V8进行隐式转换

2.
把Object 看作普通类,Object 是Function的实例
Object.proto === Function.prototype
把Object 看作构造函数,functionxxx 是 Object的派生
functionxxx.proto === Function.prototype
Function.prototype.proto === Object.prototype
Object.proto.proto === Object.prototype

  1. CommonJs AMD CMD NodeModules

4.LESS/SASS存在价值,有什么作用
编程式CSS,可维护性,可扩展性,静态样式一定程度上可以实现动态化

5.
前端基础:HTML/CSS/ECMAScript jQuery/bootStrap
新前端:React/VUE/Angular CommonJS/AMD/CMD/NodeModule
工具:npm/bowser 预编译(LESS\SASS\Babel)构建工具(webpack\gulp\grunt)
应用端服务器:Node Web应用\http协议\RPC\发布部署\微服务
跨平台开发:PC(window,mac)前端(web+h5)移动端(andorid,ios)(flutter\react-native)\小程序

My Little World

动态添加路由

发表于 2020-07-18

背景

需要将动态路由添加到当前静态路由的子路由中
即在路由的children属性中添加新路由

原理

使用router.addRoutes方法API进行添加

解决办法

在beforeEach方法中请求动态路由进行添加,
通过设置flag,保证动态路由仅请求一次

问题

直接增加同名路由无法覆盖

1
2
3
4
5
6
7
8
9
10
const routes = [
{ path: '/foo',name:'foo', component: Foo,children:[{ path: '/bar2/:id?',name:'bar2', component: Foo1 }]}
]
const routes1 = [
{ path: '/foo',name:'foo', component: Foo ,children:[{ path: '/bar21/:id?',name:'bar21', component: Foo2 }]},
]
const router = new VueRouter({
routes
})
router.addRoutes(routes1)

添加路由记录时,如果之前已经添加过同名路由,则只会警告,不会更新
所以如果想通过传入不同的children进行子路由更新无法实现

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
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap);
}

routes.forEach(function (route) {
addRouteRecord(pathList, pathMap, nameMap, route);
});

function addRouteRecord ( pathList, pathMap, nameMap ) {
//如果有子路由,递归添加到路由表中
if (route.children) {
route.children.forEach(function (child) {
var childMatchAs = matchAs
? cleanPath((matchAs + "/" + (child.path)))
: undefined;
console.log(childMatchAs)
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
});
}

if (!pathMap[record.path]) { 关键!!!!!!详见下文
pathList.push(record.path);
pathMap[record.path] = record;
}

if (name) {
if (!nameMap[name]) {
nameMap[name] = record;
} else if ( !matchAs) { //直接警告,不更新
warn(
false,
"Duplicate named routes definition: " +
"{ name: \"" + name + "\", path: \"" + (record.path) + "\" }"
);
}
}
}

addRoutes时,子路由作为children属性,依托父路由添加时,子路由被递归添加后,
但父路由因为之前已经添加过,所以不会再对父路由进行任何处理,新添加的子路由
不会跟随父路由更新到路由表中,即addRoutes过程不会对原来老路由产生任何变动
所以新的子路由也不会被添加进去

解决办法一

拼接好路由后,通过重新生成matcher对象,清空原始路由信息,再将最终路由添加进去
详见
缺点:容易引发各种问题,且引起重复路由

解决办法二

按照API规则添加路由

1
2
router.addRoutes(routes: Array<RouteConfig>)
动态添加更多的路由规则。参数必须是一个符合 routes 选项要求的数组。

将待更新的路由对象抽离,拿到动态路由拼接好后,与404路由组成数组,直接添加路由
详见

无法正常刷新

F12刷新浏览器页面后路由对象name为null,无法正常跳转
如果直接再在next函数中添加跳转信息会引起无线循环

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) {
var queue = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(function (m) { return m.beforeEnter; }),
// async components
resolveAsyncComponents(activated)
);

this.pending = route;
var iterator = function (hook, next) {
if (this$1.pending !== route) {
return abort()
}
try {
hook(route, current, function (to) {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this$1.ensureURL(true);
abort(to);
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort();
if (typeof to === 'object' && to.replace) {
this$1.replace(to);
} else {
this$1.push(to); //引起递归调用
}
} else {
// confirm transition and pass on the value
next(to);
}
});
} catch (e) {
abort(e);
}
};
}
History.prototype.push = function push (location, onComplete, onAbort) {
var this$1 = this;

var ref = this;
var fromRoute = ref.current;
this.transitionTo(location, function (route) {
pushState(cleanPath(this$1.base + route.fullPath));
handleScroll(this$1.router, route, fromRoute, false);
onComplete && onComplete(route);
}, onAbort);
};
History.prototype.transitionTo = function transitionTo (
location,
onComplete,
onAbort
) {
var this$1 = this;
var route = this.router.match(location, this.current);
this.confirmTransition(...)
}

在next具体路由后再调用next(),可实现中止导航,即暂停递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if(!hasMenus){
hasMenus = true
let temp = await createRoutes() //拿到数据
router.addRoutes(temp) //添加路由
if(!to.name){ //处理刷新,刷新时,当前页面name会变成null
next({name:to.path.substring(1)})
}
}
if (to.matched.some(res => res.meta.requireAuth)) {// 判断是否需要登录权限
cookies.getAuthorization().then(token=>{
if(!token){
next({
name: 'login',
params: {from: to.name}
})
}else{
next()
}
})
} else {
next() //终止递归
}

小结

多读源码总是用好处的

My Little World

多语言切换文字提取工具

发表于 2020-06-24

背景

项目想要添加语言切换功能,负责该任务的同事由于待翻译的文本提取工作量巨大原因产生进度问题
项目领导委派我帮忙进行辅助提取的工作

原理

i18n原理就是在全局挂载自己的具有翻译功能的方法,然后将需要翻译的文本作为参数传入,
然后函数根据当前语言环境,调用相应语言的翻译文件,通过key-value形式将对应语言的翻译结果返回给页面显示
项目中以简体中文作为key,翻译结果作为value形成翻译文件
如

1
2
3
4
5
6
7
zh-CN.js 简体中文
'开始日期': '开始日期',
'结束日期': '结束日期',

zh-TW.js 繁体中文
'开始日期': '開始日期',
'结束日期': '結束日期',

需要做的大量工作就是将项目中前端写的会展示在页面上的文本全部提取出来,形成文件,
并给相应的简体中文地方用i18n全局函数括起来,通过调用函数返回翻译结果

实现

因为同事使用纯手工作业,所以进度缓慢,按照这种做法,与其说自己比较懒,一个一个手动改,不如说自己觉得这种方式很low
一点都不酷,所以想起之前学习过的nodejs的知识,通过读取文件然后处理文件,最后生成新文件,用新文件替换老文件
处理文件过程会将简体中文提取出来形成文本提取文件,新文件中的简体中文会被i18n的全局函数包裹

读取

1
2
3
4
5
6
7
8
9
10
11
var fs = require('fs'); //引入文件处理模块
function read_file_sync(file_path) {
var data = fs.readFileSync(file_path, 'utf-8'); //读取文件
let arr = file_path.split('/')
let len = arr.length
let temp = arr[len-1].split('.')[0]
//获取文件名,如果待处理文件是子目录下的,保持生成的文本提取文件和新文件与原来文件保持相同目录结构
let fileName = arr[len-2] === 'console'?temp:arr.slice(4,arr.length-1).join('/')+'/'+temp //子目录情况
GetChinese(data,fileName)
}
read_file_sync('./src/components/console/san-manage.vue')

提取生成文件

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
function GetChinese(strValue,fileName) { 
if(strValue=== null || strValue === ""){
return []
 }
let originData = strValue
let arr = getWords(originData) //提取简体中文
let obj = {}
arr.map(item=>{obj[item]=''})
let str = JSON.stringify(obj,''," ")
str = 'export default '+ str
//生成文本文件
fs.writeFile('./words/js/'+fileName+'.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});
//添加i18n全局函数,替换老文件,生成新文件
let index = originData.indexOf('<script>')
let template = originData.substring(0,index)//这里仅替换VUE文件的template模板部分,因为js部分有时涉及到拼接处理,单独处理
let templateResult = partTransfor(template) //得到替换结果
let other = originData.substring(index)//获取vue文件里面的js部分
//二者拼接写入新文件
fs.writeFile('./words/vue/'+fileName+'.vue', templateResult+other, function(err){
if(err) console.log('重写文件操作失败');
else console.log('重写文件操作成功');
});
}

提取

利用正则提取简体中文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getWords(strValue,flag){
//flag为true的时候,提取template部分简体中文,供替换使用
//flag为false的时候,提取全部简体中文,二者正则不同
var reg = flag? /\"?([0-9a-zA-Z\u4e00-\u9fa5|\-|\s|\(|\)\:\:\、\,\,\!\!\。])+/g : /(([0-9a-zA-Z]+)?[\u4e00-\u9fa5|\-|\s|\(|\)\、\,\,\:\:\!\!\。\~]([0-9a-zA-Z]+)?)+/g
//匹配注释的正则,注释不用提取翻译,故提取前先移除
let regCommon = /(\/\/[\w\s\,\,\‘\(\)\—\:\:\=\>\、\?\。a-zA-Z\.\u4e00-\u9fa5|\[|\]|-]*\n)|(\<\!\-\-[\w\s\‘\-\、a-zA-Z\u4e00-\u9fa5|\[|\]|-]*\-\-\>)|(\/\*[\w\‘\s\r\n\*\u4e00-\u9fa5|\-]*\*\/)/g
strValue = strValue.replace(regCommon, function(word) { // 去除注释后的文本
return ''
});
//优化提取结果,去重,过滤,排序
let res = [...new Set(strValue.match(reg))].filter(item=>{return /([\u4e00-\u9fa5])+/g.test(item)}).map(item=>{return item.trim()})
res = res.sort((a,b)=>{
return b.length-a.length
})
return res
}

去掉注释

本来想参考webpack打包时去除注释的正则,发现webpack去除注释,用的分词
直接通过判断astnode类型去判断是否为注释,然后根据配置决定保留去除
后来想起在阅读vue源码的时候有看到过在解析html的时候有形成注释类型的节点
所以从vue 源码中试图寻找注释的正则,发现vue也是只通过注释开头标志//,/** 或者<!–
来判断注释开始,所以我在此基础上自己编写了解析注释的正则并将他们去除

替换

替换文本,用括号括起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function partTransfor(strValue){
let originData = strValue
let res = getWords(strValue,true) //拿到template部分简体中文
for(let str of res){
let placeholder = str.charAt(0) ==='\"'
let temp = placeholder?str.substring(1):str
let reg1 = new RegExp(str,'ig')
let reg2 = new RegExp("placeholder=\"("+temp+")\"",'ig')
let reg = placeholder?reg2:reg1
originData = originData.replace(reg,function(word,str){
if(placeholder){ //如果是placeholder部分的简体中文
return ':placeholder="i18nTitle(\''+str+'\')"'
}else{
if(/[\u4e00-\u9fa5\:\']/.test(originData[str-1]) || /[\u4e00-\u9fa5\:\']/.test(originData[str+word])){ //对于一些包含特殊字符连接的手动处理
return word
}
return '{{i18nTitle(\''+word+'\')}}' //对于纯简体中文用函数包裹进行替换
}
})
}
return originData
}

其实需要处理的情况分三种

1
2
3
1.js里面用this.i18nTitle('xxx')
2.placeholder='xxx'改成:placehoder="i18nTitle('xxxx')"
3.单纯标签的这种,<label>xxxx</label>改成<label>{{i18nTitle(xxxx)}}</label>

另外涉及到组件里面的就直接在组件里需要展示的配置的中文简体字段变量外加全局函数

合并

每个文件提取处理好后,将所有字段合成一个文件,做去重处理

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
var fs = require('fs');
var path = require('path');//解析需要遍历的文件夹
var filePath = path.resolve('./words/js');
var words = []
const getFileOfDirSync = (dir) => {
let files = fs.readdirSync(dir);
let result;
if (files) {
result = files.map((file) => {
let filePath = path.join(dir, file);
if (fs.statSync(filePath).isDirectory()) {
return getFileOfDirSync(filePath); //递归
} else {
let content = fs.readFileSync(filePath, 'utf-8');
content = content.replace('export default','module.exports =') //更改模块编写方法
fs.writeFileSync(filePath,content);//方便读取
let data = require(filePath)
let keys = Object.keys(data)
let comment = content.substring(0,content.indexOf('module'))
return keys;
}
});
}
return [... new Set(result.flat(Infinity))]; //数组摊平去重
}
let res = getFileOfDirSync(filePath)
res = res.sort((a,b)=>{
return b.length-a.length
})
let obj = {}
res.map(item=>{obj[item]=''})
//res.map(item=>{obj[item]=item}) //直接生成简体中文翻译文件
let str = JSON.stringify(obj,''," ")
str = 'export default '+ str
fs.writeFile('./words/cnAll1.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});

翻译

根据简体中文和翻译结果形成翻译文件

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
let obj = {}
let ll = {}
let llkeys = Object.keys({ //翻译结果
'建議選擇購買當前雲裸機已掛載使用的存儲類型和磁盤類型':'',
'超高端存儲:穩定性和安全性最好,適用於核心業務系統':'',
'高端存儲:穩定性和安全性較好,適用於重要業務系統':'',
'中端存儲:穩定性和安全性壹般,適用於壹般業務系統':'',
'普通存儲:穩定性和安全性較差,適用於邊緣業務系統':'',
'NVME:新壹代固態磁盤,性能最好、延時最低,適用於核心業務系統':'',
'SSD:固態磁盤,性能較好、延時較低,適用於重要業務系統':'',
'SAS:機械磁盤,性能壹般、延時壹般,適用於壹般業務系統':'',
'SATA:機械磁盤,性能較差、延時較高,適用於邊緣業務系統和歸檔數據':'',
})
let objkeys = Object.keys({ //简体中文
'建议选择购买当前云裸机已挂载使用的存储类型和磁盘类型':'',
'超高端存储:稳定性和安全性最好,适用于核心业务系统':'',
'高端存储:稳定性和安全性较好,适用于重要业务系统':'',
'中端存储:稳定性和安全性一般,适用于一般业务系统':'',
'普通存储:稳定性和安全性较差,适用于边缘业务系统':'',
'NVME:新一代固态磁盘,性能最好、延时最低,适用于核心业务系统':'',
'SSD:固态磁盘,性能较好、延时较低,适用于重要业务系统':'',
'SAS:机械磁盘,性能一般、延时一般,适用于一般业务系统':'',
'SATA:机械磁盘,性能较差、延时较高,适用于边缘业务系统和归档数据':'',
})
let tw = {}
objkeys.map((item,i)=>{
tw[item] = llkeys[i] //生成繁体翻译文件
obj[item] = item //生成简体中文
})

let str = JSON.stringify(tw,''," ")
str = 'export default '+ str
fs.writeFile('zh-TW.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});

str = JSON.stringify(obj,''," ")
str = 'export default '+ str
fs.writeFile('zh-CN.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});

后续

有新页面上新需要对新老字段进行差值处理,仅新增原来没有的字段

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
let data = require('./words/zh-CN')
let keys = Object.keys(data) //老数据
let dataNew = require('./words/cnAll1')
let keysNew = Object.keys(dataNew)//新页面数据
let obj = {}
let newWords = keysNew.filter(newkey=>{
return !keys.includes(newkey)
}).map(item=>{
obj[item]=item
return item
})//得到新增的文本
let ll = {} //新增文本翻译结果
let llkeys = Object.keys(ll)
let objkeys = Object.keys(obj)
let tw = {}
objkeys.map((item,i)=>{
tw[item] = llkeys[i]
})
//生成简体和繁体翻译文件,再复制粘贴到老文件中
// let str = JSON.stringify(obj,''," ")
let str = JSON.stringify(tw,''," ")
str = 'export default '+ str
fs.writeFile('./words/compareResult3.js', str,function(err){
if(err) console.log('提取操作失败');
else console.log('提取操作成功');
});

小结

其他云多语言切换

阿里云:首先会根据不同域名产生对应不同语言的站点,站点画面风格可能不同,如日本
另外根据语言切换,还会在同一域名下通过切换URI产生不同语言广告页面(切换时,请求cookie中会携带语言类型,aliyun_lang)
广告页面和站点页面可能相同,如香港,台湾的繁体页面,
可能不同,如简体中文和日文的时候

腾讯云:站点只有两种:中国站和国际站,两种站点风格不同
多语言切换在国际站点里面进行,同样通过切换URI,切换不同语言
切换时直接在query里面携带语言信息(https://intl.cloud.tencent.com/jp/?lang=jp&pg=)

平安云:只有两种语言切换,中文和英文,使用同一站点,
切换时通过在cookie里面携带需要的语言类型字段language,
获取相应语言的html和图片

i18n大致原理

i18n 的翻译原理就是在vue.use挂载i18n后,在install阶段会挂载上$t全局函数
我们在使用$t的时候只是单纯传入待翻译的简体中文,
$t会再调用VUEI18N对象上的内部方法,将当前语言环境和语言库传入,
然后通过message[key]的形式找到对应的翻译值,返回

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
Vue.prototype.$t = function (key) {
var values = [], len = arguments.length - 1;
while ( len-- > 0 ) values[ len ] = arguments[ len + 1 ];

var i18n = this.$i18n;
return i18n._t.apply(i18n, [ key, i18n.locale, i18n._getMessages(), this ].concat( values ))
};

VueI18n.prototype._getMessages = function _getMessages () { return this._vm.messages };

//this._vm.messages 值生成
VueI18n.prototype.setLocaleMessage = function setLocaleMessage (locale, message) {
if (this._warnHtmlInMessage === 'warn' || this._warnHtmlInMessage === 'error') {
this._checkLocaleMessage(locale, this._warnHtmlInMessage, message);
}
this._vm.$set(this._vm.messages, locale, message);
};

VueI18n.prototype.mergeLocaleMessage = function mergeLocaleMessage (locale, message) {
if (this._warnHtmlInMessage === 'warn' || this._warnHtmlInMessage === 'error') {
this._checkLocaleMessage(locale, this._warnHtmlInMessage, message);
}
this._vm.$set(this._vm.messages, locale, merge({}, this._vm.messages[locale] || {}, message));
};

VueI18n.prototype._t = function _t (key, _locale, messages, host) {
var ref;

var values = [], len = arguments.length - 4;
while ( len-- > 0 ) values[ len ] = arguments[ len + 4 ];
if (!key) { return '' }

var parsedArgs = parseArgs.apply(void 0, values);
var locale = parsedArgs.locale || _locale;

var ret = this._translate(
messages, locale, this.fallbackLocale, key,
host, 'string', parsedArgs.params
);
if (this._isFallbackRoot(ret)) {
if (!this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
warn(("Fall back to translate the keypath '" + key + "' with root locale."));
}
/* istanbul ignore if */
if (!this._root) { throw Error('unexpected error') }
return (ref = this._root).$t.apply(ref, [ key ].concat( values ))
} else {
ret = this._warnDefault(locale, key, ret, host, values, 'string');
if (this._postTranslation) {
ret = this._postTranslation(ret);
}
return ret
}
};

VueI18n.prototype._translate = function _translate (
messages,
locale,
fallback,
key,
host,
interpolateMode,
args
) {
//messages[locale] 拿到语言库
var res =
this._interpolate(locale, messages[locale], key, host, interpolateMode, args, [key]);
if (!isNull(res)) { return res }

res = this._interpolate(fallback, messages[fallback], key, host, interpolateMode, args, [key]);
if (!isNull(res)) {
if (!this._isSilentTranslationWarn(key) && !this._isSilentFallbackWarn(key)) {
warn(("Fall back to translate the keypath '" + key + "' with '" + fallback + "' locale."));
}
return res
} else {
return null
}
};

VueI18n.prototype._interpolate = function _interpolate (
locale,
message,
key,
host,
interpolateMode,
values,
visitedLinkStack
) {
if (!message) { return null }

var pathRet = this._path.getPathValue(message, key);
if (Array.isArray(pathRet) || isPlainObject(pathRet)) { return pathRet }

var ret;
if (isNull(pathRet)) {
/* istanbul ignore else */
if (isPlainObject(message)) {
ret = message[key]; //从语言库里面拿到对应翻译值
if (typeof ret !== 'string') {
if (!this._isSilentTranslationWarn(key) && !this._isSilentFallback(locale, key)) {
warn(("Value of key '" + key + "' is not a string!"));
}
return null
}
} else {
return null
}
} else {
/* istanbul ignore else */
if (typeof pathRet === 'string') {
ret = pathRet;
} else {
if (!this._isSilentTranslationWarn(key) && !this._isSilentFallback(locale, key)) {
warn(("Value of key '" + key + "' is not a string!"));
}
return null
}
}

// Check for the existence of links within the translated string
if (ret.indexOf('@:') >= 0 || ret.indexOf('@.') >= 0) {
ret = this._link(locale, message, ret, host, 'raw', values, visitedLinkStack);
}

return this._render(ret, interpolateMode, values, key)
};

VueI18n.prototype._render = function _render (message, interpolateMode, values, path) {
var ret = this._formatter.interpolate(message, values, path);
// If the custom formatter refuses to work - apply the default one
if (!ret) {
ret = defaultFormatter.interpolate(message, values, path);
}
// if interpolateMode is **not** 'string' ('row'),
// return the compiled data (e.g. ['foo', VNode, 'bar']) with formatter
return interpolateMode === 'string' && typeof ret !== 'string' ? ret.join('') : ret
};

1…678…26
YooHannah

YooHannah

260 日志
1 分类
23 标签
RSS
© 2025 YooHannah
由 Hexo 强力驱动
主题 - NexT.Pisces