Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈react #5

Open
shen1992 opened this issue Dec 10, 2017 · 0 comments
Open

浅谈react #5

shen1992 opened this issue Dec 10, 2017 · 0 comments

Comments

@shen1992
Copy link
Owner

shen1992 commented Dec 10, 2017

目录:

  • 1.由jquery过渡到react的思考
  • 2.虚拟dom
  • 3.diff比较
  • 4.patch操作
  • 5.结语

1.由jquery过渡到react的思考

react是当下十分流行的一个专注于view层的库,并不是一个完整的mvvc框架。它的出现掩盖了jquery的锋芒,倍受前端大神们的推崇。

早期的jquery核心是操作dom,但是随着现在的应用越来越复杂,频繁的dom操作会让代码变得难以维护,于是就有人提出了可以把状态和视图绑定在一起,当状态改变就更新视图,这样就摆脱了dom操作,早期的backbone就是这么做的。

但是backbone没有解决的问题是性能问题,更新视图的最原始的方式是把之前的dom树移除,插入一个新的dom树,然而操作dom的成本是十分昂贵的,试想一下:如果我只需要改变一下按钮的颜色,就必须生成一颗新的dom树,这不是很浪费性能吗?所以backbone只适合做一些小型的应用。

react的出现解决了这个性能瓶颈,它在更新视图之前,通过比较两颗新旧虚拟dom,找出差异,然后只更新有差异的那一部分dom,实现了既把状态和视图绑定在一起,又保证了性能。

2.虚拟dom

dom元素3个重要的特性是:

  • 元素的标签名(tagName)
  • 元素的属性(props)
  • 元素的子元素(children)

js可以用对象来储存这3个属性,从而模拟dom元素

function Element (tagName, props, children) {
  this.tagName = tagName
  this.props = props
  this.children = children
}

Element.prototype.render = function () {
  var el = document.createElement(this.tagName) // 根据tagName构建
  var props = this.props

  for (var propName in props) { // 设置节点的DOM属性
    var propValue = props[propName]
    el.setAttribute(propName, propValue)
  }

  var children = this.children || []

  children.forEach(function (child) {
    var childEl = (child instanceof Element)
      ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
      : document.createTextNode(child) // 如果字符串,只构建文本节点
    el.appendChild(childEl)
  })

  return el
}

可以这样使用

var el = require('./element')

var ul = el('ul', {id: 'list'}, [
  el('li', {class: 'item'}, ['Item 1']),
  el('li', {class: 'item'}, ['Item 2']),
  el('li', {class: 'item'}, ['Item 3'])
])

var ulRoot = ul.render()
document.body.appendChild(ulRoot)

可见通过render方法就可以生成一个真正的dom元素,并且把它插入document。

3.diff比较

如果想完整的比较两颗虚拟dom树之间的差别,时间复杂度是 O(n^3) ,这无疑太慢了,是不能运用在开发当中的。由于前端很少会跨级别操作dom,所以diff只会比较同一层级的dom,这样时间复杂度就将为O(n)。

深度优先遍历旧的虚拟dom树,每遍历到一个节点,就和对应的新的虚拟dom树上的节点进行比较。

function diff (oldTree, newTree) {
  var index = 0
  var patches = {}
  dfsWalk(oldTree, newTree, index, patches)
  return patches
}

function dfsWalk (oldNode, newNode, index, patches) {
  var currentPatch = []

  // Node is removed.
  if (newNode === null) {
    // Real DOM node will be removed when perform reordering, so has no needs to do anthings in here
  // TextNode content replacing
  } else if (_.isString(oldNode) && _.isString(newNode)) {
    if (newNode !== oldNode) {
      currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  // Nodes are the same, diff old node's props and children
  } else if (
      oldNode.tagName === newNode.tagName &&
      oldNode.key === newNode.key
    ) {
    // Diff props
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // Diff children. If the node has a `ignore` property, do not diff children
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
  // Nodes are not the same, replace the old node with new node
  } else {
    currentPatch.push({ type: patch.REPLACE, node: newNode })
  }

  if (currentPatch.length) {
    patches[index] = currentPatch
  }
}

主要比较

  • 如果两个节点是文本节点,比较它们的值是否相等
  • 比较元素的标签名和key是否相等,如果相等则证明是同一个元素,就比较元素的属性和子元素是否相等
  • 如果不属于以上两种情况就重新生成一个新的dom元素

4.patch操作

diff操作将节点之间的差异记录下来,在patch进行更新,其实本质就是dom操作。

var REPLACE  // 重新渲染dom节点
var REORDER  // 更新子元素
var PROPS   // 更新props
var TEXT   // 文本节点,更新内容

function applyPatches (node, currentPatches) {
  _.each(currentPatches, function (currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string')
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render()
        node.parentNode.replaceChild(newNode, node)
        break
      case REORDER:
        reorderChildren(node, currentPatch.moves)
        break
      case PROPS:
        setProps(node, currentPatch.props)
        break
      case TEXT:
        if (node.textContent) {
          node.textContent = currentPatch.content
        } else {
          // fuck ie
          node.nodeValue = currentPatch.content
        }
        break
      default:
        throw new Error('Unknown patch type ' + currentPatch.type)
    }
  })
}

5.结语

初学react的人可能会有疑惑:人人都说react的虚拟dom快,性能好,但是实际运用的时候发现和原生的操作dom并没有明显的速度上的差异。其实react从来没有说过它比原生的js快,因为最终的本质都是dom操作,只是它的出现改变了前端的编码方式,从繁杂的dom操作过渡到通过状态的改变更新视图。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant