首页 > 其他 > 详细

diff原理

时间:2021-06-02 09:35:11      阅读:16      评论:0      收藏:0      [点我收藏+]

? 简单来讲,操作真实 DOM 会引起重排和重绘,会影响到性能,因此要尽量减少 DOM 操作。

createElement

? JSX 每一个节点都会被转化为createElement方法,这个方法会返回virtual dom对象

function createElement(type, props, ...children) {
  const childElements = [].concat(...children).reduce((result, child) => {
    if(child !== false && child !== true && child !== null) {
      if(child instanceof Object) {
        result.push(child)
      } else {
        result.push(createElement(‘text‘, { textContent: child }))
      }
    }
    return result
  }, [])
  return {
    type,
    props: Object.assign({ children: childElements }, props),
    children: childElements
  }

render

? 把vdom转换为真实dom ,ReactDOM.render(virtualDOM, container)

? ReactDOM.render() 渲染传入容器节点的元素,首次调用时,将替换其中所有的 DOM 元素,以后的调用使用 React 的 虚拟 DOM diff 算法进行有效的更新,只更新需要更新的部分

function render(virtualDOM, container, oldDOM = container.firstChild) {
  diff(virtualDOM, container, oldDOM)
}

function diff(virtualDOM, container, oldDOM) {
  const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
  const oldComponent = oldVirtualDOM && oldVirtualDOM.component
  if(!oldDOM) {
    mountElement(virtualDOM, container)
  } else if(virtualDOM.type !== oldVirtualDOM.type && typeof virtualDOM.type !== ‘function‘) {
    const newElement = createDOMElement(virtualDOM)
    oldDOM.parentNode.replaceChild(newElement, oldDOM)
  } else if(typeof virtualDOM.type === "function") {
    diffComponent(virtualDOM, oldComponent, oldDOM, container)
  } else if(oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
    if(virtualDOM.type === "text") {
      updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)
    } else {
      updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
    }

    // 处理key
    // 1. 将拥有key属性的子元素放到单独的一个对象中
    let keyedElements = {}
    for(let i = 0, len = oldDOM.childNodes.lendgh; i<len; i++) {
      let domElement = oldDOM.childNodes[i]
      if(domElement.nodeType === 1) {
        // 元素节点
        let key = domElement.getAttribute(‘key‘)
        if(key) {
          keyedElements[key] = domElement
        }
      }
    }

    let hasNoKey = Object.keys(keyedElements).length === 0

    if(hasNoKey) {
      virtualDOM.children.forEach((child, i) => {
        diff(child, oldDOM, oldDOM.childNodes[i])
      })
    } else {
      // 循环virtual DOM的子元素 获取子元素的key属性
      virtualDOM.children.forEach((child, i) => {
        let key = child.props.key
        if(key) {
          let domElement = keyedElements[key]
          if(domElement) {
            // 看看当前位置的元素是不是我们期望的元素
            if(oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
              oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
            }
          }
        } else {
          // 新增元素
          mountElement(child, oldDOM, oldDOM.childNodes[i])
        }
      })
    }
    // 删除节点
    let oldChildNodes = oldDOM.childNodes
    if(oldChildNodes.length > virtualDOM.children.lendgh) {
      if(hasNoKey) {
        for(let i=oldChildNodes.length-1; i>virtualDOM.children.length-1; i--) {
          unMountNode(oldChildNodes[i])
        }
      } else {
        // 通过key属性删除节点
        for(let i=0; i<oldChildNodes.length; i++) {
          let oldChild = oldChildNodes[i]
          let oldChildKey = oldChild._virtualDOM.props.key
          let found = false
          for(let n=0; n<virtualDOM.children.length; n++) {
            if(oldChildKey === virtualDOM.children[n].props.key) {
              found = true
              break
            }
          }
          if(!found) {
            unMountNode(oldChild)
          }
        }
      }
    }
  }
}

