FE 学习笔记

489 阅读24分钟

一、FE

1.HTML(Hyper Text Markup Language)

 HTML 不是编程语言,是标记语言,用于设定文档或网站页面的结构。

 HTML 是 XML 的子集,是包含网站信息的一系列 tag 的嵌套

1.1 元素

1.1.1 元素的组成:

image.png

1)开始标签(Opening tag): 本例中为 < p >,其中 p 为元素名称。

2)结束标签(Closing tag): 与开始标签相似,只是其在元素名之前包含了一个斜杠。

3)内容(Content): 元素的内容,本例中就是所输入的文本。不包含内容的元素称为空元素。

4)属性(Attribute): 属性是可选部分。属性应该包含:

image.png
  • 在属性与元素名称或上一个属性之间的空格符。

  • 属性的名称,并接上一个等号。

  • 由引号所包围的属性值。

1.1.2 嵌套元素:

开始标签的顺序与结束标签的顺序相反。

<p>My cat is <strong>very</strong> grumpy.</p>

1.2 HTML 文档

<!-- 文档类型 -->
<!DOCTYPE html>  
<html>  <!-- 根元素 -->
  <head>  <!-- 该元素的内容不会被显示在浏览器内容页上 -->
    <meta charset="utf-8">
    <title>My test page</title>
  </head>
  <body>
    <img src="images/firefox-icon.png" alt="My test image">
  </body>
</html>

1.3 图像

图像元素没有内容,没有结束标签。这是因为图像元素不需要通过内容来产生效果,它的作用是向其所在的位置嵌入一个图像。

<img src="images/firefox-icon.png" alt="My test image" />
<!-- src 的值是图像文件路径,
     alt 的值是图像的描述文本,当用户无法看见图像时,便会使用该内容 -->

例如,当文件路径有误时,便会显示 alt 的内容。

image.png

1.4 文本标记元素

1.4.1 标题(Heading)

HTML 文档包含六个级别的标题,一般使用前四个标题即可。

<h1>主标题</h1>
<h2>顶层标题</h2>
<h3>子标题</h3>
<h4>次子标题</h4>

1.4.2 段落(Paragraph)

<p> 用于指定段落,通常用于指定常规的文本内容。

1.4.3 列表(List)

无序列表用 ul 表示,有序列表用 ol 表示。列表的每一个项目用 li 表示。

<p>At Mozilla, we're a global community of</p>

<ul>
  <li>technologists</li>
  <li>thinkers</li>
  <li>builders</li>
</ul>

<p>working together… </p>

1.4.4 链接

<a href="https://www.mozilla.org/zh-CN/about/manifesto/">Mozilla Manifesto</a>
<!-- href 代表超文本引用(hypertext reference) -->

1.5 基础规则

  • 使用 XML 语法。
  • 信息被存储在类树结构 DOM(Document Object Model)中。
  • 使得文档具有语义结构,方便计算机理解其内容。
  • 不能包含如何展示 HTML 的信息(此为 CSS 的工作),例如颜色、字体、位置等。
截屏2022-07-29 18.30.29.png

1.6 DOM

 每个 Node 只能有一个父节点,但是可以有很多子节点。

截屏2022-07-29 18.32.07.png

1.7 main tags

  • <div>:用于表示包含信息的矩形区域。
  • <img/>:图像。
  • <a>:指向另一个 URL 的可点击的链接。
  • <p>:文本段落;
  • <h1>:标题。
  • <input>:让用户输入信息的窗体。
  • <style>:插入 CSS 规则。
  • <script>:执行 JavaScript 代码。
  • <span>:空 tag,不执行任何事情。

2.CSS(Cascading Style Sheet)

 层叠样式表(Cascading Style Sheet)是为网页添加样式的代码。用于指定如何展示存储在 HTML 中的文档信息。CSS 不是真正的编程语言,而是一门样式表语言,也就是说人们可以用它来选择性地为 HTML 元素添加样式。

2.1 CSS 规则集

以元素选择器为例:

image.png

整个结构称为规则集(简称“规则”),各部分释义如下:

  • 选择器(Selector)

    HTML 元素的名称位于规则集起始位置。要给不同元素添加样式只需要更改选择器即可。

  • 声明(Declaration)

    一个单独的规则,如 color: red; 用来指定要添加样式的元素的属性。

  • 属性(Properties)

    改变 HTML 元素样式的方式。

  • 属性的值(Property value)

    在属性的右边,冒号后面即属性的值。

其他重要的语法:

  • 每个规则集(除了选择器)都应该被包含在成对的大括号里({})。
  • 在每个声明里要用冒号(:)将属性与属性值分隔开。
  • 在每个规则集里要用分号(;)将各个声明分隔开。

2.1.1 同时修改多个属性

p {
  color: red;
  width: 500px;
  border: 1px solid black;
}

2.1.2 多元素选择

将不同的选择器用逗号分隔开。

p, li, h1 {
  color: red;
}

选择器的种类有很多,更多信息: link.

2.1.3 注释

使用 /* */.

2.2 盒模型

CSS 布局主要基于盒模型。每个占据页面空间的块都有如下属性:

  • padding:即内边距,围绕着内容(比如段落)的空间。
  • border:即边框,紧接着内边距的线。
  • margin:即外边距,围绕元素外部的空间。
image.png

2.3 块级元素与内联元素

<body> 元素是块级元素,这意味着它占据了页面的空间,并且能够被赋予外边距和其他改变间距的值。

<img>内联元素,不具备块级元素的一些功能。所以为了使图像有外边距,我们必须使用 display: block 给予其块级行为。

3.JavaScript

 无类型的类 C 语言。

浏览器会按照 .html 文件中代码的顺序来加载 HTML。如果先加载的 JavaScript 代码期望修改其下方的 HTML,那么它可能由于 HTML 尚未被加载而失效。因此,将 JavaScript 代码放在.html的底部附近通常是较好的选择。

3.1 变量与运算符

JavaScript 里一切皆对象。

表达式与运算符: link.

// 推荐使用 let 关键字

// 字符串的值必须用 单引号/双引号 扩起来
let value1 = 'test';

var value2 = 10;

// true/false
let value3 = true;

let value4 = [1, 'test', 20, true];

// 等于
value2 === 9; //false
// 不等于
value2 !== 9; //true

3.2 函数

function multiply(num1, num2) {
  let result = num1 * num2;
  return result;
}

3.3 事件

事件可以为网页提供交互能力。

3.3.1 事件处理器 / 事件监听器

指事件被触发时所运行的代码块。当定义了一个用来回应事件被触发的代码块时,即是注册了一个事件处理器。

3.3.2 EventListener

// 代码中的回调函数为匿名函数

document.querySelector("html").addEventListener("click", function () {
  alert("test");
});

document.querySelector('html').addEventListener('click', () => {
  alert('test');
});

btn.removeEventListener('click', bgChange);

3.3.3 通过元素属性来注册事件

const btn = document.querySelector('button');

btn.onclick = function() {
    alert('test');
}

3.3.4 给元素注册多个处理事件

// functionB 会覆盖 functionA
myElement.onclick = functionA;
myElement.onclick = functionB;

// 注册了两个事件
myElement.addEventListener('click', functionA);
myElement.addEventListener('click', functionB);

3.3.5 事件对象

被自动传递给事件处理函数,以提供额外的功能和信息。

事件对象的 target 属性是事件对应的元素的引用。

function bgChange(e) {
  const rndCol = 'rgb(' + random(255) + ',' + random(255) + ',' + random(255) + ')';
  e.target.style.backgroundColor = rndCol;
  console.log(e);
}

btn.addEventListener('click', bgChange);

3.3.6 事件的捕获和冒泡

当一个事件发生在具有父元素的元素上时,现代浏览器运行两个不同的阶段。

默认情况下,所有事件处理程序都在冒泡阶段进行注册。

如果想在捕获阶段注册事件,则可以通过使用 addEventListener() 来注册处理程序,并将可选的第三个属性设置为 true 来实现目的。

捕获阶段:

  • 浏览器首先检查元素的最外层祖先<html>,是否在捕获阶段中注册了一个事件处理程序(例如 onclick),如果是,则运行它。
  • 然后,它移动到<html>中被点击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。

冒泡阶段,恰恰相反:

  • 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个事件处理程序(例如 onclick),如果是,则运行它。
  • 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达<html>元素。

stopPropagation():

当在事件对象上调用该函数时,它只会让当前事件处理程序运行,不会让事件在冒泡链上继续上升。

video.onclick = function(e) {
  e.stopPropagation();
  video.play();
};

3.3.7 事件委托

如果想要在大量子元素中单击任何一个都可以运行一段代码,则可以将事件监听器设置在其父节点上,并让子节点上发生的事件冒泡到父节点上,而不是每个子节点都单独设置事件监听器。

一个很好的例子是一系列列表项,如果想让每个列表项被点击时弹出一条信息,则可以将click单击事件监听器设置在父元素<ul>上,这样事件就会从列表项冒泡到其父元素<ul>上。

3.4 对象

对象使得能够将信息安全地锁在对象内部,以防止它们被损坏。

3.4.1 对象的字面量

对象有时被称为关联数组 (associative array)——对象做了字符串到值的映射,而数组做的是数字到值的映射。

手动的写出对象的内容来创建一个对象:

var objectName = {
  member1Name : member1Value,
  member2Name : member2Value,
  member3Name : member3Value
}

var person = {
  name : ['Bob', 'Smith'],
  age : 32,
  gender : 'male',
  interests : ['music', 'skiing'],
  bio : function() {
    alert(this.name[0] + ' ' + this.name[1] + ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1] + '.');
  },
  greeting: function() {
    alert('Hi! I\'m ' + this.name[0] + '.');
  }
};

3.4.2 访问对象

获取对象成员的值:

// 点表示法
person.age
person.interests[1]
person.bio()

// 括号表示法
person['age']
person['name'][0]

设置对象成员的值:

点表示法不能动态设置成员。点表示法只接受字面量作为成员名称,而不接受变量作为成员名称。

person.age = 45
person['name']['last'] = 'Cratchit'

// 设置新成员
person['eyes'] = 'hazel'
person.farewell = function() { alert("Bye everybody!") }

// 动态设置新成员
var myDataName = 'height'
var myDataValue = '1.75m'
person[myDataName] = myDataValue

3.4.3 对象原型

函数的原型:

在 javascript 中,函数可以有属性。每个函数都有一个特殊的属性叫作原型(prototype)

原型链中的方法和属性没有被复制到其他对象。上游对象的属性和方法被不会复制到下游的对象实例中,下游对象本身虽然没有定义这些成员,但浏览器会通过上溯原型链,进而从上游对象中找到它们。

