构建本身的React:(3)Instances, reconciliation and virtual DOM

翻译自:https://engineering.hexacta.c…

停止如今我们已能够运用JSX来建立并衬着页面DOM。在这一节我们将会把重点放在怎样更新DOM上。

在引见setState之前,更新DOM只能经由历程变动入参并再次挪用render要领来完成。假如我们想完成一个时钟,代码也许下面这个模样:

const rootDom = document.getElementById("root");

function tick() {
  const time = new Date().toLocaleTimeString();
  const clockElement = <h1>{time}</h1>;
  render(clockElement, rootDom);
}

tick();
setInterval(tick, 1000);

事实上,上面的代码运转后并不能到达预期的效果,屡次挪用当前版本的render要领只会不停往页面上增添新的元素,而不是我们预期的更新已存在的元素。下面我们想办法完成更新操纵。在render要领末端,我们能够去搜检父类元素是不是含有子元素,假如有,我们就用新天生的元素去替代旧的元素。

function render(element, parentDom){
    // ...
    // Create dom from element
    // ...
    if(!parentDom.lastChild){
        parentDom.appendChild(dom);
    } else {
        parentDom.replaceChild(dom, parentDom.lastChild);
    }
}

针对开首谁人时钟的例子,上面render的完成是没题目标。但关于更庞杂的状况,比方有多个子元素时上面代码就不能满足要求了。准确的做法是我们须要比较前后两次挪用render要领时所天生的元素树,对照差别后只更新有变化的部份。

Virtual DOM and Reconciliation

React把一致性校验的历程称作“diffing”,我们要做的和React一样。起首须要把当前的元素树保留起来以便和背面新的元素树比较,也就是说,我们须要把当前页面内容所对应的假造DOM保留下来。

这颗假造DOM树的节点有必要讨论一下。一种挑选是运用Didact Elements,它们已含有props.children属性,我们能够依据这个属性构建出假造DOM树。如今有两个题目摆在面前:起首,为了轻易比较,我们须要保留每一个假造DOM指向的实在DOM的援用(校验历程当中我们有须要会去更新现实DOM的属性),而且元素还如果不可变的;第二,如今元素还不支撑含有内部状况(state)的组件。

Instances

我们须要引入一个新的观点—–instances—–来处置惩罚上面的题目。一个实例示意一个已衬着到DOM的元素,它是含有elementdomchildInstances属性的一个JS对象。childInstances是由子元素对应实例构成的数组。

注重,这里说的实例和Dan Abramov在
React Components, Elements, and Instances中提到的实例并非一回事。Dan说的是大众实例,是挪用继续自
React.Component的组件的组织函数后返回的东西。我们将在背面的章节增添大众实例。

每一个DOM节点都邑有对应的实例。一致性校验的目标之一就是只管避免除建立或许移除实例。建立和移除实例意味着我们要修正DOM树,所以我们越多的重用实例就会越少的去修正DOM树。

Refactoring

接下来我们来重写render要领,增添一致性校验算法,同时增添一个instantiate要领来为元素建立实例。

let rootInstance = null; // 用来保留上一次挪用render发生的实例

function render(element, container){
    const prevInstance = rootInstance;
    const nextInstance = reconcile(container, prevInstance, element);
    rootInstance = nextInstace;
}

// 如今只是针对根元素的校验,没有处置惩罚到子元素
function reconcile(parentDom, instance, element){
    if(instance === null){
        const newInstance = instantiate(element);
        parentDom.appendChild(newInstance.dom);
        return newInstance;
    } else {
        const newInstance = instantiate(element);
        parentDom.replaceChild(newInstance.dom, instance.dom);
        return newInstance;
    }
}