function unMountNode(node) {
  const virtualDOM = node._virtualDOM
  if(virtualDOM.type === ‘text‘) {
    node.remove()
    return
  }

  let component = virtualDOM.component
  // 如果component存在,就说明节点是由组件生成的
  if(component) {
    component.componentWillUnMount()
  }

  if(virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(null)
  }

  Object.keys(virtualDOM.props).forEach(propName => {
    if(propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      const eventhandler = virtualDOM.props[propName]
      node.removeEventListener(eventName, eventhandler)
    }
  })

  if(node.childNodes.length > 0) {
    for(let i=0; i<node.childNodes.length; i++) {
      unMountNode(node.childNodes[i])
      i--
    }
  }

  node.remove()
}

function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
  if(isSameComponent(virtualDOM, oldComponent)) {
    // 同一个组件,做组件更新操作
    updateComponent(virtualDOM, oldComponent, oldDOM, container)
  } else {
    // 不是同一个组件
    mountElement(virtualDOM, container, oldDOM)
  }
}

function isSameComponent(virtualDOM, oldComponent) {
  return oldComponent && virtualDOM.type === oldComponent.constructor
}

function updateComponent(virtualDOM, oldComponent, oldDOM, container) {
  // 生命周期53.25
  oldComponent.componentWillReceiveProps(virtualDOM.props)
  if(oldComponent.shouldComponentDidUpdate(virtualDOM.props)) {
    oldComponent.componentWillUpdate(virtualDOM.props)
    // 组件更新
    oldComponent.updateProps(virtualDOM.props)
    let nextVirtualDOM = oldComponent.render()
    nextVirtualDOM.component = oldComponent
    diff(nextVirtualDOM, container, oldDOM)
    oldComponent.componentDidUpdate()
  }
}

function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {
  if(virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
    oldDOM.textContent = virtualDOM.props.textContent
    oldDOM._virtualDOM = virtualDOM
  }
}

function isFunction(virtualDOM) {
  return virtualDOM && typeof virtualDOM.type === "function"
}

function mountElement(virtualDOM, container, oldDOM) {
  if(isFunction(virtualDOM)) {
    // Component
    mountComponent(virtualDOM, container, oldDOM)
  } else {
    // 普通jsx
    mountNativeElement(virtualDOM, container, oldDOM)
  }
}

function mountNativeElement(virtualDOM, container, oldDOM) {
  let newElement = createDOMElement(virtualDOM)
  if(oldDOM) {
    container.insertBefore(newElement, oldDOM)
  } else {
    container.appendChild(newElement)
  }
  if(oldDOM) {
    unMountNode(oldDOM)
  }
  
  let component = virtualDOM.component
  if(component) {
    component.setDOM(newElement)
  }
}

function createDOMElement(virtualDOM) {
  let newElement = null
  if(virtualDOM.type === "text") {
    newElement = document.createTextNode(virtualDOM.props.textContent)
  } else {
    newElement = document.createElement(virtualDOM.type)
    updateNodeElement(newElement, virtualDOM)
  }
  // 存放旧的vdom
  newElement._virtualDOM = virtualDOM
  virtualDOM.children.forEach(child => {
    mountElement(child, newElement)
  })
  if(virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(newElement)
  }

  return newElement
}

function updateNodeElement(newElement, virtualDOM, oldVirtualDOM = {}) {
  const newProps = virtualDOM.props || {}
  const oldProps = oldVirtualDOM.props || {}
  // 更新属性
  Object.keys(newProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if(newPropsValue !== oldPropsValue) {
      if(propName.slice(0,2) === ‘on‘) {
        const eventName = propName.toLowerCase().slice(2)
        newElement.addEventListener(eventName, newPropsValue)
        if(oldPropsValue) {
          newElement.removeEventListener(eventName, oldPropsValue)
        }
      } else if(propName === "value" || propName === "checked") {
        // value || checked 不能用setAttribute设置
        newElement[propName] = newPropsValue
      } else if(propName !== ‘children‘) {
        if(propName === "className") {
          newElement.setAttributes(‘class‘, newPropsValue)
        } else {
          newElement.setAttributes(propName, newPropsValue)
        }
      }
    }
  })
  // 判断属性被删除的情况,old里面有,new里面没有
  Object.keys(oldProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if(!newPropsValue) {
      // 属性被删除了
      if(propName.slice(0, 2) === ‘on‘) {
        const eventName = propName.toLowerCase().slice(2)
        newElement.removeEventListener(eventName, oldPropsValue)
      } else if(propName !== "children") {
        newElement.removeAttribute(propName)
      }
    }
  })
}