function doSomething(){}
doSomething.prototype.foo = "bar"; // add a property onto the prototype
doSomething.pro = "test"
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value"; // add a property onto the object
console.log( doSomeInstancing );

结果:

{
    prop: "some value",
    __proto__: {
        foo: "bar",
        constructor: ƒ doSomething(), //  pro 属性在内部
        __proto__: {
            constructor: ƒ Object(),
            hasOwnProperty: ƒ hasOwnProperty(),
            isPrototypeOf: ƒ isPrototypeOf(),
            propertyIsEnumerable: ƒ propertyIsEnumerable(),
            toLocaleString: ƒ toLocaleString(),
            toString: ƒ toString(),
            valueOf: ƒ valueOf()
        }
    }
}

doSomeInstancing 的 __proto__ 属性就是doSomething.prototype.

当访问 doSomeInstancing 的属性时:

  • 浏览器首先查找 doSomeInstancing 是否有这个属性。
  • 如果没有,浏览器就会在 doSomeInstancing 的 __proto__ 中查找这个属性 (也就是 doSomething.prototype).
  • 如果没有,浏览器就会去查找 doSomeInstancing 的 __proto__ 的 __proto__(也就是 doSomething.prototype 的 __proto__ (也就是 Object.prototype)) ,看它是否有这个属性。(默认情况下,所有函数的原型属性的 __proto__ 是 window.Object.prototype
  • 如果没有,就会在 doSomeInstancing 的 __proto__ 的 __proto__ 的 __proto__ 里面查找。然而 doSomeInstancing 的 __proto__ 的 __proto__ 的 __proto__ 不存在。
  • 最后,原型链上的所有 __proto__ 都被找完了,浏所有已声明的 __proto__ 上都不存在这个属性。
  • 最终得出结论,此属性是 undefined.
function doSomething(){}
doSomething.prototype.foo = "bar";
doSomething.pro = "test";  //  在此声明的属性可以直接访问
var doSomeInstancing = new doSomething();
doSomeInstancing.prop = "some value";
console.log("doSomeInstancing.prop:      " + doSomeInstancing.prop);
console.log("doSomeInstancing.foo:       " + doSomeInstancing.foo);
console.log("doSomething.prop:           " + doSomething.prop);
console.log("doSomething.foo:            " + doSomething.foo);  //  不能直接访问
console.log("doSomething.pro:            " + doSomething.pro);
console.log("doSomething.prototype.prop: " + doSomething.prototype.prop);
console.log("doSomething.prototype.foo:  " + doSomething.prototype.foo);

结果:

doSomeInstancing.prop:      some value
doSomeInstancing.foo:       bar
doSomething.prop:           undefined
doSomething.foo:            undefined
doSomething.pro:            test
doSomething.prototype.prop: undefined
doSomething.prototype.foo:  bar

prototype 属性:

prototype 属性指向一个对象(即原型对象),在这个对象中定义需要被继承的成员。

prototype 属性中定义的属性和方法,可被继承。不在此属性中定义的属性和方法,不可被继承。

以 Object.prototype. 开头的属性适用于任何继承自 Object() 的对象类型,包括使用构造器创建的新的对象实例。

Object.is()Object.keys(),以及其他不在 prototype 对象内的成员,不会被“对象实例”或“继承自 Object() 的对象类型”所继承。这些方法/属性仅能被 Object() 构造器自身使用。

create() 函数:

create() 实际做的是从指定原型对象创建一个新的对象。下述代码以 person1 为原型对象创建了 person2 对象。

var person2 = Object.create(person1);

// 返回 person1
person2.__proto__

constructor 属性:

每个实例对象都从原型中继承了一个 constructor 属性,该属性指向了用于构造此实例对象的构造函数。

// 返回 Person() 构造器
person1.constructor

// 返回构造器的名称
person1.constructor.name

修改原型: 在原型链中,上游对象的属性和方法不会被复制到下游的对象实例中。下游对象本身虽然没有定义这些成员,但浏览器会通过上溯原型链,进而从上游对象中找到它们。

function Person(first, last, age, gender, interests) {
  // 属性与方法定义
};

var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);

Person.prototype.farewell = function() {
  alert(this.name.first + ' has left the building. Bye for now!');
}

// `farewell()` 方法仍然可用于 `person1` 对象实例——旧有对象实例的可用功能被自动更新了

常用的对象定义模式:

在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此一来,构造器只包含属性定义,而方法则被分装在不同的代码块中,从而使得代码更具可读性。

// 构造器及其属性定义

function Test(a,b,c,d) {
  // 属性定义
};

// 定义第一个方法

Test.prototype.x = function () { ... }

// 定义第二个方法

Test.prototype.y = function () { ... }

3.4.4 面向对象

  • 构造函数:在 JavaScript 中,构造函数可以用于实现类的定义,包括定义类的方法。此外,原型也可以用于实现类的定义。例如,如果一个方法定义于构造函数的 prototype 属性中,那么所有由该构造函数创造出来的对象都可以通过原型使用该方法,而我们也不再需要将它定义在构造函数中。
  • 原型链:原型链类似于继承,但是并不一样。

JS 的面向对象编程与基于类的面向对象编程的区别:

  • 在基于类的面向对象编程中,类与对象是两个不同的概念,对象通常是由类创造出来的实例。而在 JavaScript 中,经常会使用函数或对象字面量创建对象,即可以在没有特定的类定义的情况下创建对象。
  • 在原型链中,每一个层级都代表了一个不同的对象,不同的对象之间通过 __proto__ 属性链接起来。原型链的行为并不太像是继承,而更像是委托(delegation)。

3.5 JS 中的类

3.5.1 类和构造函数

如果不需要自定义初始化函数,则可以省略。此时系统会自动生成默认的构造函数。

name; 这一声明是可被省略的,因为构造函数中的 this.name = name; 代码会在初始化 name 属性前自动创建它。

class Person {

  name = '';

  constructor(name) {
    this.name = name;
  }

  introduceSelf() {
    console.log(`Hi! I'm ${this.name}`);
  }

}

使用类的名字来调用构造函数。

const giles = new Person('Giles');

giles.introduceSelf(); // Hi! I'm Giles

3.5.2 继承

class Professor extends Person {

  teaches;

  constructor(name, teaches) {
    super(name);  // 调用父类的构造函数
    this.teaches = teaches;
  }

  introduceSelf() {
    console.log(`My name is ${this.name}, and I will be your ${this.teaches} professor.`);
  }

  grade(paper) {
    const grade = Math.floor(Math.random() * (5 - 1) + 1);
    console.log(grade);
  }
}

3.5.3 私有属性和私有方法

名称要以 # 开头,并且只能在类内部使用。在类外使用,浏览器会报错。

class Student extends Person {

  #year;  // 私有属性

  constructor(name, year) {
    super(name);
    this.#year = year;
  }

  introduceSelf() {
    console.log(`Hi! I'm ${this.name}, and I'm in year ${this.#year}.`);
  }

  somePublicMethod() {
    this.#somePrivateMethod();
  }

  // 私有方法
  #somePrivateMethod() {
    console.log('You called me?');
  }
}

3.6 JSON

3.6.1 JSON 对象

JSON 对象基于 JavaScript 对象或数组对象。 基于数组对象:

[
  {
    "name" : "Molecule Man",
    "age" : 29,
    "secretIdentity" : "Dan Jukes",
    "powers" : [
      "Radiation resistance",
      "Turning tiny",
      "Radiation blast"
    ]
  },
  {
    "name" : "Madame Uppercut",
    "age" : 39,
    "secretIdentity" : "Jane Wilson",
    "powers" : [
      "Million tonne punch",
      "Damage resistance",
      "Superhuman reflexes"
    ]
  }
]

注意事项:

  • JSON 是一种纯数据格式,它只包含属性,不包含方法。
  • JSON 要求在字符串和属性名称周围使用双引号。不能使用单引号。
  • JSON 中只有带引号的字符串可以用作属性。

3.6.2 对象和字符串之间的转换

浏览器拥有一个内建的 JSON,包含以下两个方法。

  • parse(): 构造由字符串描述的 JavaScript 值或对象。
  • stringify(): 将一个 JavaScript 对象或值转换为 JSON 字符串。
const json = '{"result":true, "count":42}';
const obj = JSON.parse(json);

console.log(obj.count);
// Expected output: 42

console.log(obj.result);
// Expected output: true

console.log(JSON.stringify({ x: 5, y: 6 }));
// Expected output: "{"x":5,"y":6}"

3.7 异步操作

3.7.1 回调

事件处理程序实际上就是异步编程的一种形式:事件处理程序将在事件发生时被调用,而非立即被调用。事件处理程序是一种特殊类型的回调函数。而回调函数曾经是 JavaScript 中实现异步函数的主要方式。

然而,当回调函数本身需要调用其他同样接受回调函数的函数时,基于回调的代码会变得难以理解。当需要执行一些分解成一系列异步函数的操作时,这将变得十分常见。

同步函数逻辑:

function doStep1(init) {
  return init + 1;
}
function doStep2(init) {
  return init + 2;
}
function doStep3(init) {
  return init + 3;
}
function doOperation() {
  let result = 0;
  result = doStep1(result);
  result = doStep2(result);
  result = doStep3(result);
  console.log(`结果:${result}`);
}
doOperation();

使用回调来实现异步操作:

function doStep1(init, callback) {
  const result = init + 1;
  callback(result);
}
function doStep2(init, callback) {
  const result = init + 2;
  callback(result);
}
function doStep3(init, callback) {
  const result = init + 3;
  callback(result);
}
function doOperation() {
  doStep1(0, result1 => {
    doStep2(result1, result2 => {
      doStep3(result2, result3 => {
        console.log(`结果:${result3}`);
      });
    });
  });
}
doOperation();

因为必须在回调函数中调用回调函数,就得到了上述深度嵌套的 doOperation() 函数,这难以阅读和调试。在一些地方这被称为“回调地狱”或“厄运金字塔”。面对这样的嵌套回调,处理错误也会变得非常困难:必须在“金字塔”的每一级处理错误,而不是在最高一级一次完成错误处理。

基于以上原因,大多数现代异步 API 都不使用回调。

3.7.2 Promise

Promise 对象:

Promise 是一个由异步函数返回的可以指示当前操作所处状态的对象。当 Promise 对象被返回给调用者时,操作可能还没有完成。可以将处理函数附加到 Promise 对象上,当操作完成时(无论成功或失败),这些处理函数将被执行。

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

console.log(fetchPromise);

// 当异步操作成功时,传递给 `then()` 的处理函数将被调用
fetchPromise.then( response => {
  console.log(`已收到响应:${response.status}`);
});

