import React from react作用

因为,babel会将jsx代码转换

  • 转换前
import React, { Component } from 'react';
class Process extends Component {
  render() {
    return (
哈哈哈
) } }
  • 转换后

import React, { Component } from 'react';

class Process extends Component {
  render() {
      // 用到了React.createElement方法
    return React.createElement(
      'div',
      null,
      '\u54C8\u54C8\u54C8'
    );
  }
}

了解React.createElement

组件通过

React.createElement(type,props,children)
  • 为什么代码没有使用react相关方法,也要在文件顶部import react
import React from "react";
const App = () => (
  
Hello World!!!
); export default App;
  • 原因:通过babel会将jsx编译成普通js代码(如下)会用到React.createElement,所以需要React
var App = function App() {
  return React.createElement(
    "div",
    {className:'test'}
    "Hello World!!!"
  );
};

定义的组件如何变成DOM ?

  • 先看看index.jsx
import React from 'react' 
ReactDOM.render(
    ,
    document.getElementById('root')
);

Hello.jsx

class Hello extends React.Component{
    render(){
        return (
            
father child child1
) } }

Hello.jsx被babel编译后会变成

React.createElement(
    "div", 
    {
        id: "father"
    }, 
    "father", 
    React.createElement("span", null, "child"), React.createElement(
        "a", 
        {
            href: "/bar"
        }, 
        "child1"
    )
);

// 注意这里经过babel编译后用到了React.createElement
// 这就解释了为什么每个组件里面都要import React却发现并没有调用他
  • 看看React.createElemtn方法
const React = {
    createElement(type, props, children) {
        const element = {
            type,
            props: props || {}
        };

        if (children) {
            element.props.children = children;
        }

        return element;
    },
}

// 可以发现,createElemtn(type, props, children)接受三个参数
// 最终这个方法返回一个element形式的对象
// 可是上面的jsx代码通过babel转换后不只三个参数?


  • 分析React.createElement源码会进行如下处理
// 排除调type和config两个属性
// 将第二参数后的所有属性看作子元素赋给prop.children
 var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
    // 如果不止一个child,合成一个数组后赋值给prop.children
  } else if (childrenLength > 1) {
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    ....
    props.children = childArray;
  }


  • 所以Hello.jsx被编译后不止一个child最终会变成如下
const element = {
    type: "div",
    props: {
        id: "container",
        children: [
            {
                type: "span",
                props: {
                    children: [
                        {
                            type: "TEXT ELEMENT",
                            props: { nodeValue: "Foo" }
                        }
                    ]
                }
            },
            {
                type: "a",
                props: {
                    href: "/bar", children: [
                        {
                            type: "TEXT ELEMENT",
                            props: { nodeValue: "bar" }
                        }
                    ]
                },
            }
        ]
    }
};

所以,最后组件都会在经过Babel转换和React.createElement后变成一个 Element对象

element = {
    type:'div',
    props:{
        id:'xx',
        children:[
            {
                ....
            },
            {
                ....
            },
        ]
    }
}

最后在在index.jsx中

ReactDOM.render(element,container)

通过render 方法将element转换成我们的实际DOM

render如何将element转换成实际DOM的?

下面内容基本都是参考如下博客内容:
https://foio.github.io/react-implement/

  • render方法

//表示根节点的instance
let rootInstance = null;

function render(element, container) {
  const prevInstance = rootInstance; //虚拟DOM的根节点
  const nextInstance = reconcile(container, prevInstance, element); //对比DOM diff,并更新真实DOM
  rootInstance = nextInstance; // 新的虚拟DOM根节点
}


上面render方法主要作用就是调用reconcile方法(这里是diff算法的具体实现地方)

  • reconcile方法
    这里重要的是instantiate方法,讲element转换成真正的dom
    
function reconcile(parentDom, instance, element) {
  if (instance == null) {
    //虚拟的根节点为空时,使用当前React元素,创建新的虚拟DOM
    const newInstance = instantiate(element); 
    //将真实DOM插入容器
    parentDom.appendChild(newInstance.dom); 
    return newInstance;
  }else if(element == null){
    //删除DOM
    parentDom.removeChild(instance.dom);
    return null;
  } if (instance.element.type === element.type) {
    //原有虚拟DOM节点类型与要创建的DOM节点类型一致时,可以重用dom以提升性能,只需要更新dom节点属性
    updateDomProperties(instance.dom, instance.element.props, element.props);
    //对instance子节点进行对比,以保证尽可能的重用DOM
    instance.childInstances = reconcileChildren(instance, element);
    instance.element = element;
    return instance;
  } else {
    //使用当前React元素,创建新的虚拟DOM
    const newInstance = instantiate(element);
    //将真实DOM替换容器中的原有DOM
    parentDom.replaceChild(newInstance.dom, instance.dom);
    return newInstance;
  }
}

  • 这里说说instance
