在实现剩下两个hook(componentWillReceiveProps和componentWillUnmount)之前,需要先实现domdiff
\
-
dom-diff核心是比较新旧虚拟DOM的差异,然后把差异同步到真实DOM节点上,主要有以下几种情况
-
1 老新都没有
-
2 老有新没有
-
3.老没有新有
-
4.老新都有
老新都没有的情况很简单,不再赘述
老的有,新的没有的情况会进入unMountVdom(oldVdom); 在这个方法里面我们可以执行卸载的钩子
export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
//老新都没有,什么都不需要做
if (!oldVdom && !newVdom) {
return null;
//如果老的有,新的没有 卸载老节点
} else if (oldVdom && !newVdom) {
unMountVdom(oldVdom);
//如果老的没有,新有的
}
在卸载的方法unMountVdom里面,需要拿到组件实例对象(类组件的话)上的卸载的钩子执行,所以需要建立vdom和类组件实例的引用关系:
function mountClassComponent(vdom) {
let { type: ClassComponent, props, ref } = vdom;
let classInstance = new ClassComponent(props);
//如果类组件的虚拟DOM有ref属性,那么就把类的实例赋给ref.current属性
if (ref) ref.current = classInstance;
if (classInstance.componentWillMount) {//组件将要挂载
classInstance.componentWillMount();
}
//把类组件的实例挂载到它对应的vdom上
vdom.classInstance = classInstance;
function unMountVdom(vdom) {
let { props, ref } = vdom;
let currentDOM = findDOM(vdom);//获取此虚拟DOM对应的真实DOM
//vdom可能是原生组件span 类组件 classComponent 也可能是函数组件Function
if (vdom.classInstance && vdom.classInstance.componentWillUnmount) {
vdom.classInstance.componentWillUnmount();
}
if (ref) {
ref.current = null;
}
//取消监听函数
Object.keys(props).forEach(propName => {
//我们现在用了合成事件
if (propName.slice(0, 2) === 'on') {
delete currentDOM._store;
}
});
//如果此虚拟DOM有子节点的话,递归全部删除
if (props.children) {
//得到儿子的数组
let children = Array.isArray(props.children) ? props.children : [props.children];
children.forEach(unMountVdom);
}
//把自己这个虚拟DOM对应的真实DOM从界面删除
currentDOM.parentNode.removeChild(currentDOM);
}
接下来的一种情况是老的没有,新的有,这种情况实际就是创建新的节点,同时也需要执行新节点创建的hook
export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
//老新都没有,什么都不需要做
if (!oldVdom && !newVdom) {
return null;
//如果老的有,新的没有 卸载老节点
} else if (oldVdom && !newVdom) {
unMountVdom(oldVdom);
//如果老的没有,新有的
} else if (!oldVdom && newVdom) {
let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
if (nextDOM) {
parentDOM.insertBefore(newDOM, nextDOM)
} else {
parentDOM.appendChild(newDOM);//添加到父节点上
}
if (newDOM._componentDidMount) newDOM._componentDidMount();
最后一种,老的新的都有的情况,在代码中又体现为新老的类型不同和新老的类型相同
先来看类型不同的情况:
export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
//老新都没有,什么都不需要做
if (!oldVdom && !newVdom) {
return null;
//如果老的有,新的没有 卸载老节点
} else if (oldVdom && !newVdom) {
unMountVdom(oldVdom);
//如果老的没有,新有的
} else if (!oldVdom && newVdom) {
let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
if (nextDOM) {
parentDOM.insertBefore(newDOM, nextDOM)
} else {
parentDOM.appendChild(newDOM);//添加到父节点上
}
if (newDOM._componentDidMount) newDOM._componentDidMount();
//如果老的有,新的也有,但是类型不同
} else if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
unMountVdom(oldVdom);//删除老的节点
let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
if (nextDOM) {
parentDOM.insertBefore(newDOM, nextDOM)
} else {
parentDOM.appendChild(newDOM);//添加到父节点上
}
if (newDOM._componentDidMount) newDOM._componentDidMount();
再来看类型相同的情况,这时就需要进入更详细的对比:
我们封了一个方法专门处理这种情况
export function compareTwoVdom(parentDOM, oldVdom, newVdom, nextDOM) {
//老新都没有,什么都不需要做
if (!oldVdom && !newVdom) {
return null;
//如果老的有,新的没有 卸载老节点
} else if (oldVdom && !newVdom) {
unMountVdom(oldVdom);
//如果老的没有,新有的
} else if (!oldVdom && newVdom) {
let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
if (nextDOM) {
parentDOM.insertBefore(newDOM, nextDOM)
} else {
parentDOM.appendChild(newDOM);//添加到父节点上
}
if (newDOM._componentDidMount) newDOM._componentDidMount();
//如果老的有,新的也有,但是类型不同
} else if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
unMountVdom(oldVdom);//删除老的节点
let newDOM = createDOM(newVdom);//根据新的虚拟DOm创建新的真实DOM
if (nextDOM) {
parentDOM.insertBefore(newDOM, nextDOM)
} else {
parentDOM.appendChild(newDOM);//添加到父节点上
}
if (newDOM._componentDidMount) newDOM._componentDidMount();
//如果老的有,新的也有,并且类型也一样,只需要更新就可以,就可以复用老的节点了
} else {//进入 深度对比子节点的流程
updateElement(oldVdom, newVdom);
}
}
updateElement中,如果新老节点都是纯文本节点,直接替换文本
function updateElement(oldVdom, newVdom) {
//如果新老节点都是纯文本节点的
if (oldVdom.type === REACT_TEXT) {
if (oldVdom.props.content !== newVdom.props.content) {
let currentDOM = newVdom.dom = findDOM(oldVdom);
currentDOM.textContent = newVdom.props.content;//更新文本节点的内容为新的文本内容
}
如果是原生DOM节点,需要更新节点的属性:
function updateElement(oldVdom, newVdom) {
//如果新老节点都是纯文本节点的
if (oldVdom.type === REACT_TEXT) {
if (oldVdom.props.content !== newVdom.props.content) {
let currentDOM = newVdom.dom = findDOM(oldVdom);
currentDOM.textContent = newVdom.props.content;//更新文本节点的内容为新的文本内容
}
//此节点是下原生组件 span div而且 类型一样,说明可以复用老的dom节点
} else if (typeof oldVdom.type === 'string') {
let currentDOM = newVdom.dom = findDOM(oldVdom);//获取老的真实DOM,准备复用
updateProps(currentDOM, oldVdom.props, newVdom.props);//直接用新的属性更新老的DOM节点即可
updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children);
function updateProps(dom, oldProps, newProps) {
for (let key in newProps) {
if (key === 'children') {//children
continue;//此处忽略子节点的处理
} else if (key === 'style') {//style
let styleObj = newProps[key];
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr];
}
} else if (key.startsWith('on')) {
//dom[key.toLocaleLowerCase()] = newProps[key];
addEvent(dom, key.toLocaleLowerCase(), newProps[key]);
} else {
dom[key] = newProps[key];//className
}
}
}
function updateChildren(parentDOM, oldVChildren, newVChildren) {
oldVChildren = Array.isArray(oldVChildren) ? oldVChildren : oldVChildren ? [oldVChildren] : [];
newVChildren = Array.isArray(newVChildren) ? newVChildren : newVChildren ? [newVChildren] : [];
let maxChildrenLength = Math.max(oldVChildren.length, newVChildren.length);
//oldChildren=3 newChildren=2 oldChildren=2 newChildren=3
for (let i = 0; i < maxChildrenLength; i++) {
//试图取出当前的节点的下一个,最近的弟弟真实DOM节点
let nextVdom = oldVChildren.find((item, index) => index > i && item && findDOM(item));
compareTwoVdom(parentDOM, oldVChildren[i], newVChildren[i], findDOM(nextVdom));
}
}
如果是组件节点
function updateElement(oldVdom, newVdom) {
//如果新老节点都是纯文本节点的
if (oldVdom.type === REACT_TEXT) {
if (oldVdom.props.content !== newVdom.props.content) {
let currentDOM = newVdom.dom = findDOM(oldVdom);
currentDOM.textContent = newVdom.props.content;//更新文本节点的内容为新的文本内容
}
//此节点是下原生组件 span div而且 类型一样,说明可以复用老的dom节点
} else if (typeof oldVdom.type === 'string') {
let currentDOM = newVdom.dom = findDOM(oldVdom);//获取老的真实DOM,准备复用
updateProps(currentDOM, oldVdom.props, newVdom.props);//直接用新的属性更新老的DOM节点即可
updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children);
} else if (typeof oldVdom.type === 'function') {
if (oldVdom.type.isReactComponent) {//类组件
updateClassComponent(oldVdom, newVdom);
} else {//函数组件
updateFunctionComponent(oldVdom, newVdom);
}
}
}
初次render调用堆栈:
render(vdom, parentDOM) ->
createDOM(vdom) ->
mountClassComponent(vdom)
vdom.classInstance = classInstance
renderVdom = classInstance.render()
vdom.oldRenderVdom = renderVdom
createDOM(renderVdom)
类组件通过事件触发setState再更新的调用堆栈:
dispatchEvent ->
updateQueue.isBatchingUpdate=true
eventHandler.call(target, syntheticEvent) ->
this.setState({...}) ->
this.updater.addState(partialState) ->
this.emitUpdate()
updateQueue.updaters.push(this)
updateQueue.isBatchingUpdate=false
updateQueue.batchUpdate() ->
updater.updateComponent() ->
shouldUpdate(classInstance, nextProps, this.getState()) ->
classInstance.props=nextProps
classInstance.state=nextState
classInstance.forceUpdate()
let newRenderVdom = this.render()
compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)
\
接着上面的分析,setState之后导致的重新渲染执行的compareTwoVdom,其参数oldRenderVdom, newRenderVdom通常情况下以真实DOM居多,所以从这个入口进到compareTwoVdom时,既有真实DOM对应的虚拟DOM的情况,也有类组件或函数组件对应的虚拟DOM的情况
遍历到普通DOM属性的情况
compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)
updateElement(oldVdom, newVdom)
updateProps(currentDOM, oldVdom.props, newVdom.props)
updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children)
for (let i=0; i<maxChildrenLength; i++) {
compareTwoVdom(parentDOM, oldVChildren[i], newVChildren[i], findDOM(nextVdom))
\
遍历到类组件的情况
compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)
updateElement(oldVdom, newVdom)
updateClassComponent(oldVdom, newVdom)
classInstance.updater.emitUpdate(newVdom.props)
属性更新导致的重新渲染通常是由于setState改变了props,这个时候通常都是遍历虚拟DOM树到类组件的时候,递归调用compareTwoVdom进入的:
compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)
updateElement(oldVdom, newVdom)
updateClassComponent(oldVdom, newVdom)
classInstance.updater.emitUpdate(newVdom.props)
this.updateComponent()
classInstance.forceUpdate()
compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom)
对于普通DOM对象,vdom对象上挂载着vdom对应的DOM对象
对于类组件,vdom对象上挂载着类组件的实例classInstance和调用render渲染出来的oldRenderVdom
父子组件更新顺序:
Counter 1.constructor
Counter 2.componentWillMount
Counter 3.render
ChildCounter 1.componentWillMount
ChildCounter 2.render
ChildCounter 3.componentDidMount
Counter 4.componentDidMount
react16去掉了willxxx的生命周期函数,添加了getDerivedStateFromProps、getSnapshotBeforeUpdate两个新的生命周期函数
组件创建、更新时都会走到getDerivedStateFromProps这个钩子中
class ChildCounter extends React.Component {
constructor(props) {
super(props);
this.state = { number: 0 };
}
static getDerivedStateFromProps(nextProps, prevState) {
const { number } = nextProps;
return { number: number * 2 };
}
class Component {
static isReactComponent = true //当子类继承父类的时候 ,父类的静态属性也是可以继承的
constructor(props) {
this.props = props;
this.state = {};
this.updater = new Updater(this);
}
setState(partialState) {
this.updater.addState(partialState);
}
//根据新的属性状态计算新的要渲染的虚拟DOM
forceUpdate() {
let oldRenderVdom = this.oldRenderVdom;//上一次类组件render方法计算得到的虚拟DOM
let oldDOM = findDOM(oldRenderVdom);//获取 oldRenderVdom对应的真实DOM
if(this.constructor.getDerivedStateFromProps){
let newState = this.constructor.getDerivedStateFromProps(this.props,this.state);
if(newState)
this.state = newState;
}
getDerivedStateFromProps主要用来代替之前的componentWillReceieProps
对于某些情况,例如向一个div中插入子节点,导致div高度增加,当高度增加到大于容器高度时,会出现滚动条,如果用户再滚动一段距离,随着子节点还在不断插入,上卷距离(scrollTop)不变,内容就会不断变化,所以我们需要手动修改上卷距离,就可以在getSnapshotBeforeUpdate这个hook中完成
// DOM更新前
getSnapshotBeforeUpdate() {//很关键的,我们获取当前rootNode的scrollHeight,传到componentDidUpdate 的参数perScrollHeight
return {prevScrollTop:this.wrapper.current.scrollTop,prevScrollHeight:this.wrapper.current.scrollHeight};
}
// DOM更新后
componentDidUpdate(pervProps, pervState, {prevScrollHeight,prevScrollTop}) {
//当前向上卷去的高度加上增加的内容高度
this.wrapper.current.scrollTop = prevScrollTop + (this.wrapper.current.scrollHeight - prevScrollHeight);
}
getSnapshotBeforeUpdate的实现:
时机在getDerivedStateFromProps之后,还要把返回结果传给componentDidUpdate
forceUpdate() {
let oldRenderVdom = this.oldRenderVdom;//上一次类组件render方法计算得到的虚拟DOM
//let oldDOM = oldRenderVdom.dom;
let oldDOM = findDOM(oldRenderVdom);//获取 oldRenderVdom对应的真实DOM
if(this.constructor.contextType){
this.context = this.constructor.contextType._currentValue;
}
if(this.constructor.getDerivedStateFromProps){
let newState = this.constructor.getDerivedStateFromProps(this.props,this.state);
if(newState)
this.state = newState;
}
let snapshot = this.getSnapshotBeforeUpdate&&this.getSnapshotBeforeUpdate();
//然后基于新的属性和状态,计算新的虚拟DOM
let newRenderVdom = this.render();
compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);
this.oldRenderVdom = newRenderVdom;
if (this.componentDidUpdate) {
this.componentDidUpdate(this.props, this.state, snapshot);
}