动手实现简单版的 React 2

发布于 2022-05-07 12:01:48 字数 11433 浏览 1088 评论 0

eJ2h8HGfo_M的副本

list diff

在上篇文章中,已经基本可以完成视图的更新了,其实就目前来说,对于一个数组list,如果只是对数组进行 pushpop 来说,目前已经完成的很好了。 每次更新的时候可以打开开发者工具选择 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

纸短情长

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

qq_aHcEbj

文章 0 评论 0

寄与心

文章 0 评论 0

13545243122

文章 0 评论 0

流星番茄

文章 0 评论 0

春庭雪

文章 0 评论 0

潮男不是我

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文