阅读笔记

225 阅读18分钟

1. 函数式编程(FP)


函数式编程是编程范式之一。常见的范式还有

  1. 面向过程编程
  2. 面向指令编程
  3. 面向对象编程
  4. 面向函数式编程

1.1 函数式编程的特点

是将输入转换成对应的输出,是对数据映射关系的抽象。

1.2 函数式编程的应用

1.2.1 高阶函数

以函数为参数,或返回的函数

1.2.2 闭包

一个函数能访问到另一个函数内的变量。

1.2.3 纯函数

相同的输入能拿到相同的输出。
纯函数的好处:

  1. 可对执行结果进行缓存,提高代码性能。
  2. 方便测试
  3. 在多线程环境下,可对共享内存数据任意执行。

1.2.4 柯里化

当函数有多个参数时,可对函数进行改造,只接收部分参数,返回一个函数等待接受剩余参数,并返回相应的结果。

1.3 总结

函数式编程是一种范式、一种思想、一种约定。他有着一定的优势,更高的可组合性,灵活性以及容错性。但是在实际应用中是很难用函数式去表达的,我们应该将其当做我们现有储备的一种补充,而并非最优解去看待。以往的开发过程,我们可能习惯了用变量存储和追踪程序的状态,不停的在一些节点打印语句来观察程序的过程,现代的 JavaScript 库已经开始尝试拥抱函数式编程的概念以获取这些优势来降低系统复杂度。统一存储管理数据,将程序的运行状态置于可预见状态里。React、Rxjs、Redux 等 js 库都是这一理念的最佳实践者。

2.浏览器渲染过程与性能优化


2.1浏览器多进程架构

  • 进程(process)CPU分配的最小单位
  • 线程(thread)CPU调度最小单位,建立在进程基础上的运行单位。 Chrome由多个进程组成,相互配合完成浏览器的整体功能,每个进程内有包含多个线程,协同工作。
  • 优点:
  1. 打开新的tab默认开启一个新的进程,单个tab崩溃不会影响整个浏览器。
  2. 三方插件崩溃也不会影响整个浏览器。
  3. 充分利用现代CPU多核的优势。
  4. 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性。

2.2 浏览器的主要进程和职责

2.2.1 Browser Process

负责各个页面的管理、创建。销毁其他进程。资源管理和下载。

2.2.2 Plugin Process

每种类型的插件对应一个进程,使用插件时才会创建。

2.2.3 GPU Process

最多只有一个,用于3D绘制等。

2.2.4 Renderer Process

内部是多线程的,负责页面渲染、脚本执行、事件处理。

2.3 Renderer Process内的线程

2.3.1 GUI渲染线程

  • 负责渲染浏览器界面,html和css的解析,构建dom树和RenderObject树,布局和绘制。
  • 当页面重绘或重排时该线程会被执行。
  • 与JS引擎线程互斥,当JS引擎线程执行时,GUI更新会被保存在一个队列中等到JS引擎空闲时被执行。

2.3.2 JS引擎线程

  • 解析JS,运行代码。
  • 等待任务队列中的任务加以处理,一个渲染进程中只会有一个JS引擎线程运行JS程序。
  • 由于GUI线程与JS线程互斥,如果JS执行时间过长会导致页面渲染阻塞。

2.3.3 事件触发线程

  • 当JS引擎运行代码块如settimeout或鼠标点击,异步请求等会将对应任务添加到时间线程。
  • 当对应事件符合条件被触发时,事件线程会将他们添加到处理队列的队尾等待js引擎线程处理。
  • 由于JS单线程,处理队列中的事件排队等待js引擎空闲后处理。

2.3.3 定时触发器线程

  • settimeout/setinterval所在线程
  • js引擎是单线程的,如果处于阻塞状态会影响计时的准确性,所以需要一个单独的线程计时触发。计时完毕后,添加到任务队列中,等待js引擎线程空闲后处理。
  • settimeout小于4ms的间隔算4ms.

2.3.4 异步请求线程

  • xhr连接后开的一个请求线程。
  • 检测到状态变更时,如果有回调函数,异步线程就产生状态变更事件,然后再将事件放入事件队列中由js引擎执行。

2.4 浏览器渲染的流程

  1. 解析html文件构建dom树,浏览器主进程下载css文件。
  2. 解析css形成树形结构,结合dom树合并成renderObject树。
  3. 布局renderObject树,计算元素尺寸位置。
  4. 绘制renderObject树,绘制页面像素信息。
  5. 浏览器进程将默认图层和复合图层交给GPU进程,GPU进程将各个进程合成,最后显示出页面。

2.5 为什么js要单线程

如果js以多线程的方式来操作ui dom,可能会出现UI冲突。两个线程同时操作dom需要浏览器裁决生效哪个线程的执行结果。虽然可以通过锁解决这个问题,但是会引入更大的复杂性。

2.6 为什么js阻塞页面加载

因为js可以操作dom,如果在修改元素时渲染界面,那么渲染前后获得的数据可能就不一致了。所以GUI渲染进程和JS引擎线程是互斥的。

2.7 css加载会阻塞dom吗

dom树和csssom树是并行构建的,所以css不会阻塞dom解析,但是rendertree依赖dom tree和cssom tree,所以等到资源都加载完后在会渲染页面。css加载会阻塞dom的渲染。css会在后面的js执行前加载完,所以css会阻塞后面js的执行。

2.8 DOMContentLoaded和onload的区别

  • DOMContentLoaded:DOM解析完成不包括样式表图片。当文件中没有js,浏览器解析完文档就能触发。如果有脚本,脚本需要CSSDOM构建完才能执行,脚本解析会阻塞dom解析。
  • onload事件触发时,页面上所有DOM、样式表、脚本、图片资源已经加载完毕。

2.9 什么是CPR,如何优化?

CPR关键渲染路径就是浏览器的渲染流程。 可从以下几个方面进行优化:

  1. 优化DOM
  • 删除不必要的代码注释和空格
  • 开启gZIP压缩文件
  • 利用http缓存
  1. 优化CSSOM
  • 减少css元素数量
  • 关注媒体查询类型(极大影响CPR性能) 3.js优化
  • 使用async defer,当脚本不修改DOM和CSSOM时推荐使用async

image.png

  • 预加载
  • DNS预解析
  • 把所有脚本都丢到 之前是最佳实践,因为对于旧浏览器来说这是唯一的优化选择,此法可保证非脚本的其他一切元素能够以最快的速度得到加载和解析。

3. js执行机制