console.log("已发送请求……");


// 输出结果
Promise { <state>: "pending" }
已发送请求……
已收到响应:200

链式使用 Promise:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

fetchPromise.then( response => {
  const jsonPromise = response.json();
  jsonPromise.then( json => {
    console.log(json[0].name);
  });
});

上述代码为回调嵌套,回调函数为 then() 函数。

Promise 的优雅之处在于 then() 本身也会返回一个 Promise,这个 Promise 将指示 then() 中调用的异步函数的完成状态。因此可以直接使用json() 返回的 Promise,并在该返回值上调用第二个 then()。如此一来,就不必在第一个 then() 的处理程序中调用第二个 then()。 据此可将上述回调代码改写为:

const fetchPromise = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

fetchPromise
  .then( response => {
    return response.json();
  })
  .then( json => {
    console.log(json[0].name);
  });

这被称为 Promise 链。当需要连续进行异步函数调用时,可以通过这种方式来避免回调嵌套。

错误处理:

当异步操作成功时,传递给 then() 的处理函数被调用;

当异步操作失败时,传递给 catch() 的处理函数被调用。

如果将 catch() 添加到 Promise 链的末尾,那么它就可以在任何异步函数失败时被调用。进而使得可以在一个地方处理所有异步调用的错误。

const fetchPromise = fetch('bad-scheme://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');

fetchPromise
  .then( response => {
    if (!response.ok) {
      throw new Error(`HTTP 请求错误:${response.status}`);
    }
    return response.json();
  })
  .then( json => {
    console.log(json[0].name);
  })
  .catch( error => {
    console.error(`无法获取产品列表:${error}`);
  });

Promise 的状态:

  • 待定(pending) :初始状态,此时请求还在进行中。
  • 已兑现(fulfilled) :意味着操作成功完成。此时 then() 处理函数被调用。
  • 已拒绝(rejected) :意味着操作失败。此时 catch() 处理函数被调用。

“成功”、“失败”的含义取决于所使用的 API:例如,fetch() 认为服务器返回一个错误(如404 Not Found)时,仍为成功请求,但如果网络错误,阻止请求被发送,则认为请求失败。

合并使用多个异步函数:

有时需要所有的 Promise 都得到实现,但它们并不相互依赖。在这种情况下,可以使用 Promise.all() 方法。它接收一个 Promise 数组,并返回一个单一的 Promise:

  • 当且仅当数组中所有 Promise 都被兑现时,才会通知 then() 处理函数并提供一个包含所有响应的数组,数组中响应的顺序与被传入 all() 的 Promise 的顺序相同。
  • 如果数组中有任何一个 Promise 被拒绝。此时,catch() 处理函数被调用,并提供被拒绝的 Promise 所抛出的错误。

Promise.any():当 Promise 数组中的任何一个被兑现时它就会被兑现,如果所有的 Promise 都被拒绝,它也会被拒绝。在这种情况下,无法预测哪个获取请求会先被兑现。

const fetchPromise1 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
const fetchPromise2 = fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/not-found');
const fetchPromise3 = fetch('https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json');

Promise.all([fetchPromise1, fetchPromise2, fetchPromise3])
  .then( responses => {
    for (const response of responses) {
      console.log(`${response.url}${response.status}`);
    }
  })
  .catch( error => {
    console.error(`获取失败:${error}`)
  });
  
Promise.any([fetchPromise1, fetchPromise2, fetchPromise3])
  .then( response => {
    console.log(`${response.url}${response.status}`);
  })
  .catch( error => {
    console.error(`获取失败:${error}`)
  });

async 和 await:

在一个函数的开头添加 async,就可以使其成为一个异步函数。

在调用一个返回 Promise 的函数之前使用 await 关键字,会使得程序在该点上等待,直到函数执行完成。此时 Promise 的响应将被作为返回值返回,或者抛出错误。await 只能用于 async 声明的异步函数中。

这使得能够编写像同步代码一样的异步函数。

// 这是一个异步函数
async function fetchProducts() {
  try {
    // 在这一行之后,我们的函数将等待 `fetch()` 调用完成
    // 调用 `fetch()` 将返回一个“响应”或抛出一个错误
    const response = await fetch('https://mdn.github.io/learning-area/javascript/apis/fetching-data/can-store/products.json');
    if (!response.ok) {
      throw new Error(`HTTP 请求错误:${response.status}`);
    }
    // 在这一行之后,我们的函数将等待 `response.json()` 的调用完成
    // `response.json()` 调用将返回 JSON 对象或抛出一个错误
    const json = await response.json();
    console.log(json[0].name);
  }
  catch(error) {
    console.error(`无法获取产品列表:${error}`);
  }
}

fetchProducts();

await 强制异步操作以串联的方式完成。如果下一个操作取决于上一个操作,这是必要的。否则,像 Promise.all() 这样的操作会有更好的性能。

实现基于 Promise 的 API:

Promise() 构造器使用单个函数作为参数。这个函数称作执行器(executor)。当创建一个新的 promise 时,需要实现这个执行器。

执行器采用两个参数,这两个参数都是函数,被称作 resolve 和 reject

如果异步函数成功了,就调用 resolve,如果失败了,就调用 reject。如果执行器函数抛出了一个错误,reject 会被自动调用。

const name = document.querySelector('#name');
const delay = document.querySelector('#delay');
const button = document.querySelector('#set-alarm');
const output = document.querySelector('#output');

function alarm(person, delay) {
  return new Promise((resolve, reject) => {
    if (delay < 0) {
      throw new Error('Alarm delay must not be negative');  // error
    }
    window.setTimeout(() => {
      resolve(`Wake up, ${person}!`);  // message
    }, delay);
  });
}

button.addEventListener('click', () => {
  alarm(name.value, delay.value)
    .then(message => output.textContent = message)
    .catch(error => output.textContent = `Couldn't set alarm: ${error}`);
});

3.7.3 Workers

Workers 使得程序能在其他线程中运行某些任务。为了避免多线程问题,主代码和 worker 代码永远不能直接访问彼此的变量。

Workers 和主代码运行在完全分离的环境中,只有通过相互发送消息来进行交互。特别的,这意味着 workers 不能访问 DOM(窗口、文档、页面元素等)。

dedicated worker:

只要 worker 被创建了,woker 脚本就会开始执行。

main.js:

// 在 "generate.js" 中创建一个新的 worker
const worker = new Worker('./generate.js');

// 当用户点击 "Generate primes" 时,给 worker 发送一条消息。
// 消息中的 command 属性是 "generate", 还包含另外一个属性 "quota",即要生成的质数。
document.querySelector('#generate').addEventListener('click', () => {
  const quota = document.querySelector('#quota').value;
  worker.postMessage({
    command: 'generate',
    quota: quota
  });
});

// 当 worker 给主线程回发一条消息时,为用户更新 output 框,包含生成的质数(从 message 中获取)。
worker.addEventListener('message', message => {
  document.querySelector('#output').textContent = `Finished generating ${message.data} primes!`;
});

generate.js:

// 监听主线程中的消息
// 在 worker 中是一个全局函数
addEventListener("message", message => {
  if (message.data.command === 'generate') {
    generatePrimes(message.data.quota);
  }
});

function generatePrimes(quota) {
  
  // do something
  
  // 完成后给主线程发送一条包含生成的质数数量的消息
  postMessage(primes.length);
}

SharedWorker: 可以由运行在不同窗口中的多个不同脚本共享。

Service worker: 行为类似于代理服务器,缓存资源以便于 web 应用程序可以在用户离线时工作。

3.8 浏览器视图

image.png

  • navigator: 表示浏览器存在于 web 上的状态和标识(即用户代理)。在 JavaScript 中,用Navigator来表示。可以用这个对象获取一些信息,比如来自用户摄像头的地理信息、用户偏爱的语言、多媒体流等等。
  • window: 是载入浏览器的标签,在 JavaScript 中用Window对象来表示。使用这个对象的可用方法,可以返回窗口的大小,操作载入窗口的文档,存储客户端上文档的特殊数据(例如使用本地数据库或其他存储设备),为当前窗口绑定 event handler 等。
  • document: 在浏览器中用 DOM 表示,是载入窗口的实际页面,在 JavaScript 中用Document对象表示,可以用这个对象来返回和操作文档中 HTML 和 CSS 上的信息。例如获取 DOM 中一个元素的引用,修改其文本内容,并应用新的样式等。

3.8.1 文档对象模型(DOM)

用 DOM 来表示在浏览器标签中载入的文档,这是一个由浏览器生成的“树结构”。

这一结构使得编程语言可以很容易的访问 HTML 结构,能够将样式和其他信息应用于正确的元素。

当页面呈现完成以后,开发人员可以用 JavaScript 操作 DOM。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Simple DOM example</title>
  </head>
  <body>
      <section>
        <img src="dinosaur.png" alt="A red Tyrannosaurus Rex: A two legged dinosaur standing upright like a human, with small arms, and a large head with lots of sharp teeth.">
        <p>Here we will add a link to the <a href="https://www.mozilla.org/">Mozilla homepage</a></p>
      </section>
  </body>
</html>

DOM 树:

image.png
  • 元素节点: 一个元素,存在于 DOM 中。
  • 根节点: 树中顶层节点,在 HTML 的情况下,总是一个HTML节点(其他标记词汇,如 SVG 和定制 XML 将有不同的根元素)。
  • 子节点: 直接位于另一个节点内的节点。例如上面例子中,IMGSECTION的子节点。
  • 后代节点: 位于另一个节点内任意位置的节点。例如上面例子中,IMGSECTION的子节点,也是一个后代节点。但IMG不是BODY的子节点,因为它在树中低了BODY两级,但它是BODY的后代之一。
  • 父节点: 里面有另一个节点的节点。例如上面的例子中BODYSECTION的父节点。
  • 兄弟节点: DOM 树中位于同一等级的节点。例如上面例子中,IMGP是兄弟。
  • 文本节点: 包含文字串的节点。

4.浏览器

截屏2022-07-29 18.15.15.png

 Rendering Engine: 用于将 HTML+CSS 转换为可视化图像。

 Javascript Interpreter:用于执行 JavaScript 代码。

4.1 浏览器解析 HTML 文件的顺序

