【React进阶系列】从零开始手把手教你实现一个Virtual DOM(二)node
上集咱们实现了首次渲染从JSX=>Hyperscript=>VDOM=>DOM的过程,今天咱们来看一下当数据变更的时候怎么更新DOM,也就是下图的右半边部分。react
function view(count) { const r = [...Array(count).keys()] return <ul id="filmList" className={`list-${count % 3}`}> { r.map(n => <li>item {(count * n).toString()}</li>) } </ul> }
咱们的view函数接收一个参数count,变量r表示从0到count-1的一个数组。假如count=3, r=[0, 1, 2]。ul的className的值有三种可能:list-0, list-1, list-2。li的数量取决于count。npm
function render(el) { const initialCount = 0 el.appendChild(createElement(view(initialCount))) setTimeout(() => tick(el, initialCount), 1000) } function tick(el, count) { const patches = diff(view(count + 1), view(count)) patch(el, patches) if(count > 5) { return } setTimeout(() => tick(el, count + 1), 1000) }
render函数有两个修改,首先调用view()的时候传入count=0。其次,写了一个定时器,1秒后悔执行tick函数。tick函数接收两个参数,el表明节点元素,count是当前计数值。segmentfault
tick函数依次作了这几件事:数组
下面咱们来实现diff函数和patch函数。浏览器
咱们先列出来新旧两个VDOM对比,会有哪些不一样。在index.js文件的最前面声明一下几个常量。app
const CREATE = 'CREATE' //新增一个节点 const REMOVE = 'REMOVE' //删除原节点 const REPLACE = 'REPLACE' //替换原节点 const UPDATE = 'UPDATE' //检查属性或子节点是否有变化 const SET_PROP = 'SET_PROP' //新增或替换属性 const REMOVE_PROP = 'REMOVE PROP' //删除属性
function diff(newNode, oldNode) { if (!oldNode) { return { type: CREATE, newNode } } if (!newNode) { return { type: REMOVE } } if (changed(newNode, oldNode)) { return { type: REPLACE, newNode } } if (newNode.type) { return { type: UPDATE, props: diffProps(newNode, oldNode), children: diffChildren(newNode, oldNode) } } }
下面咱们一次看一下changed, diffProps, diffChildren函数。函数
function changed(node1, node2) { return typeof(node1) !== typeof(node2) || typeof(node1) === 'string' && node1 !== node2 || node1.type !== node2.type }
检查新旧VDOM是否有变更的方法很简单,spa
function diffProps(newNode, oldNode) { let patches = [] let props = Object.assign({}, newNode.props, oldNode.props) Object.keys(props).forEach(key => { const newVal = newNode.props[key] const oldVal = oldNode.props[key] if (!newVal) { patches.push({type: REMOVE_PROP, key, value: oldVal}) } if (!oldVal || newVal !== oldVal) { patches.push({ type: SET_PROP, key, value: newVal}) } }) return patches }
比较新旧VDOM的属性的变化,并返回相应的patches。code
function diffChildren(newNode, oldNode) { let patches = [] const maximumLength = Math.max( newNode.children.length, oldNode.children.length ) for(let i = 0; i < maximumLength; i++) { patches[i] = diff( newNode.children[i], oldNode.children[i] ) } return patches }
一样采用最大可能性原则,取新旧VDOM的children的最长值做为遍历children的长度。而后依次比较新旧VDOM的在相同INDEX下的每个child。
这里须要强烈注意一下
为了简化,咱们没有引入key的概念,直接比较的是相同index下的child。因此假如说一个列表ul有5项,分别是li1, li2, li3, li4, li5; 若是咱们删掉了第一项,新的变成了li2, li3, li4, li5。那么diffchildren的时候,咱们会拿li1和li2比较,依次类推。这样一来,原本只是删除了li1, 而li2, li3, li4, li5没有任何变化,咱们得出的diff结论倒是[li替换,li2替换, li3替换, li4替换, li5删除]。因此react让你们渲染列表的时候,必须添加Key。
截止到如今,咱们已经获得了咱们须要的补丁。下面咱们要将补丁Patch到DOM里。
function patch(parent, patches, index = 0) { if (!patches) { return } const el = parent.childNodes[index] switch (patches.type) { case CREATE: { const { newNode } = patches const newEl = createElement(newNode) parent.appendChild(newEl) break } case REMOVE: { parent.removeChild(el) break } case REPLACE: { const {newNode} = patches const newEl = createElement(newNode) return parent.replaceChild(newEl, el) break } case UPDATE: { const {props, children} = patches patchProps(el, props) for(let i = 0; i < children.length; i++) { patch(el, children[i], i) } } } }
最后咱们再补充一下patchProps函数
function patchProps(parent, patches) { patches.forEach(patch => { const { type, key, value } = patch if (type === 'SET_PROP') { setProp(parent, key, value) } if (type === 'REMOVE_PROP') { removeProp(parent, key, value) } }) } function removeProp(target, name, value) { //@ if (name === 'className') { return target.removeAttribute('class') } target.removeAttribute(name) }
这个就不用我解释了,代码很直观,setProp函数在上一集咱们已经定义过了。这样一来,咱们就完成了整个数据更新致使DOM更新的完整过程。
npm run compile后打开浏览器查看效果,你应该看到是一个背景颜色在不一样变化,同时列表项在逐渐增长的列表。
至此,咱们的VDOM就所有完成了。系列初我提出的那几个问题不知道你如今是否有了答案。有答案的童鞋能够在文章评论区将你的看法跟你们分享一下。分析全面且准确的会收到个人特殊奖励。😁😁😁😁