3.1 js事件循环

  1. 同步任务进入主线程,异步任务进入EventTable并注册函数。
  2. 当指定事情完成时,EventTable会将这个函数移入Event Queue。
  3. 主线程内的任务执行完毕,会去Event Queue读取相应函数进入主线程执行;
  4. 会不断检查主线程是否为空,空的话就去Event Queue检查是否有等待执行的函数,这个过程不断循环。
let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束');
  1. ajax进入Event Table注册毁掉函数success
  2. 执行console.log('代码执行结束')
  3. success进入Event Queue
  4. 主线程从Event Queue读取success并执行

3.2 setTimeout/setInterval

setTimeout等待指定时间后,把事件加入事件队列,如果前面的任务耗时太久,需要等待完成。 setInterval循环执行,每隔一段时间进入EventQueue,前面的任务耗时太久,同样需要等待。

3.3 Promise/process.nextTick

  • macro-task:包括整体代码script,setTimeout,setInterval
  • micro-task: Promise,process.nextTick 不同的任务类型会进入不同的事件队列,执行所有微任务后会执行宏任务。

4. 垃圾回收机制


4.1 垃圾产生&为何回收

创建基本类型、对象、函数都会占用内存。 我们举个简单的例子

let test = {
  name: "isboyjc"
};
test = [1,2,3,4,5]

js引用数据栈中保存了一个地址,堆中保存了实际的数据,例子中重新赋值后对象的引用关系就没有了,变成了无用的对象。如果不回收就会一直占用内存。

4.2 垃圾回收策略

  • 可达性:可访问或者可用的值,存储在内存中.
  • 回收:定期找到不可达的对象释放内存。

4.2.1 标记清除算法

  1. 垃圾收集器运行时会给内存中的变量都加上一个标记,假设内存中的对象都是垃圾,全标记为0;
  2. 然后从各个跟对象开始遍历,把不是垃圾的标记为1;
  3. 清理所有标记为0的垃圾,销毁并回收他们的内存空间。
  4. 最后把所有内存中的对象标记为0,等待下一次回收。
  • 优点:实现简单
  • 缺点:清除之后剩余对象内存位置不变,导致空闲内存位置不连续,出现内存碎片。内存分配会有问题,需要遍历找到合适的内存块,分配速度慢。(可用标记整理算法解决)

4.2.2 引用计数算法

跟踪记录每个变量使用的次数

  1. 当声明一个变量并赋值为引用类型是,引用次数为1。
  2. 同一个值被赋值给另一个变量,引用次数加1
  3. 变量的值被其他值覆盖,引用次数减1
  4. 如果引用次数为0,回收空间 如下例
let a = new Object() 	// 此对象的引用计数为 1(a引用)
let b = a 		// 此对象的引用计数是 2(a,b引用)
a = null  		// 此对象的引用计数为 1(b引用)
b = null 	 	// 此对象的引用计数为 0(无引用)
...			// GC 回收此对象

  • 优点: 更清晰,标记为0的那一刻就立即被清理。标记清除需要每隔一段时间去进行,js运行过程中线程需要停止去执行一段时间的GC.
  • 缺点: 需要计数器,计数器占很大的位置。无法解决循环引用的问题。

4.3 V8对GC的优化

4.3.1 分代式垃圾回收

每次垃圾回收都需要遍历内存中的所有对象,有些存活时间长的对象不需要频繁清理,所以采用分代式垃圾回收,提高垃圾回收的效率。

4.3.1.1 新老生代

V8将堆内存分为新生代和老生代两个区域,新生代对象存活时间较短,内存小(1-8M),老生代的对象存活时间长,或常驻内存(经历新生代垃圾回收还存活下来的对象),容量比较大。对于新老生代两块区域的垃圾回收,V8采用了两个垃圾回收器来管理。

4.3.1.2 新生代垃圾回收

采用Scavenge算法中的Cheney

  1. Cheney将新生代堆内存一分为二,分为使用区和空闲区
  2. 新加入的对象会放入使用区,当使用区快被写满时就会进行一次垃圾回收操作。
  3. 新生代对使用区的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序。
  4. 清理掉非活动对象,把使用区变成空闲区,空闲区变为使用区。
  5. 当一个对象经过多次复制还存活时,会被移到老生代。当复制一个对象到空闲区时,空闲区占用空间超过25%,这个对象也会被移到老生代。

4.3.1.3 老生代垃圾回收

老生代的对象通常占用内存大或存活时间长,分区复制会非常耗时。老生代垃圾回收采用标记清除法。

  1. 从根元素开始遍历,遍历过程中能访达到的对象称为活动对象,反之为非活动对象。
  2. 清除非活动对象,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。

4.3.2 并行回收

全停顿:js是单线程的,进行垃圾回收时会阻塞js脚本的执行,需要等到回收完毕后再恢复脚本的执行。

image.png 新生代对象空间采用并行策略,在垃圾回收操作过程中,启用多个线程来进行垃圾清理操作。

4.3.3 增量标记与懒性清理

对于老生代来说,并行策略依然会消耗比较多的时间。为了减少全停顿时间,从全停顿标记切换到增量标记。

4.3.3.1 什么是增量

将一次GC的过程分成了很多小步,每执行完一小步就让应用逻辑执行一会儿。

image.png

4.3.3.2 三色标记法(GC暂停与恢复)

  • 白色:未标记的对象
  • 灰色:自身被标记,该对象的引用对象未被标记。
  • 黑色:自身和引用对象都被标记 可以直接通过内存中有没有灰色节点来判断标记是否完成,有灰色节点恢复时直接从灰色节点继续执行,没有灰色节点直接进入清理阶段。三色标记法不需要每次都扫描整个内存空间,配合增量回收进行暂停恢复,减少全停顿的时间。

4.3.3.3 写屏障(增量中修改引用)

一次GC分块暂停后,执行js时,把内存中的对象引用更改了,在增量中修改了引用 image.png 一旦有黑色对象引用白色对象,写屏障机制会强制把白色对象改为灰色,从而保证标记阶段可以正常标记。

4.3.3.4 懒性清理

增量标记对活动对象和非活动对象进行标记,真正的清理内存用的是惰性清理。 当增量标记完成之后,如果当前的内存空间足以完成js代码的运行,就可以将清理的过程稍微延迟,让js的脚本

4.3.3.5 增量标记与惰性清理的优缺

优点:主线程停顿时间大大减少 缺点:每个增量标记之间执行js,可能会使对象引用发生变化,需要写屏障来记录这些变化,降低应用的吞吐量。没有减少主线程的总暂停时间。

4.3.4 并发回收