当浏览器向服务器发送请求获取 HTML 文件时,HTML 文件通常包含 <link>和 <script> 元素,这些元素分别指向了外部的 CSS 样式表文件和 JavaScript 脚本文件。

  • 浏览器首先解析 HTML 文件,并从中识别出所有的 <link> 和 <script> 元素,获取它们指向的外部文件的链接。
  • 继续解析 HTML 文件的同时,浏览器根据外部文件的链接向服务器发送请求,获取并解析 CSS 文件和 JavaScript 脚本文件。
  • 接着浏览器会给解析后的 HTML 文件生成一个 DOM 树(存储在内存中),给解析后的 CSS 文件生成一个 CSSOM 树(也被存储在内存中),并且会编译、执行解析后的 JavaScript 脚本文件。
  • 伴随着构建 DOM 树、应用 CSSOM 树的样式、以及执行 JavaScript 脚本文件,浏览器会逐步在屏幕上绘制出网页的界面。

Webview:

 应用程序中包含的浏览器引擎。WebView 允许程序员使用 HTML、JavaScript、CSS来编写应用程序的大部分内容。

截屏2022-07-29 18.20.30.png

5.TypeScript

 是 JavaScript 的类型超集,基本类型包括 number、string、boolean、null、undefined、bigint、symbol 等,扩展类型包括 any、never、unknown、void 等。

TypeScript 提供了 JavaScript 的所有功能,并在这些功能之上添加了一层:TypeScript 的类型系统。

例如,JS 提供了string 和 number类型,但在赋值时,JS 并不检查类型是否匹配。而 TS 则提供了此功能。

5.1 类型系统

5.1.1 类型推断

在许多情况下 TS 可以推断类型。

变量初始化时:

在创建变量并为其赋值时, TypeScript 根据赋值的类型自动推断变量类型。

// string
let str = "Hello World";

最佳通用类型:

当需要从几个表达式中推断类型时,会使用这些表达式的类型来推断出一个最合适的通用类型。推断出的通用类型来自于表达式中的候选类型。

若未找到最佳通用类型,则推断结果为联合数组类型。

// 被推断为 (Rhnio | Elephant | Snake)[]
let zoo = [new Rhino(), new Elephant(), new Snake()];

此种情况下,最好显式指定类型:

let zoo: Anilmal[] = [new Rhino(), new Elephant(), new Snake()];

上下文归类:

按上下文归类会发生在表达式的类型与所处的位置相关的情况下。

TS 类型检查器会通过 window.onmousedown 函数的类型来推断等号右侧函数表达式的类型,进而就能推断出参数 mouseEvent 的类型。

window.onmousedown = function(mouseEvent) { 
    console.log(mouseEvent.button); //<- Error 
};

若表达式中包含了明确的类型信息,则依据上下文推断出的类型信息将被忽略:

window.onmousedown = function(mouseEvent: any) { 
    console.log(mouseEvent.button); //<- No error
};

5.2 Interface 与 Type

Interface:

截屏2022-08-01 18.56.47.png

Type:

截屏2022-08-01 18.56.54.png

 Type 可以用于声明基本数据类型别名、联合类型、元组等,但是 Interface 不可以。

 Interface 可以进行联合声明,但是 Type 不可以。

5.3 Others

匿名定义:

截屏2022-08-01 19.00.43.png

泛型:

截屏2022-08-01 19.00.51.png

联合:

截屏2022-08-01 19.02.02.png

函数重载:

截屏2022-08-01 19.04.23.png

as:

截屏2022-08-01 19.06.11.png

typeof:

截屏2022-08-01 19.08.36.png

Partial<T>: 把属性变为可选属性。

截屏2022-08-01 19.11.03.png

Required<T>: 把属性变为必选属性。

截屏2022-08-01 19.12.29.png

Pick<T,keys>: 挑选类型的属性。

截屏2022-08-01 19.14.14.png

Omit<T,keys>: 使得类型包含类型 T 的除 keys 之外的属性。

截屏2022-08-01 19.15.38.png

ReturnType<T>: 返回 T 的类型。

Exclude<T,U>: 从 T 中去除 U 中包含的属性。

Extract<T,U>: 获取 T 与 U 的交集。

tsconfig.json:

  • files - 设置要编译的文件的名称;
  • include - 设置需要进行编译的文件,支持路径模式匹配;
  • exclude - 设置无需进行编译的文件,支持路径模式匹配;
  • compilerOptions - 设置与编译流程相关的选项。
截屏2022-08-01 19.20.00.png

6.网络请求与网页数据存储

6.1 网络请求

过程:

  • 进行 DNS 查询,将域名转换为 IP 地址。
  • 建立连接。
  • 通过 TLS handshake 获取加密数据所用的公钥。
  • 发送请求,加载内容。

同源策略: 同源是指两个网页的协议、整个域名、端口号必须相同。

 为了保证数据安全,两个非同源的网页之间不能进行如下交互:

  • 不能读取 Cookie、LocalStorage、IndexDB。
  • 不能获取 DOM。
  • 不能发送 AJAX 请求。

CORS(Cross-origin resource sharing): 是一种基于 HTTP head 的机制,该机制通过允许服务器标示除了它自己以外的其它 origin(域,协议和端口),使得浏览器允许这些 origin 访问加载自己的资源。浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch)使用 CORS,以降低跨源 HTTP 请求所带来的风险。

 浏览器自身可以获取非同源网站的资源,但是不能在代码中操作非同源网站的资源。

WebSocket:

 一种网络通信协议。在 HTTP 协议,通信只能由客户端发起,服务器无法主动向客户端推送信息。如果服务器有连续的状态变化,客户端要获知就非常麻烦。只能使用轮询:即每隔一段时间,就发出一个询问,了解服务器有没有新的变化。

 在 WebSocket 协议中,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。该协议具有如下特点:

  • 建立在 TCP 协议之上,服务器端的实现比较容易。

  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

  • 数据格式比较轻量,性能开销小,通信高效。

  • 可以发送文本,也可以发送二进制数据。

  • 没有同源限制,客户端可以与任意服务器通信。

  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

6.2 网页数据存储

 网页数据可以存储在用户浏览器中,这一方式提升了数据安全性,并且由于不需再向服务器端请求该部分数据,因此也提升了网页性能。主要存储方式有如下几种:SessionStorage、LocalStorage、Cookies、IndexDB、File System API。

7.ReactNative

  React Native 是基于 React 的跨平台开发框架,可用于 iOS、Android 平台。

 官方文档:英文版中文版

7.1 基础概念

RN 工程结构:

  • 包含 JS、Native 两部分;
  • JS 部分类似于 React 工程,可使用 web 工具进行开发;
  • Native 部分包含被集成在 app 中的 RN SDK 框架和其他依赖库。

RN output package:

  • 在 JS 部分,输出包包含 JS bundle file 或由 JsBundle 和相关资源转换而来的预编译二进制文件。
  • 在 Native 部分,输出包包含 .ipa 或 .apk 文件(包括 JsBundle 和相关资源)。

RN 在 app 中的作用:

  • RN 基于 React,使用 JS 编写。由于 RN 具有跨平台的能力,因此适合于编写 UI 和业务逻辑,这样便可以保证 UI 和业务逻辑在不同平台上一致。
  • 推荐使用 Native 端编程语言编写与设备相关的功能以及基础框架比如网络服务。因为这样程序会更加高效,并且可以更容易使用设备本身提供的接口。

RN App Architecture:

截屏2022-08-07 20.30.52.png
  • RN 代码都运行在 JSContext(JS引擎的一部分)容器中,跟 Native 代码是隔离的。JSContext理论上可以有多个。
  • JSContext 里包含了所有的 RN 代码,包括页面和数据,每个 RN 页面又是通过一个对应的 Native 页面承载出来,也就是 Native PageA,Native PageB。 NativePageA、NaitivePageB 实际上可以直接与外部的 Native 模块通信,此处为了方便理解,便都包在JSContext里了。
  • RN 页面之间可以直接通过 global data 通信,但是在JSContext隔离的情况下,就只能借助于 Native。
  • RN 跟 Native 之间的通信主要是通过 RN 框架的两个 bridge 以及 Native Modules 进行。

RN Bridge:

  • 是 JS 和 Native 端的交流层,通过它,JS 和 Native 端可以互相发送消息。
  • 也是 RN Bundle 的容器。

RN 页面的渲染: 在 RN Bridge 和 yoga 的辅助下,JS 组件可以直接被转换为 Native 组件和 Native layout,而后被转换为 layer,最后由渲染引擎进行渲染。

JS 引擎:

  • 是 RN Bridge 的核心组件,它可以将 JS 代码编译为原生代码并且运行。
  • 为其他语言提供了与 JS 进行交流的 API。RN 通过此特性构建了 RN Bridge。
  • 大多数 JS 引擎是使用 C 语言编写的,因为 C 语言具有跨平台的能力。
  • 在 iOS 上使用名为 JavascriptCore 的 JS 引擎。在 Android 上通常使用 V8、Hermes 引擎。

RN 框架结构:

截屏2022-08-07 20.58.31.png

7.2 JS 和 Native 的交互

Native Modules: 是由 Native 端实现的 UI or logic modules. RN 预置了一些 Native Modules,例如 ScrollView、Text 等。也可以自定义实现相关 Native Modules。

在 Native Modules 中接收参数:

  • 可以使用 string、number、Object、 Array 类型作为参数类型。
  • 如果想回传结果给 JS,那么必须将 promise type 作为最后一个参数的类型。

Native 发送回复给 JS: 在 iOS 中,

  • if it is single-time response, we can use resolve block to send normal response or reject block to send abnormal response.
  • If it is multi-time response such as download progress,we can use event emitter to send response to JS.

在 JS 端如何调用 Native Modules 方法: 可以直接调用,或者通过 NativeModules[ModuleName][FunctionName] 来间接调用。

获取 Native 端的回复:

  • Usually native module function is asynchronized and we just get the response within a promise or use await before this calling to wait until the response return back. 
  • But it can also be synchronized when native declared the method as synchronized, and then we can use it as a normal function.

Native Event Emitter: 提供了一条 Native 给 JS 发送数据的渠道。一旦 JS 对象订阅了一个事件,那么当 Native 通过 Native Event Emitter 发送事件时,JS 便会收到数据。

7.3 路由和页面回调(Routing and Page Callback)

Router: 是一个控制页面跳转和随页面跳转携带的数据的模块。通常使用 URL 等路由协议来定位跳转目的地。

SPA and MPA:

  • 如果一个应用包含多个 RN 页面,但只包含一个 activity 或 ViewController,则称其为 SPA。
  • 如果一个应用包含多个页面,则称其为 MPA。但是如果没有重定向便刷新了数据,那么便称为 SPA。SPA 便于分享数据。

