React的整个生命周期过程

  1. 在 React 代码执行之前,Babel 会将 JSX 转为React.createElement的方法调用. 如下
    <div className="container">
      <h3>Hello React</h3>
      <p>React is great </p>
    </div>
    
    React.createElement(
      "div",
      {
        className: "container"
      },
      React.createElement("h3", null, "Hello React"),
      React.createElement("p", null, "React is great")
    );
    
  2. React.createElement将JSX转换成一个virtualDOM.树中有children props type 属性
    export default 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
      }
    }
    
  3. 然后调用React.render去进行渲染.它的第一个参数就是要渲染的组件,它会被转为virtualDOM. 第二个参数是根节点,渲染的过程就是diff的过程.
    React.render(<KeyDemo />, root)
    
    export default function render(
      virtualDOM,
      container,
      oldDOM = container.firstChild
    ) {
      diff(virtualDOM, container, oldDOM)
    }
    
  • ①diff对比开始,判断 oldDOM 是否存在(旧节点就是root根节点的第一个子节点root.firstChild)
    • 如果diff的时候oldDOM不存在,那么根据virtualDOM.type判断是组件还是标签,并分别进行渲染
      • ②如果type是函数那说明是组件,通过判断它原型上是否有render属性来区分是函数式组件还是类组件
        • 如果是函数式组件,调用该函数并传入props参数,得到一个返回的virtualDOM
        • 如果是类组件,用new实例化它并传入props参数,然后执行render函数,同样得到一个返回的virtualDOM并缓存到该virtualDOM的component中. 同时缓存在当前作用域中的component中.
          • 这里要注意的是类组件继承了React.Component ,子类会继承父类的所有方法
          • 在React.Component中去接收props并且定义了setState函数,setDOM函数,getDOM函数,updateProps函数以及类组件的所有声明周期. setState的过程其实就是渲染的过程也就是diff对比的过程
            setState(state) {
              this.state = Object.assign({}, this.state, state)
              // 获取最新的要渲染的 virtualDOM 对象
              let virtualDOM = this.render()
              // 获取旧的 virtualDOM 对象 进行比对
              let oldDOM = this.getDOM()
              // 获取容器
              let container = oldDOM.parentNode
              // 实现对象
              diff(virtualDOM, container, oldDOM)
            }
          
        • 得到新的virtualDOM之后,又去判断是组件还是标签,如果是组件那递归第②步,否则的话进行dom的操作,也就是进入第③步
        • 所有的组件最后都是由html标签组成,递归的过程就是一个洋葱代码,当到最深一层之后,再一层层往上走,这个往上走的过程其实就是创建dom到对应容器container的过程,创建完成之后去判断缓存的component是否存在,如果存在说明是类组件,调用componentDidMount生命周期(已经挂载).并判断类组件中props是否存在ref属性,存在的话执行component.props.ref(component),洋葱代码向上执行.
      • ③如果是html标签
        • 首先根据type创建dom
          • 判断是文本节点还是元素节点,如果是文本节点用newElement = document.createTextNode(virtualDOM.props.textContent)创建,如果是元素节点用 newElement = document.createElement(virtualDOM.type)创建
          • 并将当前虚拟dom缓存在newElement._virtualDOM = virtualDOM
          • 如果virtualDOM还有子结点children,那么遍历这个children,拿到子结点的virtualDOM又去判断是组件还是标签,如果是组件那递归第②步,否则的话进入第③步对dom的操作
          • 知道以上循环结束之后,去判断virtualDOM的props中是否存在ref属性,有的话调用它virtualDOM.props.ref(newElement)
          • 然后返回newElement
        • 拿到返回的newElement之后,就可以放到对应的地方了
          • 如果oldDom存在
            • 那插入到旧的dom之前 container.insertBefore(newElement, oldDOM)
            • ④ 然后调用unmountNode(oldDom)进行删除旧节点操作
              • type为"text"的文本节点可以直接删除 oldDom.remove()
              • 看一下节点是否是由组件生成的,其实就是判断virtualDOM.component是否存在
                • 如果 component 存在 就说明节点是由组件生成的,调用声明周期 component.componentWillUnmount()
              • 看一下节点身上是否有ref属性,如果有调用virtualDOM.props.ref(null)
              • 遍历Object.keys(virtualDOM.props)看一下节点的属性中是否有’on’开头的事件属性
                • 如果有删除他们oldDom.removeEventListener(eventName, eventHandler)
              • 最后看oldDOM有没有childNodes,有的话遍历它并且进入深度递归执行步骤④
              • 递归执行到最里层之后,往上逐步删除节点oldDom.remove()
          • 如果oldDom不存在,那么添加子节点container.appendChild(newElement)
        • 如果有virtualDOM.component说明是类组件,那么将DOM对象存储在类组件实例对象中component.setDOM(newElement)用于类组件setStae获取旧的 virtualDOM 对象 进行比对
    • 如果diff的时候oldDOM存在
      • 如果不是组件且新旧节点type不同,那么不需要对比,直接进入第③步骤得到一个newElement并替换旧的dom对象oldDOM.parentNode.replaceChild(newElement, oldDOM)
      • 如果typeof virtualDOM.type === "function"是组件,那么进入组件的diff对比
        • 判断新旧类组件是否是同一个,如果是那么类组件已经被创建好了 直接更新就行virtualDOM.type === oldComponent.constructor,做类组件更新操作,涉及声明周期和diff对比
          • 调用oldComponent.componentWillReceiveProps(virtualDOM.props) 执行生命周期(当props发生变化时也就是组件更新时)
          • 判断oldComponent.shouldComponentUpdate(virtualDOM.props)是否为真(指定 React 是否应该继续渲染,默认值是 true)
          • 为真的话调用oldComponent.componentWillUpdate(virtualDOM.props)生命周期(指定组件将要更新,这个时候给的是新的props,但是组件中的this.props还是旧的)
          • 更新组件中的propsoldComponent.updateProps(virtualDOM.props)
          • 调用render函数获取组件返回的最新的 virtualDOM nextVirtualDOM = oldComponent.render()
          • 更新 component 组件实例对象 nextVirtualDOM.component = oldComponent
          • 此时拿到了新旧节点可以进行diff对比了,也就是递归第①步进行对比diff(nextVirtualDOM, container, oldDOM)
          • 对比完成之后,说明当前组件更新完成调用oldComponent.componentDidUpdate(prevProps)生命周期
        • 如果不是类组件,根据virtualDOM.type判断是组件还是标签,并分别进行渲染(②③步)
      • 如果不是组件且旧节点存在且新旧节点的type相同,那么进入html的节点对比
        • 如果是文本节点virtualDOM.type === "text",更新内容
          • 如果新旧文本内容不同那么更新内容oldDOM.textContent = virtualDOM.props.textContent.并且保存虚拟domoldDOM._virtualDOM = virtualDOM
        • 如果是元素节点,更新元素
          • 获取新旧节点对应的属性对象
          • 先遍历新节点的属性对象,以新节点的属性为基准作对比Object.keys(newProps).forEach(propName)=>{...}
            • 根据propName获取新旧节点的属性值,属性值全等不做操作.属性值不等的话继续执行
              • 判断属性是否是否事件属性 onClick -> click propName.slice(0, 2) === "on"
                • 获取事件名称const eventName = propName.toLowerCase().slice(2)
                • 为元素添加事件 newElement.addEventListener(eventName, newPropsValue)
                • 如果有原有的事件,删除原有的事件的事件处理函数 newElement.removeEventListener(eventName, oldPropsValue)
              • 判断是否有表单元素propName === "value" || propName === "checked"
                • 如果有就直接设置它 newElement[propName] = newPropsValue
              • 判断其它不为children的情况 propName !== "children"
                • 如果是classNamepropName === "className"
                  • 创建它newElement.setAttribute("class", newPropsValue)
                • 如果是其它属性
                  • 直接定义它 newElement.setAttribute(propName, newPropsValue)
          • 再遍历旧节点的属性对象,判断属性被删除的情况Object.keys(oldProps).forEach(propName)=>{...}
            • 根据propName获取新节点的属性值,如果新节点属性值不存在,那么说明属性被删除了,然后分别作判断
              • 判断属性是否是否事件属性 onClick -> click propName.slice(0, 2) === "on"
                • 获取事件名称const eventName = propName.toLowerCase().slice(2)
                • 删除原有的事件的事件处理函数 newElement.removeEventListener(eventName, oldPropsValue)
              • 判断其它不为children的情况 propName !== "children"
                • 删除属性 newElement.removeAttribute(propName)
        • 更新完当前dom节点之后,开始对它的子结点进行更新
          • 遍历oldDOM的childNodes子结点将拥有key属性的子元素放置在一个单独的对象中keyedElements
                let keyedElements = {}
            	for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
            	      let domElement = oldDOM.childNodes[i]
            	      if (domElement.nodeType === 1) {
            	        let key = domElement.getAttribute("key")
            	        if (key) {
            	          keyedElements[key] = domElement
            	        }
            	      }
            	   }```
            
          • 如果keyedElements为空let hasNoKey = Object.keys(keyedElements).length === 0
            • 循环当前虚拟dom的子元素,然后通过索引值直接取旧的虚拟dom中拿值对比子节点
            	virtualDOM.children.forEach((child, i) => {
            		diff(child, oldDOM, oldDOM.childNodes[i])
            	})
            
        • 如果keyedElements不为空,先循环 virtualDOM 的子元素 获取子元素的 key 属性,并去keyedElements中去进行匹配
          - 如果匹配到了看看当前位置的元素是不是期望的元素,如果是的话就不做操作了,如果不是就在旧节点前插入新节点.
          - 如果没匹配到那么根据virtualDOM.type判断是组件还是标签,并分别进行渲染.执行③④步
          				      virtualDOM.children.forEach((child, i) => {
          				        let key = child.props.key
          				        if (key) {
          				          let domElement = keyedElements[key]
          				          if (domElement) {
          				            // 3. 看看当前位置的元素是不是我们期望的元素
          				            if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
          				              oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
          				            }
          				          } else {
          				            // 新增元素
          				            mountElement(child, oldDOM, oldDOM.childNodes[i])
          				          }
          				        }
          				      })
          
          • 如果旧节点要比新节点多那么要做多余节点删除操作
            • 如果没有key, 从后往前删,直到新旧节点长度一致
            • 如果有key,用两个for循环,先遍历旧节点oldChildNodes,再遍历新节点virtualDOM.children,然后拿旧节点的key去新节点里面找,如果找到了就不作操作,如果没找到就执行步骤④去递归删除该元素与其子元素
                // 获取旧节点
                let oldChildNodes = oldDOM.childNodes
                // 判断旧节点的数量
                if (oldChildNodes.length > virtualDOM.children.length) {
                  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)
                      }
                    }
                  }
                }
            
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值