辅助线程能在后台完成垃圾回收的操作,主线程也可以自由执行不会挂起。堆中的对象引用关系随时都有可能发生变化,这时辅助线程之前做的一些标记或者正在进行的标记就会要有所改变,所以它需要额外实现一些读写锁机制来控制这一点。

image.png

4.3.5 V8中GC的优化。

  • V8中垃圾回收主要基于分代式。
  • 新生代中并行回收可以很大的提升垃圾回收的效率。
  • 老生代中主要是用并发标记,主线程开始执行js时,辅助线程开始并发标记。标记完成之后再进行并行清理,清理的任务会采用增量的方式分批在各个js任务之间执行。

5. React Hooks


使用Hooks的好处

  1. 可读性更强,不用把代码拆分到各个生命周期,可以将功能代码聚合,方便维护。
  2. 组件树层级变浅,可以复用通过自定义hooks复用代码逻辑。

5.1 useState

// 直接更新
setState(newCount);

// 函数式更新,依赖旧的状态值
setState(prevCount => prevCount - 1);
复制代码
  1. hooks更新state是替换而不是合并,推荐使用多个state.
  2. 调用usestate更新函数传入当前state,React跳过子组件的渲染和effect的执行(Object.is() )

5.2 useEffect

  1. 清除操作 防止内存泄漏,清除函数会在组件卸载前执行。
useEffect(() => {
  const subscription = props.source.subscribe();
  return () => {
    // 清除订阅
    subscription.unsubscribe();
  };
});
复制代码
  1. useEffect后才会render.
  2. 第二个参数不传会不停的调用。

5.3 useContext

用来多层级传递数据,父传孙

  1. 使用React Context Api 创建Context;
import React from 'react';
const ThemeContext = React.createContext(0);
export default ThemeContext;
  1. context的值由距离最近的上层组件<Context.Provider> 的 value prop 决定,当Provider更新时,hooks也会重新渲染。
import React, { useState } from 'react';
import ThemeContext from './ThemeContext';
import ContextComponent1 from './ContextComponent1';

function ContextPage () {
  const [count, setCount] = useState(1);
  return (
    <div className="App">
      <ThemeContext.Provider value={count}>
        <ContextComponent1 />
      </ThemeContext.Provider>
      <button onClick={() => setCount(count + 1)}>
              Click me
      </button>
    </div>
  );
}

export default ContextPage;
  1. useContext(MyContext) 只是让你能够读取 context 的值以及订阅 context 的变化。你仍然需要在上层组件树中使用 <MyContext.Provider> 来为下层组件提供 context
// 孙组件,在孙组件中使用 Context 对象值
import React, { useContext } from 'react';
import ThemeContext from './ThemeContext';
function ContextComponent () {
  const value = useContext(ThemeContext);
  return (
    <div>useContext:{value}</div>
  );
}
export default ContextComponent;

5.4 useReducer

  1. useState内部就是用useReduser实现的,是useState的替代方案,它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
  2. 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
const initialState = 0;
function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {number: state.number + 1};
    case 'decrement':
      return {number: state.number - 1};
    default:
      throw new Error();
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter(){
    const [state, dispatch] = useReducer(reducer, initialState,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
          <button onClick={() => dispatch({type: 'decrement'})}>-</button>
        </>
    )
}

5.5 React.Memo

父组件重新渲染时,子组件也会跟这渲染,即使state Props未更新。可以使用Memo包一层解决。 但是只能解决:

  1. 父组件未传参数给子组件
  2. 父组件传简单类型的参数给子组件(string、number、boolean等)
  3. 传复杂类型时应该使用useCallBack,useMemo
// 子组件
const ChildComp = () => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

5.5 React.useMemo

父组件传给子组件复杂类型,父组件更新时子组件也会渲染,可以用useMemo包裹,当useMemo第二个参数有更改时,第一个参数才会返回一个新的对象。

import React, { memo, useMemo, useState } from 'react';

// 子组件
const ChildComp = (info:{info:{name: string, age: number}}) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  
  // 使用 useMemo 将对象属性包一层
  const info = useMemo(() => ({ name, age }), [name, age]);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <div onClick={() => { setCount(count => count + 1); }}>点击增加</div>
      <MemoChildComp info={info}/>
    </div>
  );
};

export default Parent;

5.6 React.useCallBack

将事件传给子组件,父组件更新时,子组件也会重新更新,可以将事件用useCallBack包裹事件。

import React, { memo, useCallback, useMemo, useState } from 'react';

// 子组件
const ChildComp = (props:any) => {
  console.log('ChildComp...');
  return (<div>ChildComp...</div>);
};

const MemoChildComp = memo(ChildComp);

// 父组件
const Parent = () => {
  const [count, setCount] = useState(0);
  const [name] = useState('jack');
  const [age] = useState(11);
  const info = useMemo(() => ({ name, age }), [name, age]);
  const changeName = useCallback(() => {
    console.log('输出名称...');
  }, []);

  return (
    <div className="App">
      <div>hello world {count}</div>
      <div onClick={() => { setCount(count => count + 1); }}>点击增加</div>
      <MemoChildComp info={info} changeName={changeName}/>
    </div>
  );
};

export default Parent;

5.7、useRef

  1. 指向dom元素
import React, { useRef, useEffect } from 'react';
const Page1 = () => {
  const myRef = useRef<HTMLInputElement>(null);
  useEffect(() => {
    myRef?.current?.focus();
  });
  return (
    <div>
      <span>UseRef:</span>
      <input ref={myRef} type="text"/>
    </div>
  );
};

export default Page1;
  1. 存放变量
import React, { useRef, useEffect, useState } from 'react';
const Page1 = () => {
    const myRef2 = useRef(0);
    const [count, setCount] = useState(0)
    useEffect(()=>{
      myRef2.current = count;
    });
    function handleClick(){
      setTimeout(()=>{
        console.log(count); // 3
        console.log(myRef2.current); // 6
      },3000)
    }
    return (
    <div>
      <div onClick={()=> setCount(count+1)}>点击count</div>
      <div onClick={()=> handleClick()}>查看</div>
    </div>
    );
}

export default Page1;

5.8 useRefuseImperativeHandle

使用场景:通过 ref 获取到的是整个 dom 节点,通过 useImperativeHandle 可以控制只暴露一部分方法和属性,而不是整个 dom 节点。

5.9 useLayoutEffect

  • useLayoutEffect 和平常写的 Class 组件的 componentDidMount 和 componentDidUpdate 同时执行;
  • useEffect 会在本次更新完成后,也就是第 1 点的方法执行完成后,再开启一次任务调度,在下次任务调度中执行 useEffect;