RN 中的路由:

  • 如果开发的 SPA 只有 RN 页面,则可以在不更改 Native 页面的情况下从一个 RN 页面跳转到另一个 RN 页面,并且数据在所有 RN 页面之间共享,因此可以直接获得数据,而不需要使用路由器来传递数据。
  • 如果同时使用 Native 页面和 RN 页面开发 MPA,则无法直接从 Native 页面或 RN 页面获取数据。此时需要使用路由器来传递数据。
  • 使用 Native navigator 来实现 RN 路由,因为 RN 页面是基于Native page container 的。

页面回调:

 在 Native 端,可以通过路由来传递回调函数以从下个页面获取数据。

 在 JS 端,获取数据的方式:

  • 将回调函数挂载到上个页面中可以由 Native 代码执行的全局对象上,然后在下个页面上调用它。
  • event 就像一封邮件,我们可以通过在 JS 代码中添加监听器来获取它,而 event emitter 是分发它的邮局。event emitter 用于两个 JS 页面之间,因此可以用于获取数据。
截屏2022-08-07 22.08.44.png 截屏2022-08-07 22.08.52.png

7.4 Build and Run

Build:

  • 在 RN 端,首先要通过 npm 或 yarn 集成 node modules。然后通过 React-Native 命令将其构建到 JSBundle 文件中。
  • 在 Native 端,首先通过 Cocoapods 或在 Gradle 来集成相关库。然后将 JSBundle 或预编译文件拷贝到工程中。最后构建工程便能得到 .ipa 文件或 .apk 文件。

Run:

  • .ipa 或 .apk 文件都可以直接安装到真机或模拟器上。
  • 对于 iOS,需要将设备的 UDID 添加到 Apple 开发者证书中,并且使用相关的描述文件来构建工程。否则构建出的 .ipa 文件只能在模拟器上运行。

7.5 UI Component and Style

UI Component:

截屏2022-08-07 22.22.52.png

Style:

截屏2022-08-07 22.23.04.png

8.React & Redux

8.1 JSX

 是 JS 的语法扩展,用于描述 UI,主要用于 React。JSX 可被视为 JS 对象,遇到 HTML 标签(以 < 开头)就用 HTML 规则解析。遇到代码块(以 { 开头)就用 JavaScript 规则解析。

  JSX 必须以单个闭合标签开始,如下图所示。

截屏2022-08-08 22.54.02.png

JSX 语法: ‘{表达式}’ 语法表示输出表达式的结果,用于渲染。if 语句以及 for 循环不是 JavaScript 表达式,不能直接将其作为表达式写在 {} 中。可以先将其值赋值给一个变量,而后再写在 {} 中。

  • 变量名:{ node }
  • 函数定义表达式:{ () => true  }
  • 属性访问表达式: { node.a }
  • 函数调用表达式:{ f() }
  • 算数表达式:{ 1 + 2 }
  • 关系表达式:{ a > b}
  • 逻辑表达式:{ a && b || c }
  • 三元运算表达式:{ a ? b : c}

JSX 安全: React DOM 在渲染所有输入内容之前,默认会进行转义,以防止XSS(跨站脚本攻击)。例如以下代码输出结果为:Hello <br/> World

截屏2022-08-08 23.02.52.png

 如果想要强制输出 HTML,推荐使用 dangerouslySetInnerHTML 属性。如下代码所示,输出结果如右图所示。

截屏2022-08-08 23.06.04.png

8.2 React 的元素

元素是构成 React 应用的最小砖块。组件是由元素构成的。

8.2.1 渲染元素

示例:

在 HTML 文件中:

<div id="root"></div>

将其称为“根” DOM 节点,因为该节点内的所有内容都将由 React DOM 管理。想要将一个 React 元素渲染到根 DOM 节点中,只需:

const root = ReactDOM.createRoot(
  // 获取 HTML 中定义的 div 节点
  document.getElementById('root')
);
const element = <h1>Hello, world</h1>;
root.render(element);

8.2.2 更新元素

React 元素是不可变对象。一旦被创建,就无法更改它的子元素或者属性。一个元素就像电影的单帧:它代表了某个特定时刻的 UI。

更新 UI 唯一的方式是创建一个全新的元素,而后重新渲染。

const root = ReactDOM.createRoot(
  // 获取 HTML 中定义的 div 节点
  document.getElementById('root')
);

function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  root.render(element);
}

setInterval(tick, 1000);

React DOM 会将元素和它的子元素与它们之前的状态进行比较,而后只进行必要的更新来使 DOM 达到预期的状态。

8.3 React 组件

 即封装起来的具有独立功能的UI部件。React 组件能像原生的 HTML 标签一样输出特定的界面元素,并且也能包含一些元素相关逻辑功能的代码。

8.3.1 函数组件与 class 组件

自定义组件的名称必须以大写字母开头,因为 React 会将以小写字母开头的组件视为原生 DOM 标签。例如,<div /> 代表 HTML 的 div 标签。

函数组件: 本质上就是 JS 函数。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>
}

class 组件: 使用 ES6 的 class 进行定义:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

8.3.2 Props

可以以用户自定义组件作为元素:

function Welcome(props) {  
  return <h1>Hello, {props.name}</h1>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
// 将 `{name: 'Sara'}` 作为 props 传给组件
const element = <Welcome name="Sara" />;
root.render(element);

当 React 元素为用户自定义组件时,它会将 JSX 所接收的属性(attributes)以及子组件(children)转换为单个对象传递给组件,这个对象被称为 “props”。

Props 是只读的:

所有 React 组件的 props 均不可被更改。类似于纯函数。

纯函数:不会更改入参的函数。

// 纯函数
function sum(a, b) {
  return a + b;
}

// 非纯函数
function withdraw(account, amount) {
  account.total -= amount;
}

8.3.3 组合组件

组件可以在其输出中引用其他组件,这就使得可用同一组件来抽象出任意层次的细节。

创建一个可以多次渲染 Welcome 组件的 App 组件:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="Sara" />      
      <Welcome name="Cahal" />      
      <Welcome name="Edite" />    
    </div>
  );
}

8.3.4 拆分组件

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

上述 Comment组件可被拆分为:

function Avatar(props) {
  return (
    <img className="Avatar"      
      src={props.user.avatarUrl}      
      alt={props.user.name}    
    />  
  );
}

function UserInfo(props) {
  return (
    <div className="UserInfo">      
      <Avatar user={props.user} />      
      <div className="UserInfo-name">        
        {props.user.name}      
      </div>    
    </div>  
  );
}

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo user={props.author} />      
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

8.4 State 与生命周期

8.4.1 State 用法示例

State 是私有的。多用于可变动的属性。

由于 React 中数据流是单向的,因此最好尽量将多个 State 统一放在同一个高层级的 Component 中,并通过 props 将值的更新传递给子组件。

如何确定哪个属性需要被设置为 state:

  • 如果属性是从 parent 通过 props 传递下来的,那它很可能不是 state;
  • 如果属性一直保持不变,那它很可能不是 state;
  • 如果可以根据 component 中其他的 state 或 prop 来计算出这个属性的值,那它一定不是 state。

Clock组件:

函数组件:

const root = ReactDOM.createRoot(document.getElementById('root'));

function Clock(props) {
  return (
    <div>      
    <h1>Hello, world!</h1>      
    <h2>It is {props.date.toLocaleTimeString()}.</h2>    
    </div>  
  );
}

function tick() {
  root.render(<Clock date={new Date()} />);
}

setInterval(tick, 1000);

class 组件:

class Clock extends React.Component {
  constructor(props) {
    super(props);
    // state 包含 date 变量
    this.state = {date: new Date()};
  }

  componentDidMount() {
    this.timerID = setInterval(
      () => this.tick(),
      1000
    );
  }

  componentWillUnmount() {
    clearInterval(this.timerID);
  }

  tick() {    
    this.setState({      
      date: new Date()    
    });  
  }
  
  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);

1)当 <Clock /> 被传给 root.render()的时候,React 会调用 Clock 组件的构造函数。在构造函数内部会用一个包含当前时间的对象来初始化 this.state

2)之后 React 会调用组件的 render() 方法,从而使得 React 确定该在页面上展示什么。然后 React 更新 DOM 来匹配 Clock 渲染的输出。

3)当 Clock 的输出被插入到 DOM 中后,React 就会调用 ComponentDidMount() 生命周期方法。在这个方法中,Clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法。

4)浏览器每秒都会调用一次 tick() 方法。 在这方法之中,Clock 组件会通过调用 setState() 来计划进行一次 UI 更新。得益于 setState() 的调用,React 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。 而后,React 会相应的更新 DOM。

5)一旦 Clock 组件从 DOM 中被移除,React 就会调用 componentWillUnmount() 生命周期方法,这样计时器就停止了。

8.4.2 生命周期

componentDidMount():在组件已经被渲染到 DOM 中后运行。

componentWillUnmount():当组件从 DOM 中移除时会被调用。

8.4.3 State 的注意事项

1)不能直接修改 State。

构造函数是唯一可以给 this.state 赋值的地方。在其他地方不能直接修改 state。可以用 setState 方法进行修改。

// Wrong
this.state.comment = 'Hello';

// Correct
this.setState({comment: 'Hello'});

2)State 的更新可能是异步的

this.props 和 this.state 可能会被异步更新,因此不要依赖他们的值来更新下一个状态。

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

可以让 setState() 接收一个函数而不是一个对象:

// state 为上一次更新之后的 state,props 为本次更新被应用的 props
// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

3)State 的更新会被合并

setState 会被合并成一次执行,第一次的更新会被第二次更新覆盖。

handleClick () {
    this.setState({
      val: this.state.val + 1
    })
    this.setState({
      val: this.state.val + 1
    })
  }

如果想基于当前的 state 来计算出新值,那么就不能给 setState 的参数传递对象,而应该传递一个函数。

handleClick () {
    this.setState((prevState, props) => {
        val: prevState.val + 1
      }
    })
    this.setState((prevState, props) => {
        val: prevState.val + 1
      }
    })
 }

4)数据流是单向的

数据只能从父组件流向子组件,不能反向流动。

组件可以把它的 state 作为 props 向下传递到子组件中,FormattedDate 组件会在其 props 中接收参数 date,但是组件本身无法知道它是来自于 Clock 的 state,或是 Clock 的 props,还是手动输入的:

<FormattedDate date={this.state.date} />

function FormattedDate(props) {
  return <h2>It is {props.date.toLocaleTimeString()}.</h2>;
}

8.5 事件处理

8.5.1 React 和 DOM 的区别

1)React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • React 事件的命名采用小驼峰式(camelCase),而非纯小写。
  • 使用 JSX 语法时,需要传入一个函数作为事件处理函数,而非字符串。

传统的 HTML:

<button onclick="activateLasers()">
  Activate Lasers
</button>

在 React 中略微不同:

<button onClick={activateLasers}>  
  Activate Lasers
