前言
前两篇文章, 讲的是 VNode 和组件在初始化的情况下的渲染过程。因为没有涉及和 OldVNode 的比较所以省略了很多源码中的细节。这次我们来说说, 当 setState 时组件是如何更新的, 这中间发生了什么。我们本期会重新回顾之前几期文章中, 介绍的函数比如 diff, diffChildren, diffElementNode 等。⛽️
写的不一定对,都是我自己个人的理解,还请多多包含。
src/component.js
我们在第二期文章中介绍到。在调用 setState 的时候会将组件的实例传入 enqueueRender 函数。enqueueRender 函数会将组件的实例的_dirty 属性设置为 true。并且将组件 push 到 q 队列中。紧接着, 将 process 函数作为参数调用 defer 的函数, process 函数中会清空 q 队列, 并执行 q 队列中每个组件的 forceUpdate 方法。而 defer 则会返回一个 Promise.resolve()。
???? 当然为了降低阅读的复杂度, 组件不是很复杂。请仔细看我每一行标注的注释哦
import {h, render, Component} from ‘preact’;
class Clock extends Component {
constructor() {
super();
this.state = {
time: Date.now();
}
}
getTime = () => {
this.setState({
time: Date.now();
})
}
render(props, state) {
let time = new Date(state.time).toLocaleTimeString()
return (
<div>
<button onClick=”getTime”> 获取时间 </button>
<h1>{time}</h1>
</div>
)
}
}
render(<Clock />, document.body);
forceUpdate
Component.prototype.forceUpdate = function(callback) {
// vnode 为组件实例上挂载的组件 VNode 节点
let vnode = this._vnode,
// dom 为组件 VNode 节点上挂载的,DOM 实例由 diff 算法生成的
dom = this._vnode._dom,
// parentDom 为实例上挂载的,组件挂载的节点
parentDom = this._parentDom;
if (parentDom) {
// force 将会控制组件的 shouldComponentUpdate 的生命是否被调用
// 当 force 为 true 时, shouldComponentUpdate 不应该被调用
const force = callback!==false;
let mounts = [];
// 返回更新后 diff
// 注意这里传入的 newVNode 和 oldVNode 都是 vnode
// 那么他们的区别在那里呢?我们如何区分这两个 VNode 呢?
// 我们可以看下 setState 方法, 我们在 setState 中将最新的 setState 挂载到了_nextState 属性中
dom = diff(
dom,
parentDom,
vnode,
vnode,
this._context,
parentDom.ownerSVGElement!==undefined,
null,
mounts,
this._ancestorComponent,
force
);
// 如果挂载节点已经改变了,将更新后的 dom, push 到新的组件中
if (dom!=null && dom.parentNode!==parentDom) {
parentDom.appendChild(dom);
}
}
};
src/diff/index.js
第一次调用 diff 的过程
除了上图外,我们还可以得知,如果是一个复杂的 VNode 树???? 结构,组件在更新的时候,会先从外向里的顺序执行 getDerivedStateFromProps, componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate, getSnapshotBeforeUpdate 的生命周期。再由内向外执行 componentDidUpdate 的生命周期。
初次挂载的时候也是同里, 向外向内执行 componentWillMount 等生命周期,然后再由内向外的执行 componentDidMount 的生命周期。
我们通过 diffElementNodes 也可以看出来,Dom 元素属性的更新是由内到外的顺序,进行更新的。
export function diff(
dom,
parentDom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent,
force
) {
let c, p, isNew = false, oldProps, oldState, snapshot,
newType = newVNode.type;
try {
outer: if (oldVNode.type===Fragment || newType===Fragment) {
// … 省略源码
}
else if (typeof newType===’function’) {
// _component 属性是在初始化渲染时, 挂载在 VNode 节点上的组件的实例
if (oldVNode._component) {
c = newVNode._component = oldVNode._component;
}
else {
// … 初次渲染的情况,省略源码
}
// 挂载新的 VNode 节点, 供下一次 setState 的 diff 使用
c._vnode = newVNode;
// s 为当前最新的组件的 state 状态
let s = c._nextState || c.state;
// 调用 getDerivedStateFromProps 生命周期
if (newType.getDerivedStateFromProps!=null) {
// 更新前组件的 state
oldState = assign({}, c.state);
if (s===c.state) {
s = c._nextState = assign({}, s);
}
// 通过 getDerivedStateFromProps 更新组件的 state
assign(s, newType.getDerivedStateFromProps(newVNode.props, s));
}
if (isNew) {
// … 如果是新组件的初次渲染
}
else {
// 执行 componentWillReceiveProps 生命周期, 并更新新 state
if (
newType.getDerivedStateFromProps==null &&
force==null &&
c.componentWillReceiveProps!=null
) {
c.componentWillReceiveProps(newVNode.props, cctx);
s = c._nextState || c.state;
}
// 执行 shouldComponentUpdate 生命周期, 如果返回 false 停止渲染(不在执行 diff 函数)
// ⚠️ 如果 force 参数是 true 则不会执行 shouldComponentUpdate 的生命周期
// setState 中时,forceUpdate 函数,force 始终传入的是 false, 所以会执行 shouldComponentUpdate 的函数
if (
!force &&
c.shouldComponentUpdate!=null &&
c.shouldComponentUpdate(newVNode.props, s, cctx) === false
) {
c.props = newVNode.props;
c.state = s;
// _dirty 设置为 false, 停止更新
c._dirty = false;
break outer;
}
// 执行 componentWillUpdate 的生命周期
if (c.componentWillUpdate!=null) {
c.componentWillUpdate(newVNode.props, s, cctx);
}
}
oldProps = c.props;
if (!oldState) {
oldState = c.state;
}
// 将组件的 props 和设置为最新的状态(_nextState 经过了一些生命周期函数的更新, 所以要重新赋予组件新的 state)
c.props = newVNode.props;
c.state = s;
// 之前的 VNode 节点
let prev = c._prevVNode;
// 返回最新的组件 render 后的 VNode 节点
let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context));
c._dirty = false;
// 执行 getSnapshotBeforeUpdate 生命周期
if (!isNew && c.getSnapshotBeforeUpdate!=null) {
snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
}
// 对比新旧 VNode 节点, 在下一次的 diff 函数中我们将进入 diffElementNodes 的分支语句
c.base = dom = diff(
dom,
parentDom,
vnode,
prev,
context,
isSvg,
excessDomChildren,
mounts,
c,
null
);
// 挂载_parentDom
c._parentDom = parentDom;
}
else {
// …
}
newVNode._dom = dom;
if (c!=null) {
// 执行 setState 的回调函数
while (p=c._renderCallbacks.pop()) {
p.call(c);
}
// 执行 componentDidUpdate 的生命周期
if (!isNew && oldProps!=null && c.componentDidUpdate!=null) {
c.componentDidUpdate(oldProps, oldState, snapshot);
}
}
}
catch (e) {
catchErrorInComponent(e, ancestorComponent);
}
return dom;
}
第二次调用 diff 的过程
在第一次调用 diff 的时候, 进入了 typeof newType===’function’ 的分支, 我们调用了组件的 render 函数, 返回的是类似如下的 VNode 结构。我们在第二次 diff 的时候, 将比较新旧组件返回的 VNode, 并对属性进行修改。完成对 DOM 的更新操作。
<div>
<button onClick=”getTime”> 获取时间 </button>
<h1>{time}</h1>
</div>
// 新的 VNode
{
type: ‘div’,
props: {
children: [
{
type: ‘button’,
props: {
onClick: function () {
// …
},
children: [
{
type: null,
text: ‘ 获取时间 ’
}
]
}
},
{
type: ‘h1’,
props: {
children: {
{
type: null,
text: 新时间
}
}
}
}
]
}
}
// 旧 VNode
{
type: ‘div’,
props: {
children: [
{
type: ‘button’,
props: {
onClick: function () {
// …
},
children: [
{
type: null,
text: ‘ 获取时间 ’
}
]
}
},
{
type: ‘h1′,
props: {
children: {
{
type: null,
text: 旧时间
}
}
}
}
]
}
}
export function diff(
dom,
parentDom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent,
force
) {
let c, p, isNew = false, oldProps, oldState, snapshot,
newType = newVNode.type;
try {
outer: if (oldVNode.type===Fragment || newType===Fragment) {
// … 省略部分源码
}
else if (typeof newType===’function’) {
// … 省略部分源码
}
else {
// 将新旧 VNode 带入到 diffElementNodes 中
dom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent);
}
newVNode._dom = dom;
}
catch (e) {
catchErrorInComponent(e, ancestorComponent);
}
return dom;
}
function diffElementNodes(
dom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent
) {
// 这里 d 就是之前挂载初次渲染的 dom
let d = dom;
// …
if (dom==null) {
// … 初次渲染时 dom 不能复用,需要创建 dom 的节点,我们已经创建里 Dom 所以可以复用
}
newVNode._dom = dom;
if (newVNode.type===null) {
// …
}
else {
if (newVNode!==oldVNode) {
// 旧的 props
let oldProps = oldVNode.props;
// 新的 props
let newProps = newVNode.props;
// 递归的比较每一个 VNode 子节点,这里比较 VNode 子节点,将会插入到目前的 dom 中,
// 我们在这里不深入到子 VNode 中,而是关注与 root 节点
// 当 diffChildren 递归的执行完成后内部的 Dom 已经完成了更新的过程,我们暂时不去关心内部。
diffChildren(
dom,
newVNode,
oldVNode,
context,
newVNode.type===’foreignObject’ ? false : isSvg,
excessDomChildren,
mounts,
ancestorComponent
);
// 更新完成后,我们将更新 root 层的 dom 的属性
diffProps(
dom,
newProps,
oldProps,
isSvg
);
}
}
return dom;
}
export function diffProps(dom, newProps, oldProps, isSvg) {
// 对于新 props 的更新策略,如果 key 是 value 或者 checked 使用原生 Dom 节点和 newProps 比较
// 如果不是这两个 key 使用 oldProps 和 newProps 比较
// 更新两者属性不相等的属性
for (let i in newProps) {
if (
i!==’children’ && i!==’key’ &&
(
!oldProps ||
((i===’value’ || i===’checked’) ? dom : oldProps)[i]!==newProps[i]
)
) {
setProperty(dom, i, newProps[i], oldProps[i], isSvg);
}
}
// 多于旧的 props 的更新策略,如果在 newProps 中不存在的属性,则会去删除这个属性
// setProperty 一些内部处理细节,这里就不做展开
for (let i in oldProps) {
if (
i!==’children’ &&
i!==’key’ &&
(!newProps || !(i in newProps))
) {
setProperty(dom, i, null, oldProps[i], isSvg);
}
}
}
结语
接下来我们可以参考 (抄????) 一些博客和 preact 的源码,实现属于自己的 React