function mountComponent(virtualDOM, container, oldDOM) {
  let component = null
  const nextVirtualDOM = null
  if(isFunctionComponent(virtualDOM)) {
    // 函数组件
    nextVirtualDOM = buildFunctionComponent(virtualDOM)
  } else {
    // 类组件,原型上面有render方法
    nextVirtualDOM = buildClassComponent(virtualDOM)
    component = nextVirtualDOM.component
  }
  if(isFunction(nextVirtualDOM)) {
    mountComponent(nextVirtualDOM, container, oldDOM)
  } else {
    mountNativeElement(nextVirtualDOM, container, oldDOM)
  }
  // 处理组件ref
  if(component) {
    component.componentDidMount()
    if(component.props && component.props.ref) {
      component.props.ref(component)
    }
  }
}

function isFunctionComponent(virtualDOM) {
  const type = virtualDOM.type
  return type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)
}

function buildFunctionComponent(virtualDOM) {
  return virtualDOM.type(virtualDOM.props || {})// props就在vdom的props里存放
}

function buildClassComponent(virtualDOM) {
  const component = new virtualDOM.type(virtualDOM.props || {})
  const nextVirtualDOM = component.render()
  nextVirtualDOM.component = component
  return nextVirtualDOM
}

// extends React.Component
class Component {
  constructor(props) {
    this.props = props
  }
  setState(state) {
    this.state = Object.assign({}, this.state, state)
    // 获取最新的要渲染的vdom对象
    let virtualDOM = this.render()
    let oldDOM = this.getDOM()
    let container = oldDOM.parentNode
    diff(virtualDOM, container, oldDOM)
  }
  setDOM(dom) {
    this._dom = dom
  }
  getDOM() {
    return this._dom
  }
  updateProps(props) {
    this.props = props
  }
  // 生命周期函数
  // ...
  componentDidMount() {}
}

获取ref

render() {
  return (
  	<input ref={input => (this.input = input)} />
    <Cmp ref={cmp => (this.cmp = cmp)}/>
    <button onClick={() => { console.log(this.input.value) }}></button>
  )
}

实现思路

? 在mountComponent中,如果判断是类组件,就通过类组件返回的VirtualDOM对象中获取实例对象,判断组件实例中的props属性是否有ref属性,如果有就调用ref属性中所存储的方法并且将创建出来的DOM对象作为参数传递给ref方法

? 这样在渲染组件节点时就可以拿到元素对象并将元素对象存储为组件属性了

if(virtualDOM.props && virtualDOM.props.ref) {
  virtualDOM.props.ref(newElement)
}

diff

三个假设

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计;
  2. 两个相同组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构;
  3. 对于同一层次的一组子节点,它们可以通过唯一的 id 进行区分。

基于以上三个假设,React 分别对 tree diff、component diff 以及 element diff 进行算法优化。

tree diff

? 首先是树的比较,React 通过对两个树的同一层的节点进行比较,当发现节点已经不存在时,则该节点及其子节点会被完全删除掉,这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较

component diff

? 如果是组件之间进行比较的话,如果是同一类型,就是按照原策略继续比较虚拟 DOM Tree,如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。当然如果是同一类型,我们能确定虚拟DOM没有变化,可以通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff

element diff

? 对同一层级的同组子节点,添加唯一 key 进行区分,我们会有新的节点集合和老的节点集合。如果老的集合中没有新的元素,就创建新的节点并插入,如果有看位置是否正确,移动位置。如果老的集合中有多余的元素就直接删除。

_