</button>

2)在 React 中,不能通过 return false 的方式阻止默认行为。必须显式地使用 preventDefault

传统的 HTML 中阻止表单的默认提交行为:

<form onsubmit="console.log('You clicked submit.'); return false">
  <button type="submit">Submit</button>
</form>

React 中:

function Form() {
  // `e` 是一个合成事件。React 根据 W3C 规范来定义这些合成事件,所以不需要担心跨浏览器的兼容性问题
  function handleSubmit(e) {
    e.preventDefault();    
    console.log('You clicked submit.');
  }

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit">Submit</button>
    </form>
  );
}

8.5.2 声明事件处理函数

使用 React 时,不需要使用 addEventListener 为已创建的 DOM 元素添加监听器。事实上,只需在该元素初始渲染的时候添加监听器即可。

在 class 组件中,通常的做法是将事件处理函数声明为 class 中的方法。

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // 为了在回调中使用 `this`,绑定(bind)是必不可少的    
    this.handleClick = this.handleClick.bind(this);  
  }

  handleClick() {    
    this.setState(prevState => ({
      isToggleOn: !prevState.isToggleOn    
    }));  
  }
  
  render() {
    return (
      <button onClick={this.handleClick}>        
        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

在 JavaScript 中,class 的方法默认不会绑定this

如果忘记绑定 this.handleClick 并把它传入了 onClick,则当调用这个函数的时候 this 的值将为为 undefined

通常情况下,如果没有在方法后面添加 (),例如 onClick={this.handleClick},就应该为这个方法绑定 this

8.5.3 bind 的等价方法

使用 public class fields 语法:

class LoggingButton extends React.Component {
  // This syntax ensures `this` is bound within handleClick.  
  handleClick = () => {    
    console.log('this is:', this);  
  };  
  
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

在回调中使用箭头函数:

此种方式的问题在于每次渲染 LoggingButton 时都会创建不同的回调函数。

若该回调函数作为 props 被传入子组件,则这些组件可能会进行额外的重新渲染。因此建议在构造器中绑定或使用 public class fields 语法来避免此类性能问题。

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    return (  
      // 此语法确保 `handleClick` 内的 `this` 已被绑定   
      <button onClick={() => this.handleClick()}>        
        Click me
      </button>
    );
  }
}

8.5.4 向事件处理程序传递额外参数

若 id 是将被删除行的 ID,则以下两种方式都可以向事件处理函数传递参数:

<button onClick={(e) => this.deleteRow(id, e)}>
  Delete Row
</button>

<button onClick={this.deleteRow.bind(this, id)}>
  Delete Row
</button>

通过箭头函数的方式,事件对象 e 必须显式的进行传递。

通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

8.6 条件 render

使用条件语句来统一表示处于多种情况下的 element,当需要渲染元素时,React 会根据条件进行渲染。

以 state 作为条件:

class LoginControl extends React.Component {
  constructor(props) {
    super(props);
    this.handleLoginClick = this.handleLoginClick.bind(this);
    this.handleLogoutClick = this.handleLogoutClick.bind(this);
    this.state = {isLoggedIn: false};
  }

  handleLoginClick() {
    this.setState({isLoggedIn: true});
  }

  handleLogoutClick() {
    this.setState({isLoggedIn: false});
  }

  render() {
    const isLoggedIn = this.state.isLoggedIn;
    let button;
    if (isLoggedIn) {      
      button = <LogoutButton onClick={this.handleLogoutClick} />;    
    } else {      
      button = <LoginButton onClick={this.handleLoginClick} />;    
    }
    
    return (
      <div>
        {button}      
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<LoginControl />);

使用逻辑运算符 &&:

在 JavaScript 中,true && expression 总是返回 expression ,而 false && expression 总是返回 false

function Mailbox(props) {
  const unreadMessages = props.unreadMessages;
  return (
    <div>
      <h1>Hello!</h1>
      {unreadMessages.length > 0 &&        
        <h2>          
          You have {unreadMessages.length} unread messages.        
        </h2>      
      }    
    </div>
  );
}

const messages = ['React', 'Re: React', 'Re:Re: React'];

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Mailbox unreadMessages={messages} />);

不渲染组件:

在 component 中返回 null 并不会影响 component 的生命周期方法的调用。

function WarningBanner(props) {
  if (!props.warn) {    
    return null;  
  }
  
  return (
    <div className="warning">
      Warning!
    </div>
  );
}

8.7 列表与 key

render 多个 component:

map() 会将入参数组按照指定逻辑转换为新数组。

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>  
  <li>{number}</li>
);

<ul>{listItems}</ul>

在 component 内渲染列表:

当用数组渲染列表时,渲染之后的列表的每一项都必须有一个独一无二的 key.

但是如果使用两个数组渲染不同的列表,那么这两个列表的元素的 key 并不需要是全局唯一的。

React 默认使用数组元素索引作为 key,但是不推荐使用索引作为 key。

function NumberList(props) {
  const numbers = props.numbers;
  const listItems = numbers.map((number) =>
    // 使用数组元素索引作为 key
    <li key={index}>      
      {number}
    </li>
  );
  return (
    <ul>{listItems}</ul>
  );
}

const numbers = [1, 2, 3, 4, 5];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);

8.8 表单

8.8.1 Controlled Component

使用 state 作为组件的数据源,当 state 更新时,组件立即随之更改。

例如,在下述示例中,text显示的值永远是 this.state.value,从而使得 state 成为数据来源。由于在每一次输入字符时,handleChange 均会被执行,并更新 state,因此 value 会被同步更新。

textvalue 的值被设定为 undefinednull 时,输入可以被修改。

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {    
    this.setState({value: event.target.value});  
  }
  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      // 当 `text.value` 的值被设定为 `undefined` 或 `null` 时,输入可以被修改。
      <form onSubmit={this.handleSubmit}>        
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />           
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

8.8.2 多个输入源