// 天生元素对应实例的要领
function instantiate(element){
    const { type, props} = element;
    
    const isTextElement = type === 'TEXT_ELEMENT';
    const dom = isTextElement ? document.createTextNode('') 
        : document.createElement(type);
        
    // 增添事宜
    const isListener = name => name.startsWith("on");
    Object.keys(props).filter(isListener).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, props[name]);
     });

      // 设置属性
      const isAttribute = name => !isListener(name) && name != "children";
      Object.keys(props).filter(isAttribute).forEach(name => {
        dom[name] = props[name];
      });
      
      const childElements = props.children || [];
      const childInstances = childElements.map(instantiate);
      const childDoms = childInstances.map(childInstance => childInstace.dom);
      childDoms.forEach(childDom => dom.appendChild(childDOm));
      
      const instance = {dom, element, childInstances};
      return instance;
}

上面的render要领和之前的差不多,不同之处是保留了上次挪用render要领发生的实例。我们还把一致性校验的功用从建立实例的代码平分离了出来。

为了重用dom节点,我们须要一个能更新dom属性的要领,如许就不必每次都建立新的dom节点了。我们来革新一下现有代码中设置属性的那部份的代码。

function instantiate(element) {
  const { type, props } = element;

  // 建立DOM元素
  const isTextElement = type === 'TEXT_ELEMENT';
  const dom = isTextElement
    ? document.createTextNode("")
    : document.createElement(type);

  updateDomProperties(dom, [], props); // 实例化一个新的元素

  // 实例化并增添子元素
  const childElements = props.children || [];
  const childInstances = childElements.map(instantiate);
  const childDoms = childInstances.map(childInstance => childInstance.dom);
  childDoms.forEach(childDom => dom.appendChild(childDom));

  const instance = { dom, element, childInstances };
  return instance;
}

function updateDomProperties(dom, prevProps, nextProps){
    const isEvent = name => name.startsWith('on');
       const isAttribute = name => !isEvent(name) && name != 'children';
       
       Object.keys(prevProps).filter(isEvent).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.removeEventListener(eventType, prevProps[name]);
       });
       
       Object.keys(preProps).filter(isAttribute).forEach(name => {
        dom[name] = nextProps[name];
       });
       
       // 设置属性
      Object.keys(nextProps).filter(isAttribute).forEach(name => {
        dom[name] = nextProps[name];
      });

      // 增添事宜监听
      Object.keys(nextProps).filter(isEvent).forEach(name => {
        const eventType = name.toLowerCase().substring(2);
        dom.addEventListener(eventType, nextProps[name]);
      });
}

updateDomProperties要领会移除所有旧的属性,然后再增添新属性。假如属性没有变化的话依旧会举行移除和增添操纵,这肯定水平上有些糟蹋,但我们先如许放着,背面再处置惩罚。

Reusing DOM nodes

前面说过,一致性校验算法须要只管多的去重用已建立的节点。由于如今元素的type都是代表HTML中标署名的字符串,所以假如统一位置前后两次衬着的元素的范例一样则示意二者为统一类元素,对应的已衬着到页面上的dom节点就能够被重用。下面我们在reconcile中增添推断前后两次衬着的元素范例是不是雷同的功用,雷同的话实行更新操纵,否则是新建或许替代。

function reconcile(parentDom, instance, element) {
  if (instance == null) {
    // 建立实例
    const newInstance = instantiate(element);
    parentDom.appendChild(newInstance.dom);
    return newInstance;
  } else if (instance.element.type === element.type) { // 和老的实例举行范例比较
    // 更新
    updateDomProperties(instance.dom, instance.element.props, element.props);
    instance.element = element;
    return instance;
  } else {
    // 假如不相等的话直接替代
    const newInstance = instantiate(element);
    parentDom.replaceChild(newInstance.dom, instance.dom);
    return newInstance;
  }
}

Children Reconciliation

如今校验历程还没有对子元素举行处置惩罚。针对子元素的校验是React中的一个症结部份,这一历程须要元素的一个分外属性key来完成,假如某个元素在新旧假造DOM上的key值雷同,则示意该元素没有发生变化,直接重用即可。在当前版本的代码中我们会遍历instance.childInstanceselement.props.children,并对统一位置的实例和元素举行比较,经由历程这类体式格局完成对子元素的一致性校验。这类要领的瑕玷就是,假如子元素只是调换了位置,那末对应的DOM节点将没法重用。

