动手实现简单版的 React 2
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,事件系统 等内容,欢迎持续关注后面更新。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: ADB 实用笔记
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论