class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isGoing: true,
      numberOfGuests: 2
    };

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;
    this.setState({
      [name]: value    
    });
  }

  render() {
    return (
      <form>
        <label>
          Is going:
          <input
            name="isGoing"            
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Number of guests:
          <input
            name="numberOfGuests"            
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}

8.9 组合与继承

在 React 中,不推荐使用继承。component 的构造函数可以接受任意的 prop 作为参数,包含 primitive value、React element、function 等。

如果想要在 component 之间复用非 UI 的功能,建议将其抽离到一个独立的 JavaScript 模块中。Component 可以 import 并使用模组的 function、object,或者是 class,而不需要执行继承操作。

8.10 React 组件生命周期

截屏2022-08-08 23.24.14.png
  • constructor(props):用于初始化的 state 赋值;为事件处理绑定实例。
  • componentDidMount:用于实例化网络请求;用于添加订阅;可在函数中直接调用 setState()。
  • componentDidUpdate(prevProps, prevState, snapshot):在更新后会被立即调用,首次渲染时不会执行此方法;组件更新后,可在此处对 DOM 进行操作;对更新前后的 props 进行比较,可以在此处进行网络请求;可以在此处调用 setState(),但必须被包裹在一个条件语句里,否则会导致死循环,因为调用后者时,会自动调用前者进行相应操作。
  • componentWillUnmount():会在组件卸载及销毁之前直接调用;用于执行必要的清理工作;此处不应调用 setState(),因为该组件将永远不会被重新渲染。组件实例卸载后,将永远不会再挂载它;用于取消之前添加的订阅。
  • shouldComponentUpdate(nextProps, nextState):根据返回的 boolean 值,决定组件在 State 或者Props 变化的时候,是否渲染;用于减少不必要的组件渲染,以优化性能;建议使用内置的 PureComponent 组件,而不是手动编写此函数。因为 PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。
  • static getDerivedStateFromProps(props, state):会在调用 render 方法之前调用,并且在初始挂载及后续更新时都会被调用。它应返回一个对象来更新 state,如果返回 null 则不更新任何内容;除非特别需要,否则不建议使用此方法。
  • getSnapshotBeforeUpdate():在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期的任何返回值将作为参数传递给 componentDidUpdate()。此方法应返回 snapshot 的值或 null。

8.11 React 组件通信

截屏2022-08-09 10.42.50.png

9. React Hooks

 在hooks出现之前,react中的函数组件通常只考虑负责UI的渲染,没有自身的状态没有业务逻辑代码,是一个纯函数。hooks为函数组件提供了状态,也支持在函数组件中进行数据获取、订阅事件、解绑事件等。

 Hook API: link.

useSate(): 初始化一个 state 根据顺序来记住当前 state 对应的值,所以不能在 if 语句中使用。

截屏2022-08-09 10.48.19.png

useEffect(): 用于网络请求、日志、DOM 读取等。仅当依赖项改变时,该函数被调用。此函数中可以监听数据变化,以作出相应处理,如更新 UI。

截屏2022-08-09 10.50.26.png

useMemo & useCallback 优化: 可以使用 useMemo 和 useCallback 对变量和函数进行缓存的优化,在依赖变量没有变化的情况下,可以不用重新计算,以达到节省计算量的目的。

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

截屏2022-08-09 10.54.33.png

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

截屏2022-08-09 10.56.26.png

10. Redux

 可以理解为全局数据状态管理工具(状态管理机),用来做组件通信等。

截屏2022-08-09 10.59.06.png

 主要由三部分组成:Store、Reducer、Action。

截屏2022-08-09 11.01.56.png

Store: Redux的核心是store,它由Redux提供的 createStore(reducer, defaultState) 这个方法生成,生成三个方法: getState()、dispatch()、subscrible()。

  • getState():存储的数据,状态树;
  • dispatch(action):分发 action,并返回一个 action,这是唯一能改变 Store 中数据的方式;
  • subscrible(listener):注册一个监听者,Store 发生变化的时候被调用。

reducer: reducer 是一个纯函数,它根据 previousState 和 action 计算出新的 state。

截屏2022-08-09 11.06.08.png

action: action 本质上是一个 JavaScript 对象,其中必须包含一个 type 字段来表示将要执行的动作,其他的字段都可以根据需求来自定义。

截屏2022-08-09 11.07.21.png

11. React Redux

 Redux 本身和 React 没有关系,只是数据处理中心,是 React-Redux 把二者联系在一起。React Redux 提供了两个方法:connect 和 Provider。

Connect: connect 连接 React 组件和 Redux store。connect 实际上是一个高阶函数,返回一个新的已与 Redux store 连接的组件类。

截屏2022-08-09 11.13.08.png
  • TodoList是 UI 组件。
  • VisibleTodoList 就是由 react-redux 通过 connect 方法自动生成的容器组件。
  • mapStateToProps:从 Redux 状态树中提取需要的部分作为 props 传递给当前的组件。
  • mapDispatchToProps:将需要绑定的响应事件(action)作为 props 传递到组件上。 截屏2022-08-09 11.14.56.png

Provider: Provider 实现 store 的全局访问,将 store 传给每个组件。

截屏2022-08-09 11.16.31.png

二、General

1.HTTPS

1.1 HTTP

 HTTP 是超文本传输协议,不是编程语言,也不是 HTML。

URL:

截屏2022-08-01 12.28.40.png

Message:

截屏2022-08-01 12.32.19.png

常用方法:

  • GET: 从指定的资源处请求数据。
  • HEAD: 向指定的资源请求头部信息,不包含 body 部分。
  • POST: 向指定的资源添加数据。
  • PUT: 在指定的资源处,修改数据。

状态码:

  • 1XX:信息,服务器已收到请求,需要请求者继续执行操作。
  • 2XX:成功,操作被成功接收并被处理。
  • 3XX:重定向,需要进一步的操作以完成请求。
  • 4XX:客户端错误。
  • 5XX:服务端错误。

HTTP 过程:

20210319102327503.png

1.2 HTTPS

HTTP 与 HTTPS:

截屏2022-08-14 10.26.20.png

HTTPS 过程:

20210319101014336.png
  • 首先客户端通过URL连接服务器的443端口。
  • 服务端收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端(此时属于非对称加密,服务端生成的公钥和私钥不是同一个密钥)。
  • 客户端验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题(验证数字证书)。
  • 如果证书没有问题,客户端会建立会话密钥,然后利用服务器传输的公钥将会话密钥加密,并传送给服务端。
  • 服务器利用自己的私钥解密出会话密钥(非对称加密)。
  • 服务器利用会话密钥加密与客户端之间的通信。
  • 客户端用之前生成的会话密钥解密服务端传过来的信息,于是获取了解密后的内容(对称加密,加密与解密的密钥相同)。

 对称加密算法:DES、3DES、AES、RC等。

 非对称加密算法:DH、RSA、EIGamal、ECC等。

 Hash 算法:MD5、SHA-1、SHA-2、SHA-3、HMAC、SM3等。

 在用加密算法加密数据之前,有时会使用 Hash 算法对数据进行处理。原因是加密算法难以加密过长的数据,而使用 Hash 算法可以缩短过长数据的长度。

1.3 HTTP 的发展

截屏2022-08-14 10.43.25.png

2.Software Architecture

 含义:根据工程的上下文定义目标系统的边界。并按照一定原则对目标系统进行分层、划分子模块,以方便团队协作。同时为各个部分建立了通信机制。

 良好的软件架构便利了系统的开发、部署、运行、维护。

2.1 设计原则

  • SRP(Single Responsibility Principle, 单一职责原则):一个模块仅负责一种行为。
  • OCP(Open Closed Principle, 开闭原则):软件实体如类、模块、函数等应该通过扩展来实现更新,而不是通过修改已有代码来实现变化。若必须进行更改,则应通过接口和抽象的机制来隔离此类变化。当需要修改时,不需对抽象层做修改,而只需增加新的具体类便可实现预期变化。
  • LSP(Liskov Substitution Principle, 里氏替换原则):所有引用基类的地方,应当可以使用子类进行替换,并且不影响程序的功能;子类可以扩展基类,但是不应当覆盖基类,例如子类不应该重写基类的方法。
  • ISP(Interface Segregation Principle, 接口隔离原则):不应该依赖不需要的接口。
  • DIP(Dependency Inversion Principle, 依赖反转原则):高层次的模块不应该依赖于低层次的模块(因为这种对于低层次组件的依赖限制了高层次组件被重用的可能),两者都应该依赖于抽象接口;抽象接口不应该依赖于具体实现,而具体实现则应该依赖于抽象接口。

2.2 软件组件

 组件是可以作为系统的一部分进行部署的最小实体。可以被简单的理解为 jar、aar、npm、pod files。

组件聚合原则: 组件包含的元素所要遵循的原则。

  • REP: Release/Reuse Equivalency Principle(重用发布等价原则),即组件不能简单地由类和模块的随机组成。这些类和模块必须有着共同的主题或目的。
  • CCP: Common Closure Principle(共同封闭原则),即将所有可能被修改的类集中到一起。
  • CRP: Common Reuse Principle(共同重用原则),即类很少单独被重用。更典型的情况是,可重用类与属于相同可重用抽象的其他类一起被使用。

组件耦合原则: 组件之间所要遵循的原则。

  • ADP: Acycle Dependencies Principle(无环依赖原则),即不允许组件之间存在循环依赖关系。因为若存在循环依赖,则改变其中一个文件,循环依赖的所有文件都要重新编译。
  • SDP: Stable Dependencies Principle(稳定依赖原则),即依赖关系必须指向更稳定的方向。不稳定性计算方法:Fan-out 出向依赖/(Fan-in入向依赖 + Fan-out出向依赖);但是并非所有的组件都被要求是稳定的。通常顶层组件是可变的,而底层组件应当是稳定的。
  • SAP: Stable Abstractions Principle(稳定抽象原则),建立了稳定性和抽象性之间的关系。一方面,稳定的组件也应该是抽象的,这样它的稳定性就不会阻止它被扩展。另一方面,不稳定的组件应该是具体的,因为这样它的不稳定性便允许它可以被更改。因此,如果一个组件是稳定的,它应该由接口和抽象类组成,这样它就可以被扩展。

2.3 设计模式

设计模式原则:

  • SRP、OCP、LSP、ISP、DIP;
  • LOD(迪米特法则):一个对象应该对其他对象保持尽量少的了解。
  • CARP(合成复用):尽量使用组合而非继承。因为若使用继承,则修改基类会影响到所有的子类。

设计模式:

  • 单例:保证⼀个类仅有⼀个实例,并提供⼀个访问它的全局访问点。
  • 工厂方法:定义⼀个创建对象的接⼝,让其⼦类⾃⼰决定实例化哪⼀个⼯⼚类,⼯⼚模式使其创建过程延迟到⼦类进⾏。
  • 抽象工厂:提供⼀个创建⼀系列相关或相互依赖对象的接⼝,⽽⽆需指定具体的类。
  • 建造者:将⼀个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
  • 原型:⽤原型实例指定创建对象的种类,并且通过拷⻉这些原型创建新的对象。
  • 适配器:将⼀个类的接⼝转换成客户希望的另外⼀个接⼝。适配器模式使得原本由于接⼝不兼容⽽不能⼀起⼯作的类可以⼀起⼯作。
  • 桥接:将抽象部分与实现部分分离,使它们都可以独⽴的变化。
  • 组合:将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得⽤户对单个对象和组合对象的使⽤具有⼀致性。
  • 装饰:动态地给⼀个对象添加⼀些额外的职责。就增加功能来说,装饰器模式相⽐⽣成⼦类更为灵活。
  • 外观:为⼦系统中的⼀组接⼝提供⼀个⼀致的界⾯,外观模式定义了⼀个⾼层接⼝,这个接⼝使得这⼀⼦系统更加容易使⽤。
  • 享元:运⽤共享技术有效地⽀持⼤量细粒度的对象。
  • 代理:为其他对象提供⼀种代理以控制对这个对象的访问。
  • 责任链:避免请求发送者与接收者耦合在⼀起,让多个对象都有可能接收请求,将这些对象连接成⼀条链,并且沿着这条链传递请求,直到有对象处理它为⽌。
  • 命令:将⼀个请求封装成⼀个对象,从⽽使您可以⽤不同的请求对客户进⾏参数化。
  • 解释器:提供了评估语言的语法或表达式的方式,属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。常被用在 SQL 解析、符号处理引擎等方面。
  • 迭代器:提供⼀种⽅法顺序访问⼀个聚合对象中各个元素, ⽽⼜⽆须暴露该对象的内部表示。
  • 中介者:⽤⼀个中介对象来封装⼀系列的对象交互,中介者使各对象不需要显式地相互引⽤,从⽽使其耦合松散,⽽且可以独⽴地改变它们之间的交互。
  • 备忘录:在不破坏封装性的前提下,捕获⼀个对象的内部状态,并在该对象之外保存这个状态。
  • 观察者:定义对象间的⼀种⼀对多的依赖关系,当⼀个对象的状态发⽣改变时,所有依赖于它的对象都得到通知并被⾃动更新。
  • 状态:允许对象在内部状态发⽣改变时改变它的⾏为,对象看起来好像修改了它的类。
  • 策略:定义⼀系列的算法,把它们⼀个个封装起来,并且使它们可相互替换。
  • 模板方法:定义⼀个操作中的算法的⻣架,⽽将⼀些步骤延迟到⼦类中。模板⽅法使得⼦类可以不改变⼀个算法的结构即可定义该算法的某些特定步骤。
  • 访问者:主要用于分离数据结构与数据操作。

2.4 软件架构风格

  • 单体架构(Monolithic):

    • 分层架构(Layered architecture):分为表现层(presentation,用户界面,负责视觉和用户互动)、业务层(business,实现业务逻辑)、持久层(persistence,提供数据,SQL 语句就放在这一层)、数据库层(database,保存数据)。MVX 框架模式便属于分层架构。
    • 管道架构(Pipeline architecture):分为事件队列(event queue,接收事件的入口)、分发器(event mediator,将不同的事件分发到不同的业务逻辑单元)、事件通道(event channel,分发器与处理器之间的联系渠道)、事件处理器(event processor,实现业务逻辑,处理完成后会发出事件,触发下一步操作)。
    • 微内核/插件化架构(Microkernel architecture):内核通常只包含系统运行的最小功能。插件则是互相独立的,插件之间的通信,应该减少到最低,避免出现互相依赖的问题。
  • 分布式架构(Distributed):

    • Service-based architecture:基于服务的架构。
    • Event-driven architecture:事件驱动架构。
    • Space-based architecture:基于空间的架构。
    • Service-oriented architecture:面向服务的架构。
    • Microservices architecture:微服务架构。

三、Android

1.开发环境

 1)安装开发环境 Android Studio(link).

 2)从 oracle 下载、安装 JDK。

 3)配置环境。

// 在 ~/.bash_profile
export ANDROID_HOME=/Users/lc/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/platform-tools
JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_77.jdk/Contents/Home/
PATH=$JAVA_HOME/bin:$PATH:.
CLASSPATH=$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:.
export JAVA_HOME
export PATH
export CLASSPATH

Android Studio:

截屏2022-08-02 16.18.03.png

开发者模式: 连续单击7次 setting 中的手机版本号,进入开发者模式。之后便可以开始调试。

工程目录:

