资讯专栏INFORMATION COLUMN

【React进阶系列】从零开始手把手教你实现一个Virtual DOM(三)

qqlcbb / 953人阅读

摘要:函数依次做了这几件事调用函数,对比新旧两个,根据两者的不同得到需要修改的补丁将补丁到真实上当计数器小于等于的时候,将加,再继续下一次当计数器大于的时候,结束下面我们来实现函数和函数。

上集回顾

【React进阶系列】从零开始手把手教你实现一个Virtual DOM(二)

上集我们实现了首次渲染从JSX=>Hyperscript=>VDOM=>DOM的过程,今天我们来看一下当数据变动的时候怎么更新DOM,也就是下图的右半边部分。

改写view()
function view(count) { 
  const r = [...Array(count).keys()]
  return 
    { r.map(n =>
  • item {(count * n).toString()}
  • ) }
}

我们的view函数接收一个参数count,变量r表示从0到count-1的一个数组。假如count=3, r=[0, 1, 2]。ul的className的值有三种可能:list-0, list-1, list-2。li的数量取决于count。

改写render()
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是当前计数值。

tick函数依次做了这几件事:

调用diff函数,对比新旧两个VDOM,根据两者的不同得到需要修改的补丁

将补丁patch到真实DOM上

当计数器小于等于5的时候,将count加1,再继续下一次tick

当计数器大于5的时候,结束

下面我们来实现diff函数和patch函数。

我们先列出来新旧两个VDOM对比,会有哪些不同。在index.js文件的最前面声明一下几个常量。

const CREATE = "CREATE"   //新增一个节点
const REMOVE = "REMOVE"   //删除原节点
const REPLACE = "REPLACE"  //替换原节点
const UPDATE = "UPDATE"    //检查属性或子节点是否有变化
const SET_PROP = "SET_PROP"  //新增或替换属性
const REMOVE_PROP = "REMOVE PROP"  //删除属性
diff()
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)
     }
   }
}

假如旧节点不存在,我们返回的patches对象, 类型为新增节点;

假如新节点不存在,表示是删除节点;

假如两者都存在的话,调用changed函数判断他们是不是有变动;

假如两者都存在,且changed()返回false的话,判断新节点是否是VDOM(根据type是否存在来判断的,因为type不存在的话,newNode要么是空节点,要么是字符串)。假如新节点是VDOM,则返回一个patches对象,类型是UPDATE,同时对props和children分别进行diffProps和diffChildren操作。

下面我们一次看一下changed, diffProps, diffChildren函数。

changed()
function changed(node1, node2) {
  return typeof(node1) !== typeof(node2) ||
         typeof(node1) === "string" && node1 !== node2 ||
         node1.type !== node2.type
}

检查新旧VDOM是否有变动的方法很简单,

首先假如数据类型都不一样,那肯定是变动了;

其次假如两者的类型都是纯文本,则直接比较两者是否相等;

最后比较两者的类型是否相等。

diffProps()
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。

首先我们采用最大可能性原则,将新旧VDOM的所有属性都合并赋值给一个新的变量props

遍历props变量的所有Keys,依次比较新旧VDOM对于这个KEY的值

假如新值不存在,表示这个属性被删除了

假如旧值不存在,或者新旧值不同,则表示我们需要重新设置这个属性

diffChildren()
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里。

patch()
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)
      }
    }
  }
}

首先当patches不存在时,直接return,不进行任何操作

利用childNodes和Index取出当前正在处理的这个节点,赋值为el

开始判断补丁的类型

当类型是CREATE时,生成一个新节点,并append到根节点

当类型是REMOVE时,直接删除当前节点el

当类型是REPLACE时,生成新节点,同时替换掉原节点

当类型是UPDATE时,需要我们特殊处理

调用patchProps将我们之前diffProps得到的补丁渲染到节点上

遍历之前diffChildren得到的补丁列表,再依次递归调用patch

最后我们再补充一下patchProps函数

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就全部完成了。系列初我提出的那几个问题不知道你现在是否有了答案。有答案的童鞋可以在文章评论区将你的见解跟大家分享一下。分析全面且准确的会收到我的特殊奖励。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/94686.html

相关文章

  • React进阶系列从零开始把手教你实现一个Virtual DOM(一)

    摘要:可实际上并不是创造的,将这个概念拿过来以后融会贯通慢慢地成为目前前端最炙手可热的框架之一。则是将再抽象一层生成的简化版对象,这个对象也拥有上的一些属性,比如等,但它是完全脱离于浏览器而存在的。所以今天我要手把手教大家怎么从零开始实现。 假如你的项目使用了React,你知道怎么做性能优化吗?你知道为什么React让你写shouldComponentUpdate或者React.PureCo...

    PumpkinDylan 评论0 收藏0
  • React进阶系列从零开始把手教你实现一个Virtual DOM(二)

    摘要:上集回顾从零开始手把手教你实现一个一上一集我们介绍了什么是,为什么要用,以及我们要怎样来实现一个。完成后,在命令行中输入安装下依赖。最后返回这个目标节点。明天,我们迎接挑战,开始处理数据变动引起的重新渲染,我们要如何新旧,生成补丁,修改。 上集回顾 从零开始手把手教你实现一个Virtual DOM(一)上一集我们介绍了什么是VDOM,为什么要用VDOM,以及我们要怎样来实现一个VDOM...

    dendoink 评论0 收藏0
  • Vue.js最佳实践(五招让你成为Vue.js大师)

    摘要:但如果你想更加高效地使用来开发,成为大师,那下面我要传授的这五招你一定得认真学习一下了。虽然损失了一丢丢性能,但避免了无限的。所以我们需要设置,这些默认行为将会被去掉以上两点的优化才能成功。陆续可能还会更新一些别的招数,敬请期待。 本文面向对象是有一定Vue.js编程经验的开发者。如果有人需要Vue.js入门系列的文章可以在评论区告诉我,有空就给你们写。 对大部分人来说,掌握Vue.j...

    CocoaChina 评论0 收藏0
  • javascript知识点

    摘要:模块化是随着前端技术的发展,前端代码爆炸式增长后,工程化所采取的必然措施。目前模块化的思想分为和。特别指出,事件不等同于异步,回调也不等同于异步。将会讨论安全的类型检测惰性载入函数冻结对象定时器等话题。 Vue.js 前后端同构方案之准备篇——代码优化 目前 Vue.js 的火爆不亚于当初的 React,本人对写代码有洁癖,代码也是艺术。此篇是准备篇,工欲善其事,必先利其器。我们先在代...

    Karrdy 评论0 收藏0
  • 前方来报,八月最新资讯--关于vue2&3的最佳文章推荐

    摘要:哪吒别人的看法都是狗屁,你是谁只有你自己说了才算,这是爹教我的道理。哪吒去他个鸟命我命由我,不由天是魔是仙,我自己决定哪吒白白搭上一条人命,你傻不傻敖丙不傻谁和你做朋友太乙真人人是否能够改变命运,我不晓得。我只晓得,不认命是哪吒的命。 showImg(https://segmentfault.com/img/bVbwiGL?w=900&h=378); 出处 查看github最新的Vue...

    izhuhaodev 评论0 收藏0

发表评论

0条评论

qqlcbb

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<