6. http灵魂之问


6.1 HTTP报文结构

起始行 + 头部 + 空行(区分头部和实体) + 实体

头部

展示一下请求头和响应头在报文中的位置:

6.2 HTTP请求方法

http/1.1规定了以下请求方法(注意,都是大写):

  • GET: 通常用来获取资源
  • HEAD: 获取资源的元信息
  • POST: 提交数据,即上传数据
  • PUT: 修改数据
  • DELETE: 删除资源(几乎用不到)
  • CONNECT: 建立连接隧道,用于代理服务器
  • OPTIONS: 列出可对资源实行的请求方法,用来跨域请求
  • TRACE: 追踪请求-响应的传输路径 GET和POST的区别
  • 缓存角度:GET会被浏览器主动缓存,留下历史记录,POST默认不会
  • 编码角度:GET只能进行URL编码接收ASCII,POST无限制。
  • 参数角度:GET参数只能放在链接中,长度有限制。POST参数放在请求体中,长度无限制。
  • 幂等角度 GET请求操作多次返回结果一致。POST可能会不一致。
  • TCP角度:GET请求只会发送一个TCP,POST会发送两次(头返回100,然后发送body)。

6.3 URI

scheme 表示协议名,比如http, https, file等等。后面必须和://连在一起。

user:passwd@ 表示登录主机时的用户信息,不过很不安全,不推荐使用,也不常用。

host:port表示主机名和端口(http默认端口:80,https:443)。

path表示请求路径,标记资源所在位置。

query表示查询参数,为key=val这种形式,多个键值对之间用&隔开。

fragment表示 URI 所定位的资源内的一个锚点,浏览器可以根据这个锚点跳转到对应的位置。

uri只能使用ASCII编码,所有非 ASCII 码字符界定符转为十六进制字节值,然后在前面加个%

6.4 HTTP状态码

  • 1XX:协议处理中间状态,还需要后续处理
    101: http升级websocket服务器同意变更。
  • 2XX:成功状态,资源位置发生变动
    200: 成功,响应体有数据
    204: 成功,响应体无数据
    206: 成功,数据不完整()
  • 3XX:重定向相关
    301:永久重定向
    302:临时重定向
    304:命中协商缓存
  • 4XX:请求错误
    400:badrequest
    403:服务器禁止访问
    404:资源未找到
    405:请求方法错误
  • 5XX:服务端错误
    500:服务器错误
    501:客户端请求不支持
    502:服务器正常,访问出错
    503:服务器当前忙,无法接受响应

6.5 HTTP的特点与缺点

6.5.1 特点:

  1. 灵活可扩展:语义自由,传输形式多样性,可以传文本图片等任意数据。
  2. 可靠传输:http请求基于TCP/IP是可靠传输
  3. 请求-应答模式,一发一收,有来有回。
  4. 无状态,状态指通信过程中的上下文信息,每次http请求都是独立的,默认不需要保留状态信息。

6.5.2 缺点

  1. 无状态:在需要长连接时(多个请求用同一个tcp连接),需要保存大量上下文信息避免重复传输,无状态就是缺点。 如果只是为了获取数据,不需要保存上下文信息,无状态就是优点,节省开销。
  2. 明文传输:协议里的报文使用文本,没有使用二进制数据。(WIFI陷阱)
  3. 队头阻塞: http开启长链接共用一个TCP,同一时间只能处理一个请求,其他请求处于阻塞状态。

6.6 accept系列字段

6.6.1 数据格式

发送端:Content-Type 接收端可以规定指定接受类型:Accept 这两个字段的取值可以分为下面几类:

 //接收方
 Accept: text/html
 //发送方
 Content-Type: text/html
  • text: text/html, text/plain, text/css 等
  • image: image/gif, image/jpeg, image/png 等
  • audio/video: audio/mpeg, video/mp4 等
  • application: application/json, application/javascript, application/pdf, application/octet-stream

6.6.2 压缩方式

采取什么方式压缩体现在发送发,接收什么压缩方式体现在接收方。

//接收方
Accept-Encoding: g-zip
//发送方
Content-Encoding: g-zip
  • gzip: 当今最流行的压缩格式
  • deflate: 另外一种著名的压缩格式
  • br: 一种专门为 HTTP 发明的压缩算法

6.6.3 支持语言

 //接收方
 Accept-Language: zh-CN,zh,en
 //发送方
 Content-Language: zh-CN,zh,en

6.6.4 字符集

 //接收方,指定接收的字符集
 Accept-Charset: charset=utf-8
 //发送方
 Content-Type: text/html; charset=utf-8

6.7 HTTP如何传输定长与不定长的数据

  • 定长包:发送方会带上Content-Length指明包的长度。(设置长度短会截取,设置长度长会报错)
  • 不定长包:Transfer-Encoding: chunked,忽略Content-Length,基于长链接持续推送动态内容。

6.8 HTTP如何处理大文件的传输

范围请求,允许客户端请求资源的一部分。

  • 请求头:
// 单段数据
Range: bytes=0-9
// 多段数据
Range: bytes=0-9, 30-39

Range 的书写格式:

  • 0-499表示从开始到第 499 个字节。
  • 500- 表示从第 500 字节到文件终点。
  • -100表示文件的最后100个字节。

服务器收到后验证范围是否合法,越界返回416,否则读取相应片段返回206.

  • 响应头:
//单段数据
HTTP/1.1 206 Partial Content
Content-Length: 10
Accept-Ranges: bytes
Content-Range: bytes 0-9/100
i am xxxxx

Accept-Ranges表示支持范围请求。Content-Range字段,0-9表示请求的返回,100表示资源的总大小。

//多段数据
HTTP/1.1 206 Partial Content
Content-Type: multipart/byteranges; boundary=00000010101
Content-Length: 189
Connection: keep-alive
Accept-Ranges: bytes


--00000010101
Content-Type: text/plain
Content-Range: bytes 0-9/96

i am xxxxx
--00000010101
Content-Type: text/plain
Content-Range: bytes 20-29/96

eex jspy e
--00000010101--

这个时候出现了一个非常关键的字段Content-Type: multipart/byteranges;boundary=00000010101,它代表了信息量是这样的:

  • 请求一定是多段数据请求
  • 响应体中的分隔符是 00000010101

6.9 HTTP如何处理表单数据的提交

