React 原理 - 图1

一、函数式编程

函数式编程是一种编程范式,在 React 中应用有两个特点:

  • 纯函数
  • 不可变值

二、vdom 和 diff 算法

  • vdom 是实现 vue 和 react 的重要基石
  • diff 算法是 vdom 中最核心、最关键的部分

1、vdom

vdom - 用 JS 模拟 DOM 结构,计算出最小的变更,数据驱动视图,操作 DOM

vdom.png

2、 Snabbdom (Virtual DOM库)

详细可参考教程:snabbdom 源码阅读分析

2.1 Snabbdom的核心

  • 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
  • init() 设置模块,创建 patch()
  • patch() 比较新旧两个 VNode,它有两个参数,第一个参数可以是真实的 DOM 或者 VNode,第二个参数是新的 VNode
  • 把变化的内容更新到真实 DOM 树上

2.2 h 函数

snabbdom的h函数是用来创建vnode的,它利用了函数重载的思想,根据传入的参数个数或类型的不同,执行不同函数

  1. // h 函数的重载
  2. export function h(sel: string): VNode;
  3. export function h(sel: string, data: VNodeData | null): VNode;
  4. export function h(sel: string, children: VNodeChildren): VNode;
  5. export function h(sel: string, data: VNodeData | null, children:
  6. VNodeChildren): VNode;
  7. export function h(sel: any, b?: any, c?: any): VNode {
  8. var data: VNodeData = {}, children: any, text: any, i: number;
  9. // 处理参数,实现重载的机制
  10. if (c !== undefined) {
  11. // 处理三个参数的情况
  12. // sel、data、children/text
  13. if (b !== null) { data = b; }
  14. if (is.array(c)) { children = c; }
  15. // 如果 c 是字符串或者数字
  16. else if (is.primitive(c)) { text = c; }
  17. // 如果 c 是 VNode
  18. else if (c && c.sel) { children = [c]; }
  19. } else if (b !== undefined && b !== null) {
  20. // 处理两个参数的情况
  21. // 如果 b 是数组
  22. if (is.array(b)) { children = b; }
  23. // 如果 b 是字符串或者数字
  24. else if (is.primitive(b)) { text = b; }
  25. // 如果 b 是 VNode
  26. else if (b && b.sel) { children = [b]; }
  27. else { data = b; }
  28. }
  29. if (children !== undefined) {
  30. // 处理 children 中的原始值(string/number)
  31. for (i = 0; i < children.length; ++i) {
  32. // 如果 child 是 string/number,创建文本节点
  33. if (is.primitive(children[i])) children[i] = vnode(undefined,
  34. undefined, undefined, children[i], undefined);
  35. }
  36. }
  37. if (
  38. sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
  39. (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  40. ) {
  41. // 如果是 svg,添加命名空间
  42. addNS(data, children, sel);
  43. }
  44. // 返回 VNode
  45. return vnode(sel, data, children, text, undefined);
  46. };
  47. // 导出模块
  48. export default h;

2.3 VNode

一个 VNode 就是一个虚拟节点,用来描述一个 DOM 元素

  1. export interface VNode {
  2. // 选择器
  3. sel: string | undefined;
  4. // 节点数据:属性/样式/事件等
  5. data: VNodeData | undefined;
  6. // 子节点,和 text 只能互斥
  7. children: Array<VNode | string> | undefined;
  8. // 记录 vnode 对应的真实 DOM
  9. elm: Node | undefined;
  10. // 节点中的内容,和 children 只能互斥
  11. text: string | undefined;
  12. // 优化用
  13. key: Key | undefined;
  14. }
  15. export function vnode(sel: string | undefined,
  16. data: any | undefined,
  17. children: Array<VNode | string> | undefined,
  18. text: string | undefined,
  19. elm: Element | Text | undefined): VNode {
  20. let key = data === undefined ? undefined : data.key;
  21. return {sel, data, children, text, elm, key};
  22. }
  23. export default vnode;

2.4 patch 函数

patch 函数是 snabbdom 的核心,调用 init 会返回这个函数,用来做 dom 相关的更新

  1. function patch(oldVnode: VNode | Element, vnode: VNode): VNode {}

3、diff

diff 即对比前后 vdom 的变化

传统 diff 算法,即树 diff 的时间复杂度是O(n^3),新的 diff 算法的时间复杂度是 O(n),方法是:

  • 只比较同一层级,不跨级比较
  • tag 不相同,则直接删掉重建,不再深度比较
  • tag 和 key,两者都相同,则认为是相同节点,不再深度比较

三、JSX 本质

  • React.createElement 类似 Snabbdom 的 h 函数,返回 vNode
  • 组件名,首字母必须大写

    1. // 第一个参数,可能是组件,也可能是 html 的 tag
    2. // 第二个参数是标签的属性
    3. // 后面参数为子元素,可逐个添加,也可使用数组形式
    4. React.createElement(tag | component, props, child1, child2, child3)

    四、合成事件

  • React 中所有事件挂载到 document 上

  • event 不是原生的,是 SyntheticEvent 合成事件对象

合成事件.png
合成事件的意义:

  • 更好的兼容性和跨平台
  • 挂载到 document,可减少内存消耗,避免频繁解绑
  • 方便事件的统一管理(如事务机制)

**

五、setState 和 batchUpdate

  • setState 有时异步(普通使用),有时同步(setTimeout,DOM 事件)
  • setState 有时合并(对象形式),有时不合并(函数形式)

setState 运行机制:
setState机制.png
上图中判断是否处于 batch update 的机制是 React 内置的 isBatchingUpdates 变量是否为 false,仅在 React 可管理的入口存在这个机制,如:

  • 生命周期(和它调用的函数)
  • React 中注册的事件(和它调用的函数)

而 setTimeout 和 自定义 Dom 则不能触发这个机制,举例如下:

  1. class App extends React.Component {
  2. increse = () => {
  3. // 开始:处于 batchUpdate
  4. // isBatchingUpdates = true
  5. this.setState({ count: this.state.count + 1 })
  6. console.log('count:' this.state.count) // 异步的,拿不到最新值
  7. // 结束
  8. // isBatchingUpdates = false
  9. }
  10. increse = () => {
  11. // 开始:处于 batchUpdate
  12. // isBatchingUpdates = true
  13. setTimeout(() => {
  14. // 此时 isBatchingUpdates 是 false
  15. this.setState({ count: this.state.count + 1 })
  16. console.log('count:' this.state.count) // 可获取最新值
  17. })
  18. // 结束
  19. // isBatchingUpdates = false
  20. }
  21. componentDidMount() {
  22. // 开始:处于 batchUpdate
  23. // isBatchingUpdates = true
  24. document.body.addEventListener('click', () => {
  25. // 此时 isBatchingUpdates 是 false
  26. this.setState({ count: this.state.count + 1 })
  27. console.log('count:' this.state.count) // 可获取最新值
  28. })
  29. // 结束
  30. // isBatchingUpdates = false
  31. }
  32. }

这种在函数开始执行一些操作,结束后执行一些操作的方式叫事务机制(transaction)

六、组件渲染过程

1、渲染和更新过程

渲染过程

  1. 获取 props、state
  2. render() 生成 vNode
  3. patch(elem, vnode) 操作 DOM

更新过程

  1. setState(newState) —> dirtyComponents(可能有子组件)
  2. render() 生成 newVNode
  3. patch(vnode, newVNode) 操作 DOM

上述的 patch 被拆分为两个阶段

  1. reconciliation 阶段 - 执行 diff 算法,纯 JS 计算
  2. commit 阶段 - 将 diff 结果渲染 DOM

2、性能问题与 fiber

渲染可能存在性能问题,由于 JS 是单线程,并且和 DOM 渲染公用一个线程,当组件足够复杂,组件更新时计算和渲染压力都大,同时如再有 DOM 操作需求(动画、鼠标拖拽等),将造成卡顿

React 针对上述问题,有一个解决方案,即 fiber

  1. 将 reconciliation 阶段进行任务拆分(commit 无法拆分)
  2. DOM 需求渲染时暂停,空闲时恢复(可通过 window.requestIdleCallback 知悉是否需要渲染)