一、函数式编程
函数式编程是一种编程范式,在 React 中应用有两个特点:
- 纯函数
- 不可变值
二、vdom 和 diff 算法
- vdom 是实现 vue 和 react 的重要基石
- diff 算法是 vdom 中最核心、最关键的部分
1、vdom
vdom - 用 JS 模拟 DOM 结构,计算出最小的变更,数据驱动视图,操作 DOM
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的,它利用了函数重载的思想,根据传入的参数个数或类型的不同,执行不同函数
// h 函数的重载
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData | null, children:
VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}, children: any, text: any, i: number;
// 处理参数,实现重载的机制
if (c !== undefined) {
// 处理三个参数的情况
// sel、data、children/text
if (b !== null) { data = b; }
if (is.array(c)) { children = c; }
// 如果 c 是字符串或者数字
else if (is.primitive(c)) { text = c; }
// 如果 c 是 VNode
else if (c && c.sel) { children = [c]; }
} else if (b !== undefined && b !== null) {
// 处理两个参数的情况
// 如果 b 是数组
if (is.array(b)) { children = b; }
// 如果 b 是字符串或者数字
else if (is.primitive(b)) { text = b; }
// 如果 b 是 VNode
else if (b && b.sel) { children = [b]; }
else { data = b; }
}
if (children !== undefined) {
// 处理 children 中的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果 child 是 string/number,创建文本节点
if (is.primitive(children[i])) children[i] = vnode(undefined,
undefined, undefined, children[i], undefined);
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
// 如果是 svg,添加命名空间
addNS(data, children, sel);
}
// 返回 VNode
return vnode(sel, data, children, text, undefined);
};
// 导出模块
export default h;
2.3 VNode
一个 VNode 就是一个虚拟节点,用来描述一个 DOM 元素
export interface VNode {
// 选择器
sel: string | undefined;
// 节点数据:属性/样式/事件等
data: VNodeData | undefined;
// 子节点,和 text 只能互斥
children: Array<VNode | string> | undefined;
// 记录 vnode 对应的真实 DOM
elm: Node | undefined;
// 节点中的内容,和 children 只能互斥
text: string | undefined;
// 优化用
key: Key | undefined;
}
export function vnode(sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
let key = data === undefined ? undefined : data.key;
return {sel, data, children, text, elm, key};
}
export default vnode;
2.4 patch 函数
patch
函数是 snabbdom
的核心,调用 init
会返回这个函数,用来做 dom
相关的更新
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
组件名,首字母必须大写
// 第一个参数,可能是组件,也可能是 html 的 tag
// 第二个参数是标签的属性
// 后面参数为子元素,可逐个添加,也可使用数组形式
React.createElement(tag | component, props, child1, child2, child3)
四、合成事件
React 中所有事件挂载到 document 上
- event 不是原生的,是 SyntheticEvent 合成事件对象
合成事件的意义:
- 更好的兼容性和跨平台
- 挂载到 document,可减少内存消耗,避免频繁解绑
- 方便事件的统一管理(如事务机制)
五、setState 和 batchUpdate
- setState 有时异步(普通使用),有时同步(setTimeout,DOM 事件)
- setState 有时合并(对象形式),有时不合并(函数形式)
setState 运行机制:
上图中判断是否处于 batch update 的机制是 React 内置的 isBatchingUpdates 变量是否为 false,仅在 React 可管理的入口存在这个机制,如:
- 生命周期(和它调用的函数)
- React 中注册的事件(和它调用的函数)
而 setTimeout 和 自定义 Dom 则不能触发这个机制,举例如下:
class App extends React.Component {
increse = () => {
// 开始:处于 batchUpdate
// isBatchingUpdates = true
this.setState({ count: this.state.count + 1 })
console.log('count:' this.state.count) // 异步的,拿不到最新值
// 结束
// isBatchingUpdates = false
}
increse = () => {
// 开始:处于 batchUpdate
// isBatchingUpdates = true
setTimeout(() => {
// 此时 isBatchingUpdates 是 false
this.setState({ count: this.state.count + 1 })
console.log('count:' this.state.count) // 可获取最新值
})
// 结束
// isBatchingUpdates = false
}
componentDidMount() {
// 开始:处于 batchUpdate
// isBatchingUpdates = true
document.body.addEventListener('click', () => {
// 此时 isBatchingUpdates 是 false
this.setState({ count: this.state.count + 1 })
console.log('count:' this.state.count) // 可获取最新值
})
// 结束
// isBatchingUpdates = false
}
}
这种在函数开始执行一些操作,结束后执行一些操作的方式叫事务机制(transaction)
六、组件渲染过程
1、渲染和更新过程
渲染过程
- 获取 props、state
- render() 生成 vNode
- patch(elem, vnode) 操作 DOM
更新过程
- setState(newState) —> dirtyComponents(可能有子组件)
- render() 生成 newVNode
- patch(vnode, newVNode) 操作 DOM
上述的 patch 被拆分为两个阶段
- reconciliation 阶段 - 执行 diff 算法,纯 JS 计算
- commit 阶段 - 将 diff 结果渲染 DOM
2、性能问题与 fiber
渲染可能存在性能问题,由于 JS 是单线程,并且和 DOM 渲染公用一个线程,当组件足够复杂,组件更新时计算和渲染压力都大,同时如再有 DOM 操作需求(动画、鼠标拖拽等),将造成卡顿
React 针对上述问题,有一个解决方案,即 fiber
- 将 reconciliation 阶段进行任务拆分(commit 无法拆分)
- DOM 需求渲染时暂停,空闲时恢复(可通过 window.requestIdleCallback 知悉是否需要渲染)