前言
前兩篇文章, 講的是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