我们把统一实例上一次的instance.childInstances和此次对应元素的element.props.children举行递归比较,而且保留每次reconcile返回的效果以便更新childInstances

function reconcile(parentDom, instance, element){
    if(instance == null){
       const newInstance = instantiate(element);
        parentDom.appendChild(newInstance.dom);
        return newInstance;
    } else if(instance.element.type === element.type){
        updateDomProperties(instance.dom, instance.element.props, element.props);
        instance.childInstances = reconcileChildren(instance, element);
        instance.element = element;
        return instance;
    } else {
        const newInstance = instantiate(element);
        parentDom.replaceChild(newInstance.dom, instance.dom);
        return newInstance;
    }
}

function reconcileChildren(instance, element){
    const dom = instance.dom;
    const childInstances = instance.childInstances;
    const nextChildElements = element.props.children || [];
    const newChildInstances = [];
    const count = Math.max(childInstances.length, nextChildElements.length);
    for(let i = 0; i< count; i++){
        const childInstance = childInstances[i]; 
        const childElement = nextChildElements[i];//上面一行和这一行都轻易涌现空指针,稍后处置惩罚
        const newChildInstance = reconcile(dom, childInstance, childElement);
        newChildInstances.push(newChildInstance);
    }
    return newChildInstances;
}

Removing DOM nodes

假如nextChildElements数目多于childInstances,那末对子元素举行一致性校验时就轻易涌现undefined与剩下的子元素举行比较的状况。不过这不是什么大题目,由于在reconcile中的if(instance == null)会处置惩罚这类状况,而且会依据多出来的元素建立新的实例。假如childInstances的数目多于nextChildElement,那末reconcile就会收到一个undefined作为其element参数,然后在尝试猎取element.type时就会抛出毛病。

涌现这个毛病是由于我们没有斟酌DOM节点须要移除的状况。所以接下来我们要做两件事变,一个是在reconcile中增添增添element === null的校验,一个是在reconcileChildren中过滤掉值为nullchildInstances元素。

function reconcile(parentDom, instance, element){
    if(instance == null){
        const newInstance = instantiate(element);
        parentDom.appendChild(newInstance.dom);
        return Instance;
    } else if(element == null){
        parentDom.removeChild(instance.dom);
        return null; // 注重这处所返回null了
    } else if(instance.element.type === element.type){
        updateDomProperties(instance.dom, instance.element.props, element.props);
        instance.childInstances = reconcileChildren(instance, element);
        instance.element = element;
        return instance;
    } else {
        const newInstance = instantiate(element);
        parentDom.replaceChild(newInstance.dom, instance.dom);
        return newInstance;
    }
}

function reconcileChildren(instance, element){
    const dom = instance.dom;
    const childInstances = instance.childInstances;
    const nextChildElements = element.props.children || [];
    const newChildInstances = [];
    const count = Math.max(childInstances.length, nextChildElements.length);
    for(let i = 0; i < count; i++){
        const childInstance = childInstances[i];
        const childElement = nextChildElements[i];
        const newChildInstances = reconcile(dom, childInstance, childElement);
        newChildInstances.push(newChildInstance);
    }
    return newChildInstances.filter(instance => instance != null)
}

Summary

这一节,我们为Didact增添了更新DOM的功用。我们经由历程重用节点,避免了频仍的建立和移除DOM节点,提高了Didact的工作效率。重用节点另有肯定的优点,比方保留了DOM的位置或许核心等一些内部状况信息。

如今我们是在根元素上挪用render要领的,每次有变化时也是针对整棵元素树举行的一致性校验。下一节我们将引见组件。有了组件我们就能够只针对有变化的那一部份子树举行一致性校验。

    原文作者:许三多
    原文地址: https://segmentfault.com/a/1190000017321684
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