blog
blog copied to clipboard
动手实现简单版的 React(二)
--
list diff
在上篇文章中,已经基本可以完成视图的更新了,其实就目前来说,对于一个数组list,如果只是对数组进行 push
和 pop
来说,目前已经完成的很好了。 每次更新的时候可以打开开发者工具选择 Element 面板,看到每次操作影响的DOM,影响的DOM会存在闪动的效果。但是考虑一下数组移动的情况:
如将 [1,2,3,4] 修改为 [2,1,4,3],如果你的这些 List 结构一致,可能仅仅是更新文本内容,但是如果内部可能会因数据的不同渲染不同的组件,那么这个时候甚至需要重新 tear down 所有的旧节点,然后再挂载新节点,也就是,create 2,delete1,create1,delete2,依次类推。同理如果是在中间插入也就会出现这种影响。看到这里,相信不会再有虚拟DOM比DOM操作快的这种谬误了,本质上来说,如果真的操作dom,肯定会是移动1或者2,再移动3或者4的,这才是最快的方法。
那么现在的问题就在于如何记录对应操作(增、删、移)?对于增删,相对来说比价简单,
React 聪明地通过记录一个 LastIndex
的索引,目的就是只有大于 index 值的元素才标记为移动,也就是说,react 的移动是前面的元素向后移动的,这样就保证能够正确的移动了。
那么大致的过程就是遍历新节点,找出对应的增加和移动操作,遍历旧节点,找到删除操作,这样就得到的补丁(patch),然后根据得到的补丁更新 Dom。
照例先编写类型:
export interface keyChanges {
type: 'insert' | 'remove' | 'move',
item: VdomInterface,
afterNode?: VdomInterface,
index: number
}
根据只有 key
属性的 list 才做这样的 diff,判断是否含有 key:
function isKeyChildren(oldChildren: Vdom, newChildren: Vdom):boolean {
return !!(oldChildren && oldChildren[0] && oldChildren[0].props && oldChildren[0].props.key && newChildren && newChildren[0] && newChildren[0].props && newChildren[0].props.key)
}
在 diff 过程中,判断是否含有 key,含有的话就根据上面的原则进行 diff:
const _component = dom._component as VdomInterface
const isKeyed = isKeyChildren(_component.children, vdom.children)
if(!isKeyed) {
for(let i = 0; i < max; i++) {
diff(dom.childNodes[i] || null, vdom.children[i] || null, dom)
}
} else {
const patchesList = diffKeyChildren(_component.children, vdom.children)
patch(patchesList, dom)
}
这时候整个 diff 的代码如下:
export default function diff(dom: Dom, vdom, parent: Dom = dom.parentNode):void {
if(!dom) {
render(vdom, parent)
} else if (!vdom) {
dom.parentNode.removeChild(dom)
} else if ((typeof vdom === 'string' || typeof vdom === 'number') && dom.nodeType === 3) {
if(vdom !== dom.textContent) dom.textContent = vdom + ''
} else if (vdom.nodeType === 'classComponent' || vdom.nodeType === 'functionalComponent') {
const _component = dom._component as ClassComponent
if (_component.constructor === vdom.type) {
_component.props = vdom.props
diff(dom, _component.render())
} else {
const newDom = render(vdom, dom.parentNode)
dom.parentNode.replaceChild(newDom, dom)
}
} else if (vdom.nodeType === 'node') {
if(!isSameNodeType(dom, vdom)) {
const newDom = render(vdom, parent)
dom.parentNode.replaceChild(newDom, dom)
} else {
const max = Math.max(dom.childNodes.length, vdom.children.length)
diffAttribute(dom as HTMLElement, dom._component.props, vdom.props)
const _component = dom._component as VdomInterface
const isKeyed = isKeyChildren(_component.children, vdom.children)
if(!isKeyed) {
for(let i = 0; i < max; i++) {
diff(dom.childNodes[i] || null, vdom.children[i] || null, dom)
}
} else {
const patchesList = diffKeyChildren(_component.children, vdom.children)
patch(patchesList, dom)
}
}
}
}
根据上面 list diff 的分析,对应的代码如下:
export default function diffChildren(oldVdom: VdomInterface[], newVdom: VdomInterface[]):keyChanges[] {
const changes = []
let lastIndex = 0
let lastPlacedNode = null
const oldVdomKey = oldVdom.map(v => v.props.key)
const newVdomKey = newVdom.map(v => v.props.key)
newVdom.forEach((item, i) => {
const index = oldVdomKey.indexOf(item.props.key)
if (index === -1) {
changes.push({
type: 'insert',
item: item,
afterNode: lastPlacedNode
})
lastPlacedNode = item
} else {
if (index < lastIndex) {
changes.push({
type: 'move',
item: oldVdom[index],
afterNode: lastPlacedNode
})
}
lastIndex = Math.max(index, lastIndex)
lastPlacedNode = oldVdom[index]
}
})
oldVdom.forEach((item, i) => {
if (newVdomKey.indexOf(item.props.key) === -1) {
changes.push({
type: 'remove',
index: i,
item
})
}
})
return changes
}
方便测试,可以将其简化为对应的数组diff,如下:
function diffChildren(oldVdom, newVdom) {
const changes = []
let lastIndex = 0
let lastPlacedNode = null
newVdom.forEach((item, i) => {
const index = oldVdom.indexOf(item);
if (index === -1) {
changes.push({
type: 'insert',
item: item,
afterNode: lastPlacedNode
})
} else {
if (index < lastIndex) {
changes.push({
type: 'move',
item: item,
afterNode: lastPlacedNode
})
}
lastIndex = Math.max(index, lastIndex)
}
lastPlacedNode = item
})
oldVdom.forEach((item, i) => {
if (newVdom.indexOf(item) === -1) {
changes.push({
type: 'remove',
index: i
})
}
})
return changes
}
编写测试:
const changes = diffChildren([1, 2, 3, 7, 4], [1, 4, 5, 3, 7, 6])
console.log(changes)
运行可以看到对应的更改的结构输出:
拿到对应的操作记录补丁,就需要应用到真实的DOM中,如下:
import render from './render'
import { keyChanges } from './types/keyChanges'
import { Dom } from './types/dom'
import { VdomInterface, Vdom } from './types/vdom'
export default function patch(changes: keyChanges[], dom: Dom):void {
changes.forEach(change => {
switch (change.type) {
case 'insert': {
const node = change.afterNode.base
const parent = node.parentNode as Node
const child = render(change.item, parent)
parent.insertBefore(child, node.nextSibling)
}
break;
case 'remove':{
const removedNode = change.item.base as Node
removedNode.parentNode.removeChild(removedNode)
}
break;
case 'move': {
const node = change.item.base
const afterNode = change.afterNode.base as Node
node.parentNode.insertBefore(node,afterNode.nextSibling)
}
break;
default:
}
})
const vchildren = Array.from(dom.childNodes).map((e: Dom):Vdom => e._component as VdomInterface)
const _component: VdomInterface = dom._component as VdomInterface
_component.children = vchildren
}
这里需要注意的是我们需要更新父节点的 _component.children
属性的值,让他始终保持最新,这样保证下一次 list diff 是正确的。
编写对应的测试代码:
export default class App extends Component<any, any> {
public state = {
list: [1, 2, 3, 7, 4]
}
constructor(props) {
super(props)
}
update() {
const { list } = this.state
this.setState({
list: list.indexOf(4) > 2 ? [1, 4, 5, 3, 7, 6] : [1, 2, 3, 7, 4]
})
}
render() {
const { list } = this.state
return (
<div>
<h1> list diff</h1>
<div className="optcontainer">
<div className="opt" onClick={this.update.bind(this)}>
update
</div>
</div>
<ul>{list.map(l => <li key={l}>{l}</li>)}</ul>
</div>
)
}
}
打开浏览器可以看到对应的界面:
通过 update 操作可以看到视图的更新,同时打开开发者工具也能够看到影响DOM的闪动是符合我们预期的。
生命周期
这里依然以 React 16.3 之前的版本的生命周期为准。
首先可以先更新 setState
时的生命周期,在 flush
函数中,如下:
function flush(component) {
component.prevState = Object.assign({}, component.state)
if(component.shouldComponentUpdate(component.props, component._pendingStates)) {
component.componentWillUpdate && component.componentWillUpdate(component.props ,component._pendingStates )
Object.assign(component.state, component._pendingStates)
diff(component.base, component.render())
component.componentDidUpdate && component.componentDidUpdate(component.props, component.prevState)
}
}
接着更新 diff 中的的生命周期,并且是只有在组件时才有生命周期:
const _component = dom._component as ClassComponent
if (_component && _component.constructor === vdom.type) {
_component.componentWillReceiveProps && _component.componentWillReceiveProps(vdom.props)
if(_component.shouldComponentUpdate(vdom.props, _component.state)) {
_component.componentWillUpdate && _component.componentWillUpdate(vdom.props, _component.state)
const prevProps = Object.assign({}, _component.props)
_component.props = vdom.props
diff(dom, _component.render())
_component.componentDidUpdate && _component.componentDidUpdate(prevProps, _component.state)
}
} else {
const newDom = render(vdom, dom.parentNode)
_component.componentWillUnmount && _component.componentWillUnmount()
dom.parentNode.replaceChild(newDom, dom)
}
以上已经处理了更新和卸载时的生命周期,接着在 render
中加入挂载阶段的生命周期:
if(instance.componentWillMount) instance.componentWillMount()
const classChildVdom = instance.render()
if(instance.componentDidMount) instance.componentDidMount()
编写测试用例:
import { createElement, Component } from '../lib5'
class List extends Component<any, any> {
componentWillReceiveProps(nextProps) {
console.log(nextProps)
console.log(this.props)
console.log('List: componentWillReceiveProps')
}
componentWillUpdate(nextProps, nextState) {
console.log(nextProps)
console.log(this.props)
console.log('List: componentWillUpdate')
}
render() {
const { name } = this.props
return <div>test {name}</div>
}
}
export default class App extends Component<any, any> {
public state = {
name: 'huruji'
}
update() {
this.setState({
name: this.state.name + '1'
})
}
componentWillMount() {
console.log('APP: componentWillMount')
}
componentDidMount() {
console.log('APP: componentDidMount')
}
componentWillUpdate(nextProps, nextState) {
console.log(nextProps)
console.log(nextState)
console.log(this.props)
console.log(this.state)
console.log('APP: componentWillUpdate')
}
componentDidUpdate() {
console.log('APP: componentDidUpdate')
}
render() {
console.log('render')
const { name } = this.state
return (
<div className="container">
<div className="optcontainer">
<div className="opt" onClick={this.update.bind(this)}>
update state
</div>
</div>
<p>{name}</p>
<List name={name} />
</div>
)
}
}
打开控制台后可以看到相应输出:
说明此时已经正常运行了。
目前所有代码存放在 github,可以在 https://github.com/huruji/rego 看到。
梳理一下,目前代码其实基本已经可以跑了,不过还缺少 react-fiber ,react-hooks,事件系统 等内容,欢迎持续关注后面更新。