React(基于15
)中最值得称道的部分莫过于 Virtual DOM 与 diff 的完美结合,特别是其高效的 diff 算法,让用户可以无需顾忌性能问题而”任性自由”的刷新页面,让开发者也可以无需关心 Virtual DOM 背后的运作原理,因为 React diff 会帮助我们计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染。
什么是VirtualDOM?
virtual dom(虚拟DOM),也就是React.createElment函数返回的虚拟节点。它通过JS的Object对象模拟DOM中的节点。第一个参数是type,第二个是props,第3个或更多参数均是children(array或1个)1
2
3
4
5
6
7
8
9
10
11
12// ReactElement.createElement = function (type, config, children)
var element = {
// This tag allow us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE,
// Built-in properties that belong on the element
type: type, // string or function
key: key, // 标识虚拟dom与实例的关系
ref: ref, // 引用
props: props, // children等
// Record the component responsible for creating this element.
_owner: owner
};
VirtualDOM实例化
- 如果type是function或class
实例化为ReactCompositeComponent
- 如果是string
- 实例化
ReactDOMComponent
- _updateDOMProperties
- _createInitialChildren
- Y children(Array)
string/number会实例化为ReactDomTextComponent
- 按对应类型实例化
- N
string/number直接更新dom,不会实例化为ReactDomTextComponent
- Y children(Array)
- 实例化
diff比较的是什么?
- string/number比较值
- Object则比较type和key
diff基本原则
- 相同类型(
key和type
)nextRenderedElement(data)
用旧实例更新
(receiveComponent)- nextRenderedElement虽然根据
key
找到实例,并不保证找到实例的currentElement和nextRenderedElement一致
- 如果
nextRenderedElement与prevInstance(currentElement)对不上
,甚至DOM操作会更多
。
- 不同类型
- 创建新实例,无复用逻辑
diff结果操作类型
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
- INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
- MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
- REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
treeDiff
React 只会对相同颜色方框内的 DOM 节点进行比较
,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。1
2
3
4
5A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);
childrenDiff(内容)
- _reconcilerUpdateChildren(改之前是1,2,3,4,之后是2,3,1,5)
- flattenChildren(Array to Object
按定义顺序,仍然
有序)
- flattenChildren(Array to Object
1 | //#########before顺序 |
- updateChildren
- 遍历
nextChildren
更新每个child(参考key索引与id区别
)- 根据
nextChild
的key
去prevChildren
里找
是否有可用的元素 - 如果
找到可用的
,则用该组件(prev组件实例-壳)更新nextChildElement(data)
没找到可用实例
则创建新实例并挂载旧的有并且新的无(key)
则放入待删除数据里(4
)
- 根据
- 遍历
1 | prevChildren = { |
childrenDiff(顺序)
- 元素(1和2)交换逻辑(1放2后)
- parentNode.insertBefore(1,
2的后一个节点(nextSibling)
)
- parentNode.insertBefore(1,
- 删除node([1,2,3,4]->[2,3,4]),旧的元素顺序无须调整,只需执行删除操作
- 添加并且调整顺序[1,2,3,4]->[1,5,2,4,3],移动操作都是
after操作
参考lastPlacedNode
- 调整顺序[1,2,3,4]->[2,1,4,3]
diff源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43var updates = null;
var name; // `nextIndex` will increment for each child in `nextChildren`, but
// `lastIndex` will be the last index visited in `prevChildren`.
var nextIndex = 0;
var lastIndex = 0; // `nextMountIndex` will increment for each newly mounted child.
var nextMountIndex = 0;
var lastPlacedNode = null;
for (name in nextChildren) {
if (!nextChildren.hasOwnProperty(name)) {
continue;
}
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
if (prevChild === nextChild) {
updates = enqueue(updates, this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex));
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
if (prevChild) {
// Update `lastIndex` before `_mountIndex` gets unset by unmounting.
lastIndex = Math.max(prevChild._mountIndex, lastIndex); // The `removedNodes` loop below will actually remove the child.
} // The child must be instantiated before it's mounted.
updates = enqueue(updates, this._mountChildAtIndex(nextChild, mountImages[nextMountIndex], lastPlacedNode, nextIndex, transaction, context));
nextMountIndex++;
}
nextIndex++;
lastPlacedNode = ReactReconciler.getHostNode(nextChild);
} // Remove children that are no longer present.
for (name in removedNodes) {
if (removedNodes.hasOwnProperty(name)) {
updates = enqueue(updates, this._unmountChild(prevChildren[name], removedNodes[name]));
}
}