这里的instance就是所谓的虚拟DOM,每个真实的DOM都有一个虚拟DOM(instance)与之匹配

这样在操作DOM变化的时候,对比新老element的变化,得到最终需要改变的DOM
再去跟新真实的DOM,达到最小化改变DOM


  • reconcileChildren函数
    在真实的React实现中,需要元素提空额外属性(key)来匹配,以提升diff的效率。本文我们实现一个简版的diff算法,只比较children数组中相同位置的子节点。

function reconcileChildren(instance, element) {
  // 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];
    //调用reconcile创建子节点的虚拟DOM
    /*这里存在三种情况:
     * (1). childInstance和childElement都存在,则调用reconcile进行diff操作
     * (2). childInstance为空而childElement存在,调用调用reconcile创建新的instance
     * (3). childInstance存在而childElement为空,则调用reconcile进行删除操作,此时会返回null
     */
    const newChildInstance = reconcile(dom, childInstance, childElement);
    newChildInstances.push(newChildInstance);
  }
   return newChildInstances.filter(instance => instance != null); //过滤null
}


  • instantiate
    存在与reconcile方法中,作用将element更新为真实的DOM

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

  const isTextElement = type === "TEXT ELEMENT";
  const dom = isTextElement
    ? document.createTextNode("")
    : document.createElement(type);

  updateDomProperties(dom, [], props); //更新DOM节点的属性、绑定事件

  //递归地调用instantiate函数,创建虚拟DOM的子节点
  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;
}

  • updateDomProperties函数
    调用浏览器的DOM api对DOM修改,首先从dom节点中删除所有旧属性,然后添加所有新属性。
function updateDomProperties(dom, prevProps, nextProps) {

  //判断是否为事件属性
  const isEvent = name => name.startsWith("on");
  //判断是否为普通属性
  const isAttribute = name => !isEvent(name) && name != "children";


  //移除原有DOM节点上绑定的事件
  Object.keys(prevProps).filter(isEvent).forEach(name => {
    const eventType = name.toLowerCase().substring(2);
    dom.removeEventListener(eventType, prevProps[name]);
  });

  //移除原有DOM节点的普通属性
  Object.keys(prevProps).filter(isAttribute).forEach(name => {
    dom[name] = null;
  });

  //添加新属性
  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]);
  });
}


小总结

  • 1、babel将jsx转换成(type,props,child1,child2…),并调用createELement
  • 2 、React.createELement(type,props,[…child])变成element
element = {
    type:xx,
    props:{
        id:xx,
        child:{
            type:xx
            props:xx
        }
    }
}


  • ReactDOM.render(element,container)变成真正的DOM
这里涉及到:
// render主要主要就是嗲用reconcile
render: reconcile
// reconcile主要通过diff算法减少更新dom
reconcile: instantiate,reconcileChildren
// instantiate将element变成真正的DOM
instantiate: updateDomProperties
// updateDomProperties更新DOM属性,绑定事件


Component组件

上面我们实现了基本的虚拟DOM比较,能够在一定程度上复用DOM,但仍然有如下问题

(1)每次更改都会触发整棵虚拟DOM树的比较

(2)需要显式的调用render函数,来渲染最新状态

(3)不支持自定义组件


为了解决上面缺点,于是有了Component,于是我们将原来的instance的三个属性新增了publicInstance,表示对Component实例的引用;

而Component实例内部又有一个对它自身Instance的引用:internalInstance。

internalInstance: 作用是为了在组件内部拿到子虚拟DOM的根节点,从而进行子树的diff操作,避免整个虚拟DOM进行diff操作。

  • 开始instance
instance = {
    element: {},            //React元素的引用
    dom: {},                //真实浏览器DOM的引用
    childInstances: []      //子节点的Instance引用
}


  • 优化有组件的instance
instance = {
    element: {},            //React元素的引用
    dom: {},                //真实浏览器DOM的引用
    childInstances: [],       //子节点的Instance引用
    publicInstance: {  //表示Component的引用
    //对自身instance的引用
    // 用于在setState时通过组件this拿到instance以触发更新
      __internalInstance: {} 
  }
}


引入Component后, React的element可能是一个自定义的组件, 我们需要一个用于创建自定义组件Component实例(publicInstance)的方法:

  • createPublicInstance实例化自定义组件
function createPublicInstance(element, internalInstance) {
  // 当元素进到这里来, 说明type是个自定义element的构造函数
  const { type, props } = element;
  // 调用组件的构造函数,创建组件实例
  const publicInstance = new type(props);
  // 自定义组件对应的Instance引用, 用于在实例中通过this.__internalInstance获取组件对应的instance,以更新组件
  publicInstance.__internalInstance = internalInstance; 
  return publicInstance;
}