? 进行比对时,需要用到更新后的Virtual DOM和更新前的Virtual DOM,对于更新前的Virtual DOM,对应的其实就是已经在页面显示的真实DOM对象。所以可以在创建真实DOM对象时,把Virtual DOM添加到真实DOM对象的属性当中。

? 首先是同级节点之间进行比对,当Virtual DOM的类型相同时,如果是元素节点,就对比元素节点属性是否发生变化,看看新节点的属性值是否和旧节点的属性值相同,相同不做处理,如果不同就用新节点属性值替换旧节点属性值,再看看新节点中是否有被删除的属性,使用旧节点属性名称去新节点属性对象中取值,取不到说明该属性被删除了;如果是文本节点就对比文本节点内容是否发生变化,如果发生变化就用新的文本节点替换旧的文本节点。

? 节点比对时执行的是深度优先的比对顺序,即子节点对比优先于同级别的节点对比,循环Virtual DOM的children属性,在循环体内递归调用diff方法进行比对,就是优先查找子节点进行比对,当所有子节点对比完成之后再跳回同级节点进行比对,同级节点还有子节点的话,对比完同级节点马上比对其子节点,完成之后再跳回同级节点开始比对下一个同级节点

? 当比较的节点类型不相同时就没必要进行比对了,用新的VirtualDOM对象去生成新的DOM对象,使用这个新的DOM对象去替换旧的DOM对象。

? 删除节点发生在节点更新之后,分析哪些节点需要被删除,并且发生在同一个父节点下的所有子节点身上。如果更新完成之后旧节点对象的数量多于新VirtualDOM节点的数量,就说明有节点需要被删除。

? 在diff方法中还要判断要更新的VirtualDOM是否是组件,如果是组件还要判断要更新的组件和未更新前的组件是否是同一个组件,如果不是同一个组件就不需要做组件的更新操作,直接调用mountElement方法将组件返回的VirtualDOM添加到页面中;如果是同一个组件,就执行更新组件操作,其实就是将最新的props传递到组件中,再调用组件的render方法获取组件返回的最新VirtualDOM对象,再将VirtualDOM对象传递给diff方法,让diff方法找出差异,从而将差异更新到真实DOM对象中。在更新组件的过程中还要在不同阶段调用其不同的的组件生命周期函数。

?

key 属性

? 在React中,渲染列表数据时通常需要加key属性,是数据的唯一标识。可以帮助react识别哪些数据被修改或删除了,从而达到DOM最小化操作的目的。在比对同一个父节点下类型相同的子节点时需要用到key属性

节点对比

? 实现思路是在两个元素进行比对时,如果类型相同,就循环旧的DOM元素的子元素,查看其身上是否有key属性,如果有就将这个子元素的DOM对象存储在一个JS对象中,接着循环要渲染的vdom的子元素,在循环过程中获取到这个子元素的key属性,然后使用这个key属性到JS对象中查找DOM对象,如果能找到说明这个元素是已经存在的,是不需要重新渲染的。如果通过key属性找不到这个元素,就说明这个元素是新增的是需要渲染的

节点卸载

? 在比对节点的时候,如果旧节点的数量多于要渲染的新节点的数量就说明有节点要被删除了,继续判断keyedElements对象中是否有元素,如果没有就用索引的方式删除,如果有就要使用key比对的方式删除。

? 实现思路是循环旧节点,在循环旧节点的过程中获取旧节点对应的key属性,然后根据key属性在新节点中查找这个旧节点,如果找到就说明这个旧节点没有被删除,如果没有找到,就说明节点被删除了,调用卸载节点的方法卸载节点即可

? 卸载节点并不是直接删除就可以了,还需要考虑一些情况

  1. 如果是文本节点可以直接删除
  2. 如果是组件,则还需调用其生命周期函数
  3. 如果包含了其他组件生成的节点,需要调用其他组件的卸载生命周期函数
  4. 如果有ref,删除通过ref属性传递给组件的DOM节点对象
  5. 如果有事件,需要删除事件对应的事件处理函数

diff原理

原文:https://www.cnblogs.com/hyhwy/p/14839174.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!