,体现在两种不同的Content-Type取值:

  • application/x-www-form-urlencoded
  • multipart/form-data在实际的场景中,对于图片等文件的上传,基本采用multipart/form-data而不用application/x-www-form-urlencoded,因为没有必要做 URL 编码,multipart/form-data 格式最大的特点在于:每一个表单元素都是独立的资源表述

6.10 HTTP1.0如何解决HTTP的队头阻塞问题。

  1. 并发连接:允许一个域名并发多个长链接,Chrome中最多可以并发六个。
  2. 域名分片:一个域名可以并发六个长链接,例如:cat.com可以分片成big.cat.com,samll.cat.com等多个二级域名,他们都指向同一台服务器,这样可以并发的长链接就更多了。

6.11 对cookie了解多少

http请求默认无状态,可以用cookie保存状态信息。像同一个域名发送请求会携带cookie,服务器会拿到cookie进行解析。 Cookie字段来对客户端写入Cookie。举例如下:

// 请求头
Cookie: a=xxx;b=xxx
// 响应头
Set-Cookie: a=xxx
set-Cookie: b=xxx

6.11.1 Cookie 属性

  1. 生命周期:
    • Expires:过期时间
    • Max-Age: 是一段时间间隔从浏览器收到报文开始计算。
  2. 作用域
    • Domain和path给cookie绑定了域名和路径。path:/表示域名下的所有路径都允许使用cookie.
  3. 安全相关
    • Secure:只能通过https传输cookie。
    • HttpOnly: cookie只能通过http传输,不能通过js访问。(预防XSS攻击)
    • Same-Site: 预防CSRF攻击,可设置三个值。
      1. strict: 浏览器完全禁止第三方请求携带cookie,cat.com的网站只能在cat.com的域名的请求中才会携带cookie,跳转其他域名就算有cookie也不会携带。
      2. Lax: 宽松一点,get方法提交表单,a标签发送get请求可以携带。
      3. None: 默认模式,请求会自动携带上cookie。

6.11.2 Cookie的缺点

  1. 容量缺陷:大小只有4kb
  2. 性能缺陷:cookie紧跟域名,不然域名下的地址是否需要cookie都会发送。可以通过设置domain和path解决。
  3. 安全缺陷:以纯文本在浏览器和服务器之前传输,容易被非法获取,在HttpOnly为false时,可以通过js document.cookie可以获取到。

6.12 HTTP代理

HTTP协议是请求响应模式,客户端发起请求,服务器响应。引入代理服务器后,代理服务器具有双重身份。

6.12.1 代理的作用

  1. 负载均衡:客户端的请求先会到达代理服务器,代理服务器可以拿到请求之后通过特定算法尽可能负载均衡的分发给源服务器。
  2. 保障安全:对数据过滤,非法IP限,利用心跳机制监测后台服务器,一发现故障机器就踢出集群。
  3. 缓存代理:代理服务器可以缓存数据,客户端可以直接从代理服务器拿到数据。

6.12.2 相关头部字段

  1. Via: 记录痕迹 现在中间有两台代理服务器,在客户端发送请求后会经历这样一个过程:
客户端 -> 代理1 -> 代理2 -> 源服务器

在源服务器收到请求后,会在请求头拿到这个字段:

Via: proxy_server1, proxy_server2

而源服务器响应时,最终在客户端会拿到这样的响应头:

Via: proxy_server2, proxy_server1
  1. X-Forwarded-For:为谁转发,记录请求方的IP(需要解析修改,影响数据性能。HTTPS中原始数据报文不可修稿,可以使用协议代理解决 // PROXY + TCP4/TCP6 + 请求方地址 + 接收方地址 + 请求端口 + 接收端口
    PROXY TCP4 0.0.0.1 0.0.0.2 1111 2222
    GET / HTTP/1.1)
  2. X-Real-IP:记录客户端的真实IP
    X-Forwarded-Host:客户单域名
    X-Forwarded-Proto:客户端协议

6.13 HTTP缓存以及代理缓存。

6.13.1 HTTP强缓存与协商缓存

首先通过Cache-Control验证强缓存是否有效

  1. 如果强缓存可用,直接使用
  2. 否则进入协商缓存,服务端通过请求头重的If-Modified-SinceIf-None-Match检查资源是否更新 3, 如果更新,直接200和资源
  3. 如果没有更新,返回304告诉浏览器直接从缓存中获取资源

6.13.2 代理缓存

让代理服务器接管一部分服务端HTTP缓存,可分为源服务端的控制和客户端的控制

6.13.3 源服务器端控制

  1. Cache-Control:源服务端响应头中,表示是否允许代理服务器缓存。private禁止,public 允许。
  2. must-revalidate的意思是客户端缓存过期就去源服务器获取,而proxy-revalidate则表示代理服务器的缓存过期后到源服务器获取。
  3. s-maxage: 限定缓存在代理服务器可以存放多久,

6.13.4 客户端器端控制

在客户端请求头重添加

  1. max-stalemax-fresh:对代理服务器上的缓存进行宽容或限制操作
max-stale: 5

表示客户端到代理服务器上拿缓存的时候,即使代理缓存过期了也不要紧,只要过期时间在5秒之内,还是可以从代理中获取的。

又比如:

min-fresh: 5

表示代理缓存需要一定的新鲜度,不要等到缓存刚好到期再拿,一定要在到期前 5 秒之前的时间拿,否则拿不到。 2. only-if-cached:表示只接受代理服务器上的缓存,代理缓存无效直接返回504

6.14 什么是跨域?浏览器如何拦截响应?如何解决?

浏览器遵循同源策略(协议,域名,端口相同),不同源发起请求会跨域,跨域请求会被浏览器拦截。在服务端处理完数据后,将响应返回,主进程检查到跨域,且没有cors(后面会详细说)响应头,将响应体全部丢掉,并不会发送给渲染进程。这就达到了拦截数据的目的。

6.14.1 CROS

跨域资源共享,需要浏览器和服务器共同支持

  • 简单请求: 请求方法为GET/POST/HEAD, 请求头的取值范围: Accept、Accept-Language、Content-Language、Content-Type(只限于三个值application/x-www-form-urlencodedmultipart/form-datatext/plain)
  • 请求步骤
  1. 在请求头中自动添加Origin表示源,拿到请求后服务器会对应添加Access-Control-Allow-Origin,如果Origin不在这个字段中,浏览器就会拦截。
  2. Access-Control-Allow-Credentials,表示是否允许客户端发送cookie,前端也需要向相应设置withCredentials.
  3. Access-Control-Expose-Headers是给 XMLHttpRequest 对象赋能,Access-Control-Expose-Headers: aaa那么在前端可以通过 XMLHttpRequest.getResponseHeader('aaa') 拿到 aaa 这个字段的值
  • 非简单请求:不同体现在预检请求和响应字段。预检请求的方法是OPTIONS,同时会加上Origin源地址和Host目标地址,这很简单。同时也会加上两个关键的字段:

    Access-Control-Request-Method, 列出 CORS 请求用到哪个HTTP方法
    Access-Control-Request-Headers,指定 CORS 请求将要加上什么请求头 预检请求的响应。如下面的格式:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0