组件为什么能通过setstate更新组件本身?

关键: 组件内部获取该组件实例,然后调用reconcile方法

所有的React组件都需要继承一个基类Component,该基类有一个构造函数和setState方法,其中setState方法会触发组件的更新

class Component {
  constructor(props) {
    this.props = props;
    this.state = this.state || {};
  }

  //用于更新组件的内部状态
  setState(partialState) {
    this.state = Object.assign({}, this.state, partialState);
    // 通过this.__internalInstance获取Component对应的instance,并通过updateInstance进行更新
    updateInstance(this.__internalInstance);
  }
}

function updateInstance(internalInstance) {
  //internalInstance是自定义组件对应的instance {element, dom, childInstances}
  const parentDom = internalInstance.dom.parentNode;
  const element = internalInstance.element;
  //调用reconile函数,进行虚拟DOM比较,并更新DOM树
  reconcile(parentDom, internalInstance, element);
}


更新reconcile方法,新增了当element是组件时的更新

unction reconcile(parentDom, instance, element) {
  if (instance == null) {
    //虚拟的根节点为空时,使用当前React元素,创建新的虚拟DOM
    const newInstance = instantiate(element); 
    //将真实DOM插入容器
    parentDom.appendChild(newInstance.dom); 
    return newInstance;
  }else if(element == null){
    //删除DOM
    parentDom.removeChild(instance.dom);
    return null;
  }  else if(instance.element.type !== element.type){
    //使用当前React元素,创建新的虚拟DOM
    const newInstance = instantiate(element);
    //将真实DOM替换容器中的原有DOM
    parentDom.replaceChild(newInstance.dom, instance.dom);
    return newInstance;
  } else if (typeof element.type === 'string') { 
    //原有虚拟DOM节点类型与要创建的DOM节点类型一致且为原生类型(非自定义类型),可以重用dom以提升性能,只需要更新dom节点属性
    updateDomProperties(instance.dom, instance.element.props, element.props);
    //对instance子节点进行对比,以保证尽可能的重用DOM
    instance.childInstances = reconcileChildren(instance, element);
    instance.element = element;
    return instance;
  } else {
    //逻辑到之有两个条件
    //(1)原有虚拟DOM节点类型与要创建的DOM节点类型一致
    //(2)element type为自定义类型,其中publicInstance是自定义组件的实例
    //更新自定义组件的属性
    instance.publicInstance.props = element.props;
    //原有孩子节点instance数组
    const oldChildInstance = instance.childInstance;
    //调用自定义的render函数,创建自定义组件的孩子节点element
    const childElement = instance.publicInstance.render(); // 组件的render函数 
    //对比自定义组件的虚拟DOM,更新DOM
    const childInstance = reconcile(parentDom, oldChildInstance, childElement);
    //更新instance引用
    instance.dom = childInstance.dom;
    instance.childInstance = childInstance;
    instance.element = element;
    return instance; 
  }
}


nstantiate函数的作用是将element转成为instance结构,它也需要额外的修改以支持自定义组件。在instantiate函数中,对于DOM类型的组件,我们需要调用document.createElement,而 对于自定义的组件,则需要调用createPublicInstance。

function instantiate(element) {
  const { type, props } = element;
  const isDomElement = typeof type === 'string';

  if(isDomElement) {
    const isTextElement = type === "TEXT ELEMENT";
    const dom = isTextElement
      ? document.createTextNode("")
      : document.createElement(type);

    updateDomProperties(dom, [], props); //更新DOM节点的属性、绑定事件

    //递归地调用instantiate函数,创建虚拟DOM的子节点
    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;
  }else {
    const instance = {};
    //对于自定义类组件,创建对应的publicInstance
    const publicInstance = createPublicInstance(element, instance);
    //调用自定义组件的render方法,获取child element
    const childElement = publicInstance.render(); 
    //创建child element的instance
    const childInstance = instantiate(childElement); // 递归 孩子拿到 { dom, element, childInstances }
    const dom = childInstance.dom;
    //返回自定义类型组件的instance,其中publicInstance为自定义组件的实例
    //自定义组件的instance有几个特殊的地方:
    //(1) childInstance不是数组,而是自定义组件的根节点对应的instance
    //(2) dom是自定义组件的根节点对应的DOM
    //(3) publicInstance是自定义组件类实例,内部维护着__internalInstance指向instance
    Object.assign(instance, { dom, element, childInstance, publicInstance });
    return instance;
  }
}


你可能感兴趣的:(js)