preact源码分析(五)

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理