其中有这样几个关键的响应头字段:

  • Access-Control-Allow-Origin: 表示可以允许请求的源,可以填具体的源名,也可以填*表示允许任意源请求。
  • Access-Control-Allow-Methods: 表示允许的请求方法列表。
  • Access-Control-Allow-Credentials: 简单请求中已经介绍。
  • Access-Control-Allow-Headers: 表示允许发送的请求头字段
  • Access-Control-Max-Age: 预检请求的有效期,在此期间,不用发出另外一条预检请求。

在预检请求的响应返回后,如果请求不满足响应头的条件,则触发XMLHttpRequestonerror方法,当然后面真正的CORS请求也不会发出去了。

CORS 请求的响应。绕了这么一大转,到了真正的 CORS 请求就容易多了,现在它和简单请求的情况是一样的。浏览器自动加上Origin字段,服务端响应头返回Access-Control-Allow-Origin。可以参考以上简单请求部分的内容。

6.14.2 JSONP

利用Script标签跨域。

const jsonp = ({ url, params, callbackName }) => {
  const generateURL = () => {
    let dataStr = '';
    for(let key in params) {
      dataStr += `${key}=${params[key]}&`;
    }
    dataStr += `callback=${callbackName}`;
    return `${url}?${dataStr}`;
  };
  return new Promise((resolve, reject) => {
    // 初始化回调函数名称
    callbackName = callbackName || Math.random().toString.replace(',', ''); 
    // 创建 script 元素并加入到当前文档中
    let scriptEle = document.createElement('script');
    scriptEle.src = generateURL();
    document.body.appendChild(scriptEle);
    // 绑定到 window 上,为了后面调用
    window[callbackName] = (data) => {
      resolve(data);
      // script 执行完了,成为无用元素,需要清除
      document.body.removeChild(scriptEle);
    }
  });
}

6.14.3 Nginx

反向代理服务器,正向代理帮助客户端访问客户端自己访问不到的服务器,然后将结果返回给客户端。

反向代理拿到客户端的请求,将请求转发给其他的服务器,主要的场景是维持服务器集群的负载均衡,换句话说,反向代理帮其它的服务器拿到请求,然后选择一个合适的服务器,将请求转交给它。然会跨域了,那这个时候让 Nginx 登场了,通过下面这个配置:

server {
  listen  80;
  server_name  client.com;
  location /api {
    proxy_pass server.com;
  }
}
复制代码

Nginx 相当于起了一个跳板机,这个跳板机的域名也是client.com,让客户端首先访问 client.com/api,这当然没有跨域,然后 Nginx 服务器作为反向代理,将请求转发给server.com,当响应返回时又将响应给到客户端,这就完成整个跨域请求的过程。

6.15 HTTP2 有哪些改进

6.15.1 头部压缩

请求体一般会通过Content-Encoding指定的字段来压缩,当GET请求时,请求报文都在请求头,有很大的压缩空间,HTTP/2采用了HPACK算法对头部进行压缩。

  • HPACK的亮点
  1. 在服务端和客户端之间建立哈希表,将用到的字段放到这张表中,在传输之前传过的值时只需要传入索引,根据索引去表中查找。废除了起始行,将起始行中的请求方法、URI、状态码转换成了头字段,不过这些字段都有一个":"前缀,用来和其它请求头区分开。
  2. 对于整数和字符串进行哈夫曼编码,将字符建立一张索引表,出现次数多的字符索引尽可能的短,传输的时候也是传输索引序列。

6.15.2 多路复用

并发连接域名分片多条 TCP 连接会竞争有限的带宽,让真正优先级高的请求不能优先处理。 HTTP/2 把报文全部换成二进制格式,全部传输01串,原来Headers + Body的报文格式如今被拆分成了一个个二进制的帧,用Headers帧存放头部字段,Data帧存放请求体数据。这些二进制帧不存在先后关系,因此也就不会排队等待,也就没有了 HTTP 的队头阻塞问题。通信双方都可以给对方发送二进制帧,这种二进制帧的双向传输的序列,也叫做(Stream)。HTTP/2 用来在一个 TCP 连接上来进行多个数据帧的通信,这就是多路复用的概念。

6.15.3 服务器推送

在 HTTP/2 当中,服务器已经不再是完全被动地接收请求,响应请求,它也能新建 stream 来给客户端发送消息,当 TCP 连接建立之后,比如浏览器请求一个 HTML 文件,服务器就可以在返回 HTML 的基础上,将 HTML 中引用到的其他资源文件一起返回给客户端,减少客户端的等待。

7. React 事件机制


7.1 必要的只是概念

7.1.1 JSX事件最终会变成什么

经过babel转成了React.createElement形式,最终转成了fiber对象,通过momeizedProps和pendingProps保存事件。

7.1.2什么是合成事件

  1. 在JSX上绑定的事件(handleClick,handleChange)并没有绑定到真实的dom上,而是绑定在document上统一管理。
  2. 真实dom上的click事件被react底层替换成了空函数。
  3. 在React上绑定的事件如onChange,在document处可能有多个事件与之对应。
  4. react采取按需绑定,发现了onClick事件,再去document上绑定click。 react合成事件:在react中绑定的onClick事件并不是原声事件,而是合成的。如click事件合成了onClick。比如blur,change,input,keydown,keyup合成了onChange。

react为什么采用合成事件:

  1. 绑定在documnet处统一方便管理。
  2. 提供全浏览器全一致性的事件系统,抹平不同浏览器之间的差异。

7.2 事件初始化-事件合成插件机制

7.2.1 必要的概念

  1. nameToPlugins 事件名到事件插件模块的映射。SimpleEventPlugin是处理各个事件函数的插件,每次点击都会找到SimpleEventPlugin对应的处理函数。
const namesToPlugins = {
    SimpleEventPlugin,
    EnterLeaveEventPlugin,
    ChangeEventPlugin,
    SelectEventPlugin,
    BeforeInputEventPlugin,
}
  1. plugin:上面注册的所有插件列表,初始化为空。const plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];

  2. registrationNameModules,记录了合成事件与对应事件插件之间的关系,在react中处理props中的事件,会根据事件名找到对应的事件插件,然后统一绑定到document处。