截屏2022-08-02 16.21.30.png

Gradle: 运行在 JVM 上的开源、自动构建工具。

 Setting.gradle:

截屏2022-08-02 16.27.17.png

 build.gradle:

截屏2022-08-02 16.27.55.png

配置依赖:

截屏2022-08-02 16.30.01.png

Android Profiler:

截屏2022-08-02 16.32.16.png
  • CPU Profiler: 追踪运行时的性能问题。
  • Memory Profiler: 帮助追踪内存配置问题。
  • Network Profiler: 监控网络流量使用情况。
  • Energy Profiler:追踪电量使用情况。

2.Java

 通过 JVM 机制,Java 得以实现跨平台编程。

2.1 基本数据类型

  • int => Integer
  • float => Float
  • short => Short
  • char => Character
  • double => Double
  • byte => Byte
  • long => Long
  • bool => Boolean

2.2 Cache

 Short、Long、Byte、Integer、Boolean、Character 类型,当相应对象的值超出默认范围时,内部会创建一个新的对象来承载相应的值。

 例如,对于 Interger 对象,如果初始化一个 int 类型数字,则会先在 IntegerCache.cache (一个 Integer 类型的数组;IntegerCache 是 Integer 类中一个私有的静态类,负责 Integer 的缓存) 中查找。若找到了相应的对象,便会直接引用相应的对象。否则会创建一个新的 Integer 对象。通过此种机制,可以降低资源消耗,提升效率。

2.3 for-each 语句

截屏2022-08-02 17.40.37.png

2.4 OOP

  • 继承:不支持多继承。
  • 封装:

  访问权限:

截屏2022-08-02 17.44.13.png
  • 多态:静态多态(函数重载)、动态多态。

2.5 接口与抽象类

  • 接口只能包含定义与默认的方法体,不能有方法的具体实现;抽象类中可以包含方法的具体实现、方法定义。
  • 接口用于实现,抽象类用于扩展。一个类可以实现多个接口,但是只能继承于一个抽象类。因此可以通过接口实现多继承。
  • 接口的成员变量默认属性是 public、static、final,并且必须被赋初值、不能被修改。抽象类的成员变量可以在子类中被重定义、重赋值。

2.6 final

  • 用于类,则当前类不能被继承。
  • 用于 field,则必须被初始化,且初始化之后不能被修改。
  • 用于方法,则该方法不能被子类重写。

2.7 异常

  • 不要在 final 语句体中使用 return。因此若 try 或 catch 语句体中包含 return 语句,则在执行该语句之前会先执行 final 语句体。一旦 final 语句体中包含 return 语句,则会直接返回。从而不会再执行 try 或 catch 语句体中所包含的 return 语句。
  • 当错误发生时,使用 SLog 语句上传 log。通过该 log,可以快速定位问题。
  • 通过 Thread.setDefaultUncaughtExceptionhandler(UncaughtExceptionHandler) 语句处理未捕获的异常。

2.8 字符串

  • String:使用的存储空间是常量池,因此 String 对象不可被修改。对 String 对象的更新操作都会创建新的 String 对象,而后引用指向新的对象。
  • StringBuilder、StringBuffer 类型对象是可变的,因此常用于需要修改字符串的场合。但前者不是线程安全的,而后者是线程安全的。

四、iOS

1.iOS 开发的一些知识

 iOS => iPhone Operating System

 iOS 是基于 XNU 内核的类 Unix 系统。

 在以 .mm 为后缀的文件中,OC 可以与 C++ 混编。

 Swift 是 POP 语言,即面向协议编程语言。

 安装 Cocoapods 后,在 XX.xcodeproj 文件所在目录执行pod init命令,即可自动创建 Podfile 文件。

 若存在 Podfile.lock 文件,则此文件会始终覆盖同一目录下的 Podfile 文件。即执行

pod install/update命令时,只会拉取 Podfile.lock 文件中指定的库文件。

 iOS app 程序是单进程、多线程程序。每个 app 执行在一个进程上,当前 app 程序不能访问其他 app 所属进程的资源。

 可通过 instruments 的 Zombies 功能检测“僵尸对象(因引用计数为0而被释放的对象)”。使用此功能会导致内存占用上升。“僵尸对象”的内存已经被系统回收,虽然其数据可能依然在内存中,但该类对象已经是不稳定对象了,不可以再被访问或使用,其内存随时可能被别的对象申请。

 可通过使用 console(控制台)程序查看其他程序的更丰富的日志。

 在运行 App 时,可以点击下方调试窗口上的暂停按钮,以暂停当前进程。

2.React Native

 React 是一套可以用简洁语法来绘制 DOM 的框架。

 React Native 可以将 React 组件编译成原生的组件和插件,让前端开发者可以使用 JavaScript 编写原生的iOS 和 Android 移动应用。React Native 实际调用的是 Objective-C 代码,渲染用的依旧是原生组件。

 在 iOS 中,React Native 能够运行起来,全靠 Objective-C 和 JavaScript 的交互,如下图所示。 截屏2022-08-07 22.40.16.png

 绿色的是应用开发的部分。蓝色代表公用的跨平台的代码和工具引擎,一般不会改动蓝色部分代码。黄色代码代表平台相关的代码,做定制化的时候会添加修改代码。此部分不能跨平台,要针对平台写不同的代码。每个 bridge 都有对应的 js 文件,js 部分是可以共享的。如果要定制原生控件,就得写 bridge 部分。红色部分是系统平台相关。红色部分上面的虚线表示所有平台相关的东西都通过 bridge 隔离开来了。

3.RN 和 Native 之间的通信

 RN 和 Native 之间的通信主要有三个阶段: RN init、RN call Native、Native call RN。

截屏2022-08-07 22.50.36.png

 在上图中:

  • RCTBridge 与 RCTCxxBridge 属于 iOS 平台特有,前者是 RN 对业务层接口(图中其他类都属于内部类,业务层无感知),具体工作在其子类RCTCxxBridge中完成;
  • 整个 RN 的核心在跨平台的 C++ 层。NativeToJsBridge 是 Native to JS 的桥接,所有从 Native 到 JS 的调用都是从 NativeToJsBridge 中的接口发出去的。NativeToJsBridge 做的最重要的一件事就是线程管理——使所有的 JS 调用都在指定的线程上执行。
  • JSCExecutorFactory 具体工厂负责创建 JSCExecutor,默认情况下采用 JSCExecutor 实例来执行JS代码。JSExecutor执行JS代码的抽象类。JSCExecutor的构造函数做了一条非常重要的事情:在 JS Context 中设置了一个全局代理nativeModuleProxy,其最终指向 JSCExecutor 类的 getNativeModule 方法。
  • 在 RN 中少不了 JS 的支持,从上图可知,JS 与 Native 的通信发生在 JavaScript 与 C++ 间。
  • NativeModules:  native 方法映射到 JS 的映射表。

3.1 RN init

截屏2022-08-08 12.37.09.png

 在 RN 初始化阶段,首先 Native 端会遍历开发者自定义的 Native 模块与 RN 框架提供的 Native 模块,并将其注册到一张原生模块映射表中,然后,Native 端也会将需要调用的 JS 模块注册到一张 JS 模块映射表中,但原生端并没有实现 JS Module,因此只是有一份接口而已。

 紧接着 JS Core 会将两份映射表传入到 JS 端,在 JS 端,原生模块映射表会绑定到 RN 提供的NativeModule 上面,这样 JS 就可以通过 NativeModule 来调用 Native 提供的 API 了。对于JS模块映射表来说,JS 端会实现对应的 JS 方法,并注册进去。这样 Native 就可以调用 JS 提供的方法。并且 JS 可以以回调参数的形式来接受 Native 传来的数据。

截屏2022-08-08 12.41.41.png

 上述流程如下:

  • 创建 RCTRootView,设置窗口根控制器的 View,把 RN 的 View 添加到窗口上显示。
  • 创建 RCTBridge 来桥接对象,管理JS和OC交互。
  • 创建 RCTBatchedBridge,批量桥接对象,JS 和 OC 交互具体实现都在这个类中。
  • 执行 [RCTBatchedBridge loadSource],加载JS源码。
  • 执行[RCTBatchedBridge initModulesWithDispatchGroup],创建 OC 模块表。
  • 执行[RCTJSCExecutor injectJSONText],往 JS 中插入 OC 模块表。
  • 执行完 JS 代码,回调 OC,调用 OC 中的组件。
  • 完成UI渲染。

3.2 RN call Native

截屏2022-08-08 19.17.35.png
  • OC 创建了一个单独的线程,这个线程只用于执行 JS 代码,而且 JS 代码只会在这个线程中执行。
  • 在 RN 中,OC 和 JS 的交互都是通过传递 ModuleId、MethodId 和 Arguments 进行的。
  • OC 和 JS 两端都保存了一份配置表,里面标记了所有 OC 暴露给 JS 的模块和方法。这样,无论是哪一方调用哪一方的方法,实际上传递的数据只有 ModuleId、MethodId 和 Arguments 这三个元素,它们分别表示类、方法和方法参数。
截屏2022-08-08 22.08.30.png
  • JS 调用 OC 模块暴露出来的方法.
  • 把调用方法分解为 ModuleName、MethodName、arguments,再交给 MessageQueue 处理。
  • 把 JS 的 callback 函数缓存在 MessageQueue 的一个成员变量里面,同时生成一个CallbackID来代表callback;再通过保存在 MessageQueue 的模块配置表把 ModuleName、MethodName 转成 ModuleID、MethodID。
  • 把 ModuleID、MethodID、CallbackID 和其他参数传给 OC(JavaScriptCore)。
  • OC 接到消息,通过模块配置表拿到对应的模块和方法。
  • RCTModuleMethod 对 JS 传过来的参数进行处理。
  • OC 模块方法执行完,执行 block 回调。
  • 调用第6步中 RCTModuleMethod 生成的 block。
  • block 带着 CallbackID 和 block 传过来的参数去调用 JS 里的 MessageQueue方法invokeCallbackAndReturnFlushedQueue。
  • MessageQueue 通过 CallbackID 找到相应的 JS 的 callback 方法。
  • 调用 callback 方法,并把 OC 带过来的参数一起传过去完成回调。

3.3 Native call RN

截屏2022-08-08 22.14.26.png
  • Native 在初始化时创建的 JS 模块映射表中找到需要调用的 JSModule_1。
  • 然后通过 JSCore 来将被调用的模块名、方法名以及参数传递到 JS 端。
  • JS 在 JS 模块映射表中找到提前注册好的 JSModule_1,传入参数并执行。

JS、Native 相互调用流程如下图: 截屏2022-08-08 22.21.19.png