{
    onBlur: SimpleEventPlugin,
    onClick: SimpleEventPlugin,
    onClickCapture: SimpleEventPlugin,
    onChange: ChangeEventPlugin,
    onChangeCapture: ChangeEventPlugin,
    onMouseEnter: EnterLeaveEventPlugin,
    onMouseLeave: EnterLeaveEventPlugin,
    ...
}
  1. 事件插件:以simpleEventPlugin为例: 有两个属性,第一个extractEvents作为事件统一处理函数,第二个eventTypes是一个对象,对象保存了原生事件名和对应的配置项dispatchConfig的映射关系。v16React的事件是统一绑定在document上的,React用独特的事件名称比如onClickonClickCapture,来说明我们给绑定的函数到底是在冒泡事件阶段,还是捕获事件阶段执行。
const SimpleEventPlugin = {
    eventTypes:{ 
        'click':{ /* 处理点击事件  */
            phasedRegistrationNames:{
                bubbled: 'onClick',       // 对应的事件冒泡 - onClick 
                captured:'onClickCapture' //对应事件捕获阶段 - onClickCapture
            },
            dependencies: ['click'], //事件依赖
            ...
        },
        'blur':{ /* 处理失去焦点事件 */ },
        ...
    }
    extractEvents:function(topLevelType,targetInst,){ /* eventTypes 里面的事件对应的统一事件处理函数,接下来会重点讲到 */ }
}
  1. registrationNameDependencise:记录合成事件与原声事件的对应关系。
{
    onBlur: ['blur'],
    onClick: ['click'],
    onClickCapture: ['click'],
    onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
    onMouseEnter: ['mouseout', 'mouseover'],
    onMouseLeave: ['mouseout', 'mouseover'],
    ...
}

7.2.2 事件初始化

  1. injectEventPluginByName(),在react底层默认执行,形成nameToPlugins,然后执行recomputPluginOrdering()
/* 第一步:注册事件:  */
injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin,
});

export function injectEventPluginsByName(injectedNamesToPlugins){
     for (const pluginName in injectedNamesToPlugins) {
         namesToPlugins[pluginName] = injectedNamesToPlugins[pluginName]
     }
     recomputePluginOrdering()
}

  1. recomputePluginOrdering做了些什么? 形成Plugins数组,执行publishEventForPlugin
const eventPluginOrder = [ 'SimpleEventPlugin' , 'EnterLeaveEventPlugin','ChangeEventPlugin','SelectEventPlugin' , 'BeforeInputEventPlugin' ]
function recomputePluginOrdering(){
for (const pluginName in namesToPlugins) { 
    /* 找到对应的事件处理插件,比如 SimpleEventPlugin */ 
    const pluginModule = namesToPlugins[pluginName]; 
    const pluginIndex = eventPluginOrder.indexOf(pluginName); 
    /* 填充 plugins 数组 */ 
    plugins[pluginIndex] = pluginModule; 
    const publishedEvents = pluginModule.eventTypes; 
    for (const eventName in publishedEvents) { 
    // publishedEvents[eventName] -> eventConfig , pluginModule -> 事件插件 , eventName -> 事件名称  
        publishEventForPlugin(publishedEvents[eventName],pluginModule,eventName,)
    } 
    }
}
  1. publishEventForPlugin publishEventForPlugin 作用形成上述的 registrationNameModulesregistrationNameDependencies 对象中的映射关系。
/*
  dispatchConfig -> 原生事件对应配置项 { phasedRegistrationNames :{  冒泡 捕获  } ,   }
  pluginModule -> 事件插件 比如SimpleEventPlugin  
  eventName -> 原生事件名称。
*/
function publishEventForPlugin (dispatchConfig,pluginModule,eventName){
    eventNameDispatchConfigs[eventName] = dispatchConfig;
    /* 事件 */
    const phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
    if (phasedRegistrationNames) {
    for (const phaseName in phasedRegistrationNames) {
        if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
            // phasedRegistrationName React事件名 比如 onClick / onClickCapture
            const phasedRegistrationName = phasedRegistrationNames[phaseName];
            // 填充形成 registrationNameModules React 合成事件 -> React 处理事件插件映射关系
            registrationNameModules[phasedRegistrationName] = pluginModule;
            // 填充形成 registrationNameDependencies React 合成事件 -> 原生事件 映射关系
            registrationNameDependencies[phasedRegistrationName] = pluginModule.eventTypes[eventName].dependencies;
        }
    }
    return true;
    }
}

7.2.3 事件合成总结

到这儿初始化阶段已经完成,形成了上述的几个重要对象,构建初始化React合成事件与原生事件的对应关系,合成事件与对应的事件处理插件的对应关系。

7.3 绑定事件流程

<button onClick={ this.handerClick } className="button" >点击 React如何处理点击事件

7.3.1 diffProperties处理合成事件

  1. 在对应元素的fiber上以memoizedProps 和 pendingProps形成保存。 58E6A4AF-1902-42BC-9D11-B47234037E01.jpg
  2. React在调合子节点后,进入diff阶段,如果是dom类型的fiber,会用diffProperties单独处理。
function diffProperties(){
    /* 判断当前的 propKey 是不是 React合成事件 */
    if(registrationNameModules.hasOwnProperty(propKey)){
         /* 这里多个函数简化了,如果是合成事件, 传入成事件名称 onClick ,向document注册事件  */
         legacyListenToEvent(registrationName, document);
    }
}

diffProperties函数在 diff props 如果发现是合成事件(onClick) 就会调用legacyListenToEvent函数。注册事件监听器。

7.3.2 legacyListenToEvent()

  1. legacyListenToEvent()中找到合成事件对应的原生事件集合 比如 onClick -> ['click'] , onChange -> [blur , change , input , keydown , keyup],遍历依赖数组,绑定事件,所以只给元素绑定了onChange,但在document处会有多个事件监听器。 React 对于 click 等基础事件,会默认按照事件冒泡阶段的事件处理,不过这也不绝对的,比如一些事件的处理,有些特殊的事件是按照事件捕获处理的。
case TOP_SCROLL: {                                // scroll 事件
    legacyTrapCapturedEvent(TOP_SCROLL, mountAt); // legacyTrapCapturedEvent 事件捕获处理。
    break;
}
case TOP_FOCUS: // focus 事件
case TOP_BLUR:  // blur 事件
legacyTrapCapturedEvent(TOP_FOCUS, mountAt);
legacyTrapCapturedEvent(TOP_BLUR, mountAt);
break;
//  registrationName -> onClick 事件
//  mountAt -> document or container
function legacyListenToEvent(registrationName,mountAt){
   const dependencies = registrationNameDependencies[registrationName]; // 根据 onClick 获取  onClick 依赖的事件数组 [ 'click' ]。
    for (let i = 0; i < dependencies.length; i++) {
    const dependency = dependencies[i];
    //这个经过多个函数简化,如果是 click 基础事件,会走 legacyTrapBubbledEvent ,而且都是按照冒泡处理
     legacyTrapBubbledEvent(dependency, mountAt);
  }
}
  1. legactTrapBulledEvent将事件绑定到真正的dom并冒泡处理。
function legacyTrapBubbledEvent(topLevelType,element){
   addTrappedEventListener(element,topLevelType,PLUGIN_EVENT_SYSTEM,false)
}

7.3.3 绑定dispatchEvent,进行事件监听。

addTrappedEventListener首先绑定我们的事件统一处理函数 dispatchEvent,绑定几个默认参数,事件类型 topLevelType demo中的click ,还有绑定的容器doucment然后真正的事件绑定,添加事件监听器addEventListener 事件绑定阶段完毕。

/*
  targetContainer -> document
  topLevelType ->  click
  capture = false
*/
function addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture){
   const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer) 
   if(capture){
       // 事件捕获阶段处理函数。
   }else{
       /* TODO: 重要, 这里进行真正的事件绑定。*/
      targetContainer.addEventListener(topLevelType,listener,false) // document.addEventListener('click',listener,false)
   }
}

7.3.4 事件绑定过程总结

  1. 在React中diff DOM元素类型的fiber的props时,如果发现是React合成事件,系统会单独处理。
  2. 根据React合成事件类型,会找到对应原生时间的集合,大部分事件会按照冒泡逻辑处理,少数事件会按照捕获逻辑处理。(onScroll)
  3. 调用addTrappedEventListener,会进行真实的事件绑定,绑定在document上,dispatchEvent为同意的事件处理函数。
  4. 只有上述那几个特殊事件比如 scorll,focus,blur等是在事件捕获阶段发生的,其他的都是在事件冒泡阶段发生的,无论是onClick还是onClickCapture都是发生在冒泡阶段

7.4 事件触发-一次点击事件React底层会发生什么。

。。。

8. 虚拟DOM和DOM diff

8.1 虚拟DOM

用js按照dom的结构来实现的树形结构的对象。

  1. createElement(type,props,children)创建虚拟dom
  • type:元素的标签类型,如a,li,div
  • props:元素的属性如class,style,自定义属性。
  • children: 元素的子节点,参数以数组形式传入。

2.render渲染虚拟dom。

  • document.createElement创建真实dom
  • 设置属性setAttr(节点, 类型, 值);
  • 遍历子节点如果是虚拟dom就递归渲染,不是就代表文本节点,直接document.createTextNode(child)创建,通过el.appendChild(child);添加到对应元素内。
  • 最后将元素appendChild插入页面内

8.2 DOM-diff

给定两棵树,采用先序深度优先遍历的算法找到最少的转换步骤,通过对比两个虚拟dom,创建出补丁,描述改变内容,将这个补丁用来更新dom。

比较规则

  1. 新的DOM节点不存在{type:REMOVE,index}
  2. 文本节点的变化{type:'TEXT',text:变化的内容}
  3. 节点类型相同时查看属性属性是否相同,产生一个属性的补丁包,{type:ATTR,attr:{class:'aaa'}}
  4. 节点类型不相同直接采用替换模式{type:REPLACE,newNode}

walk方法都了什么

  1. 每个元素都有一个补丁,需要创建存放当前补丁的数组。
  2. 如果没有new节点,就把type为remove的类型push到补丁数组里
    if (!newNode) {
        current.push({ type: 'REMOVE', index });
    }
  1. 如果新老节点是文本的话,判断文本是否一致,在指定类型TEXT,将新节点放到当前补丁。
    else if (isString(oldNode) && isString(newNode)) {
        if (oldNode !== newNode) {
            current.push({ type: 'TEXT', text: newNode });
        }
    }
  1. 如果新老节点的类型相同比较他们的属性props。
    • 属性比较

      • diffAttr

        • 去比较新老Attr是否相同
        • 把newAttr的键值对赋给patch对象上并返回此对象
    • 然后如果有子节点的话就再比较一下子节点的不同,再调一次walk

      • diffChildren

      • 遍历oldChildren,然后递归调用walk再通过child和newChildren[index]去diff

    else if (oldNode.type === newNode.type) {
        // 比较属性是否有更改
        let attr = diffAttr(oldNode.props, newNode.props);
        if (Object.keys(attr).length > 0) {
            current.push({ type: 'ATTR', attr });
        }
        
        // 如果有子节点,遍历子节点
        diffChildren(oldNode.children, newNode.children, patches);
    }
  1. 上面都没发生的话,那就是表示节点被替换了,type为REPLACE,直接用newNode替换。
else {
current.push({ type: 'REPLACE', newNode});
}
  1. 当前补丁有值的话,将对应的补丁放到大补丁包里。

8.2 patch-补丁更新

  • 用一个变量来得到传递过来的所有补丁allPatches

  • patch方法接收两个参数(node, patches)

    • 在方法内部调用walk方法,给某个元素打上补丁
  • walk方法里获取所有的子节点

    • 给子节点也进行先序深度优先遍历,递归walk
    • 如果当前的补丁是存在的,那么就对其打补丁(doPatch)
  • doPatch打补丁方法会根据传递的patches进行遍历

    • 判断补丁的类型来进行不同的操作
    1. 属性ATTR for in 遍历attrs对象,如果当前值存在就设置属性setAttr,如果不存在就直接删除这个属性。
    2. 文字TEXT直接将补丁的text赋值给node节点的textContent.
    3. 替换REPLACE,新节点替换老节点,判断新节点是否是Element的实例,是的话调用render渲染新节点。不是的话就代表新节点是个文本节点,直接创建文本节点。之后在通过调用parentNode的replaceChild方法替换成新节点。
    4. 删除REMOVE,直接调用父级的removeChilde方法删除节点。

8.3 总结

  1. 用js对象模拟dom(虚拟dom)
  2. 将虚拟dom转成真实的dom并插入到页面中(render)
  3. 如果有事件发生修改了虚拟dom,比较两棵虚拟dom树的差异得到差异对象(diff)
  4. 将差异对象应用到真实的dom树上(patch)