react核心知识原理整理(react16/17)

1,212 阅读13分钟

参考16.x的版本

2个包的作用

  • import React from './react';
  • import ReactDOM from './react-dom';
  • 每次写的时候 都会默认引入这两个包,ReactDOM这个是主要提供了render方法,吧react元素挂载到页面中。React则是核心的包,大部分react api 都源于他,比如createElement Component createRef createContext 等

jsx

  • jsx:把一种js和html混合的语法,jsx是语法糖 会通过babel转义成 React.createElement语法(16版本之前 React 必须要引入就这个原因)
  • jsx 语法通过 babel转译成js babel-loader @babel/core @babel/preset-react(预设,他负责把html标签转成js代码)
<div>123</div>
babel 转义后 ===>
React.createElement("div", null, "123");

createElement

  • React.createElement将编译后的jsx语法创建组件,接收三个参数
    • 第一个是组件或者标签
    • 第二个是标签的配置对象id、style等
    • 第三个是及其后面的都是children,可能是一个对象,也可能是一个数组
  • 静态属性 isReactComponent 是用来用区别函数组件和类组件,在render 里面用到的
  • 伪代码
function createElement(type,config,children){
  let propName;
  const props = {};
  for(propName in config){
    props[propName] = config[propName]
  }
  const childrenLength = arguments.length - 2//看看有几个儿子
  if(childrenLength === 1){
    props.children = children;// props.children就是一个普通的对象
  }else if(childrenLength >1){// 如果说儿子数量大于1的话 props.children就是一个数组
    props.children = Array.from(arguments).slice(2);
  }
  return {type,props}
}
class Component{
  static isReactComponent = true
  constructor(props){
    this.props = props
  }
}
export default {
  createElement,
  Component
}

render

  • render函数就是将 React.createElement 创建的虚拟DOM转化成真实DOM挂载到第二个参数上。核心就就是createDOM函数
  • createDOM会根据传进的元素进行分类普通文本 class 原生DOM function根据不同的类型分别进行处理,最后都会返回处理后的新DOM进行返回,然后挂载到root上。对于react多数组,同一级别的元素react都会进行打平操作
// 打平 最后会打平成这个样子
{[1],[2],[3,[4]]} ==={[1,2,3,4]}

渲染

  • 主要解析要渲染的对象:原生js class fnction
  • React.createElement用来创建虚拟DOM, 他主要对元素进行分类,分成原生js class fnction,最后包装成一个对象返回,如果儿子节点是多层的情况 还会进行递归处理。
import React from './react';
import ReactDOM from './react-dom';
function FunctionCounter(props){
  return React.createElement('div', {id: 'counter'}, 'hello2','123');
}
class ClassComponent extends React.Component{
  render() {
    return React.createElement(FunctionCounter, {id: 'counter'}, 'hello1');
  }
}
ReactDOM.render(
  elm,
  document.getElementById('root')
);

babel 编译后的样子

let onClick1 = (event) => {
  console.log('onclick1',event);
}
let onClick = (event) => {
  console.log('onclick', event);
  event.persist();
  setInterval(()=>{
    console.log('-----------------------------------')
  },1000) 
}
// js里面变量名基本上是驼峰
//  写法
let elm = React.createElement('div',
  {
   id:'sayHello',
   onClick,
   style:{
     with:'100px',
     height:'100px',
     border:'1px solid red'
   }
  },
  'div',
  React.createElement('section', {
      onClick:onClick1,
      style: {
        color: 'red',
        with:'50px',
        height:'50px',
        border:'1px solid green'
      }},
      'section')  
)

合成事件

  • 1、因为合成事件可以屏蔽浏览器的差异,不同浏览器绑定事件和触发事件的方法不一样
  • 2、合成阔以实现事件对象的复用,重用,减少垃圾回收,提高性能
  • 3、因为默认我要实现批量更新 setState, setState 两个SetState 合并成一次更新,这个也是合成事件中实现的

事件流程,所有的事件都是冒泡到 document,进行统一管理

  • 比如 click 事件触发的时候,他会从目标源到document进行遍历,获取每一个需要处理的元素绑定了click事件函数,进行触发
  • event persist 作用是event事件持久化,默认情况下 合成event 在函数执行完成后, 合成event 里面的数据会被指向null。persist作用是让当前的合成的event继续存在,实现就是在当前函数执行的时候执行 event.persist(),他会改变内部 合成event 指向,等待事件遍历完成,去清除合成event时候,只是处理改变后的,之前的合成event并没有被清除
  • 处理批量更新,在处理事件的之前就开始 批量更新模式,再去处理函数里面的setState,等所有的事件函数都执行完成后,在关闭批量更新模式,对页面更新

setState

  • 里面有个变量控制 批量更新,默认是开启批量更新。
  • 在事件处理中,同步情况下,每次调用setState他都会把状态保存起来,然后尝试类组件更新。
    • 如果当前还是处于批量更新的情况,把自己放到更新队列中。多次调用情况下, 会一直走这里 请数据保存起来
    • 如果当前不是批量更新,那么就去更新,去执行 shouldUpdate更新钩子逻辑,在对组件进行forceUpdate强制更新
  • 类组件 阔以调用forceUpdate 对组件 进行强制更新。
  • import { unstable_batchedUpdates } from './react-dom' 强制更新
    • unstable_batchedUpdates 逻辑很简单,强制开始批量更新,执行传递进来的逻辑,在关闭批量更新,调用队列更新,去更新组件

forceUpdate

  • 每次setState执行完成组件尝试去更新,首先判断shouldUpdate钩子是否支持,如果支持才调用forceUpdate执行强制更新。另外我们可以通过类组件直接调用forceUpdate进行强制更新
  • 在这里 会走componentWillMount & getSnapshotBeforeUpdate & render & componentDidUpdate 钩子一次执行, render之后, 拿新老dom比较更新

diff

  • 1、新的元素没有、type不一样、文本不一样直接进行替换
  • 2、如果都是类和函数组件,他们会再次进入循环体进行dom比较
  • 3、核心在两个type相同的native元素进行比较 以下面例子比较 A B C DA C B E F
    • 下面有2点 深度优先 第一点是 diffQueue 收集,对需要操作的dom进行搜集
    • 第二点 patch 对dom进行统一处理
// 1、patch 打印diffQueue 需要操作的dom
//     [
//       {
//         "parentNode": {
//           "eventStore": {}
//         },
//         "type": "MOVE",
//         "fromIndex": 1,
//         "toIndex": 2
//       },
//       {
//         "parentNode": {
//           "eventStore": {}
//         },
//         "type": "INSERT",
//         "toIndex": 3,
//         "dom": {}
//       },
//       {
//         "parentNode": {
//           "eventStore": {}
//         },
//         "type": "INSERT",
//         "toIndex": 4,
//         "dom": {}
//       },
//       {
//         "parentNode": {
//           "eventStore": {}
//         },
//         "type": "REMOVE",
//         "fromIndex": 3
//       }
//     ]
// 2、下面是具体的4个变化的dom
//       a、首先匹配A情况  都是一样的直接赋用老元素
//       b、查看新元素里面的C,在老元素有对应的 并且赋用老元素的位子lastIndex(2)大于当前挂载的位子mountIndex(1),即同样不用操作
//       c、查看B元素,去老的元素找到同样的,但是他的mountIndex(1)小于lastIndex(2),即需要操作,将他移动到lastIndex的后面, 
//           看上面 patch 第一个参数 type 即为MOVE,fromIndex 代表最后要落下的位子(用新元素的mountIndex即可), toIndex(代表当前老元素的位子)
//       d、查看E,老元素里面没有 那就插入,patch 第二个参数 type 为INSERT, toIndex 表示要插入到那儿(用新元素的mountIndex 表示)
//       e、F同E
//       f、新元素遍历完成,去遍历老元素 删除新元素内没有用到的,即D 被删除掉 fromIndex代表当前老元素的位子
//     MOVE {$$typeof: Symbol(ELEMENT), type: "li", key: "B", ref: undefined, props: {…}, …}
//     INSERT {$$typeof: Symbol(ELEMENT), type: "li", key: "E", ref: undefined, props: {…}}
//     INSERT {$$typeof: Symbol(ELEMENT), type: "li", key: "F", ref: undefined, props: {…}}
//     REMOVE {$$typeof: Symbol(ELEMENT), type: "li", key: "D", ref: undefined, props: {…}, …}
//    3、深度优先 搜集 diffQueue 对dom进行统一处理
//       a、 MOVE 的操作是 先删除 在插入,先将 REMOVE 和 MOVE 统一删除
//       b、 在将 INSERT 和 MOVE 统一插入节点操作
    
class ClassComponent extends React.Component{
  constructor(props){
    super(props);
    this.state = {
      show: true
    }
  }
  
  handleClick = () => {
    this.setState(state => ({show: !state.show}));
  }
  render() {
    if(this.state.show){
      return (
        <ul onClick={this.handleClick}>
          <li key='A'>A</li>
          <li key='B'>B</li>
          <li key='C'>C</li>
          <li key='D'>D</li>
        </ul>
      )
    }else {
      return (
        <ul onClick={this.handleClick}>
          <li key='A'>A</li>
          <li key='C'>C</li>
          <li key='B'>B</li>
          <li key='E'>E</li>
          <li key='F'>F</li>
        </ul>
      )
    }
  }
}

life cycle

初始阶段

  • 组建实例化 会执行constructor => getDerivedStateFromProps => render => componentDidMount

更新阶段

  • 接受新的数据 getDerivedStateFromProps => shouldComponentUpdate => render => 获取快照getSnapshotBeforeUpdate 这个的返回值会传给 componentDidUpdate 第三个参数

销毁阶段

  • componentWillUnMount 这个是发生在diff的时候 如果比较dom diff 元素不见了 就会卸载组件

老生命周期有2个被删除了~

  • componentWillMount &componentWillUpdate
  • 使用不当 会造成死循环,他阔以拿到this 修改父组件的数据,新的getDerivedStateFromProps方法替代 他是一个函数没法获取到this

context

  • context用法很简单
    • createContext返回两个对象Provider组件注册数据,Consumer接受回调拿数据
    • 类组件中,获取可以通过static contextType = ThemeContext 内部在解析组件的时候 会判断是否有这个存在 如果有的话,会把Provider传递的数据挂在当前实例上context
let ThemeContext = React.createContext(null);
// ....父组件
<ThemeContext.Provider value={{}}>
    <div>
    </div>
</ThemeContext.Provider>

<ThemeContext.Consumer>
    {
        (value) => (
            <div>
              {value}
            </div>
        )
    }
</ThemeContext.Consumer>
// 解析类组件
 if(oldElement.type.contextType){
        componentInstance.context = oldElement.type.contextType.Provider.value;
    }
function createContext(defaultValue){
    Provider.value = defaultValue;// context会复制一个初始值
    function Provider(props){
        Provider.value = props.value;// 每次Provider重新更新时候 也会重新赋值
        return props.children;
    }
    function Consumer(props){
        return onlyOne(props.children)(Provider.value)
    }
    return {Provider, Consumer}
}

fiber(17+)

屏幕刷新率

  • 大多数设备的屏幕都是 60次/秒,页面是一帧绘制出来,当每秒绘制的帧数(FPS)到达60时,页面是流程的,小于这个值,用户会感觉到卡段
  • 每个帧的预算事件是16.66毫秒(1秒/60),1s 60帧,所以每一帧分到的事件是 1000/60 = 16ms,所以我们的代码力求不让义诊的工作量超过16ms

  • 每个帧的开头包括样式计算,布局和绘制
  • js执行的js引擎和页面渲染引擎在同一个渲染现成,GUI渲染和js执行是互斥
  • 如果某个任务执行时间过长 浏览器会推迟渲染
  • 图片若显示过小 在新网页中看

lifeofframe.jpg

rAf(requestAnimationFrame)

  • requestAnimationFrame回调函数会在绘制之前执行,上图中显示他执行的时候在layout前面
  • 下面的用法 当浏览器绘制前操作dom 让他的width 一直增加
<body>
  <div style="background: red;width: 0;height: 20px;"></div>
  <button>开始</button>
  <script>
    const div = document.querySelector('div')
    const button = document.querySelector('button')
    let start;
    function progress(){
      div.style.width = div.offsetWidth + 1 + 'px'
      div.innerHTML = div.offsetWidth + '%'
      if(div.offsetWidth < 100){
        let current = Date.now()
        start = current
        timer = requestAnimationFrame(progress)
      }
    }
    button.onclick = function(){
      div.style.width = 0;
      start = Date.now();
      requestAnimationFrame(progress);
    }
  </script>
</body>

requestIdleCallback

  • requestIdleCallback 作用是 当正常帧任务完成后没超过16秒,说明时间有富余,此时就会执行requestIdleCallback里注册的响应
  • requestIdleCallback(callback,{timeout:1000}),callback接收2个参数(didTimeout,timeRemaining())
    • didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
    • timeRemaining(), 表示当前帧剩余的时间
    • timeout表示如果超过这个时间后,任务还没有执行,则强制执行,不必等待
<body>
  <script>
    // 
    function sleep(d){
      for(var t = Date.now();Date.now() - t <= d;){}
    }
    const works = [
      ()=>{
        console.log('第一个任务开始')
        sleep(20)
        console.log('第一个任务结束')
      },
      ()=>{
        console.log('第二个任务开始')
        sleep(20)
        console.log('第二个任务结束')
      },
      ()=>{
        console.log('第三个任务开始')
        sleep(20)
        console.log('第三个任务结束')
      }
    ]
    // timeout 意思是 告诉浏览器 1000ms 即使你没有空闲时间也得帮我执行, 因为我等不及了
    requestIdleCallback(workLoop,{timeout:1000})
    function workLoop(deadLine){
      // 返回值 deadLine.didTimeout 布尔值 表示任务是否超时 
      // deadLine.timeRemaining() 表示当前帧剩余的时间
      console.log('本帧剩余时间', parseInt(deadLine.timeRemaining()));
      while((deadLine.timeRemaining() > 1 || deadLine.didTimeout) && works.length>0){
        performUnitOfWork()
      }
      if(works.length>0){
        console.log(`只剩下${parseInt(deadLine.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
        requestIdleCallback(workLoop)
      }
    }

    function performUnitOfWork(){
      works.shift()()
    }
  </script>
</body>

单链表

  • 单链表是一种链式存取的数据结构
  • 链表中的数据是以节点来表示的,每个节点的构成:元素+指针(指示后继元素的存储位子),元素就是存储数据的存储单元,指针就是连接每个节点的地址
class Update{
  constructor(payload,nextUpdate){
    this.payload = payload
    this.nextUpdate = nextUpdate
  }  
}

class UpdateQueue{
  constructor(){
    this.baseState = null
    this.firstUpdate = null
    this.lastUpdate = null
  }
  enqueueUpdate(update){
    if(this.firstUpdate == null){
      this.firstUpdate = this.lastUpdate = update
    }else{
      this.lastUpdate.nextUpdate = update
      this.lastUpdate = update
    }
  }
  forceUpdate(){
      let currentState = this.baseState || {}
      let currentUpdate = this.firstUpdate
      while(currentUpdate){
        let nextState = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
        currentState = {...currentState,...nextState}
        currentUpdate = currentUpdate.nextUpdate
      }
      this.firstUpdate = this.lastUpdate = null;
      this.baseState = currentState
      return currentState
  }
}

let queue = new UpdateQueue();
queue.enqueueUpdate(new Update({name:'sg'}))
queue.enqueueUpdate(new Update({age:12}))
queue.enqueueUpdate(new Update((data)=>({age:data.age+1})))
queue.enqueueUpdate(new Update((data)=>({age:data.age+2})))
console.log(queue.forceUpdate()) ;
console.log(queue.baseState)

DOM=>fiber

  • 为什么需要fiber这种结构?

Fiber 之前的Reconcilation(协调/调和)

  • React 会递归对比VirtualDOM树,找出需要变动的节点,然后同步更新他们,这个过程叫Reconcilation
  • 在协调期间,React 会一直占用着浏览器的资源,一则会导致用户触发的事件得不到响应,二会导致卡顿

Fiber

  • 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应数据
  • 通过Fiber,让自己的 Reconcilation 过程变成可被中断。适时让出CPU执行权,可以让浏览器及时地响应用户的交互
  • Fiber也是一个执行单元,每次执行完一个执行单元,React就会检查现在还剩多少时间,如果没有时间就将控制权让出去

fiberflow.jpg

  • fiber是一个数据结构,也是一个对象。
    • React目前使用的是链表,每个VirtualDOM 节点内部表示一个Fiber
type Fiber = {
  //类型  
  type: any,
  //父节点
  return: Fiber,
  // 指向第一个子节点
  child: Fiber,
  // 指向下一个弟弟
  sibling: Fiber
}
  • 下面是通过babel编译后的DOM, 每一个节点都会转成 特定的数据结构(fiber),所以第一步是将dom全部转换成fiber。fiber有一些必备属性(type props return effectTag nextEffect等)他们记录了每个DOM信息以及DOM直接的关联
// let element = (
//     <div id='A1'>
//         <div id='B1'>
//             <div id='C1'></div>
//             <div id='C2'></div>
//         </div>
//         <div id='B2'></div>
//     </div>
// )
// console.log(JSON.stringify(element, null, 2))
let element = {
    "type": "div",
    "props": {
      "id": "A1",
      "children": [
        {
          "type": "div",
          "props": {
            "id": "B1",
            "children": [
              {
                "type": "div",
                "props": {
                  "id": "C1"
                },
              },
              {
                "type": "div",
                "props": {
                  "id": "C2"
                },
              }
            ]
          },
        },
        {
          "type": "div",
          "props": {
            "id": "B2"
          },
        }
      ]
    },
  }

  • fiber 遍历是从跟节点开始 深度优先。图可以看出关联,child指向元素的子字节,sibling 指向 元素的下一个兄弟节点,return 执行父节点。beginWork方法,实现了DOM转fiber,以及child return sibling之间的关联 fiberconstructor.jpg
  • performUnitOfWork方法对DOM进行递归遍历,原则是先找最深的第一个元素,接着找他的下一个兄弟元素, 依次寻找,兄弟元素找完成后, 找父元素 在找父元素的兄弟元素 依次循环,遍历所有节点

模拟fiber 执行过程 后面会详细分析

/*
  1、从顶点开始遍历
  2、如果有儿子,先遍历大儿子
*/
// 在浏览器执行
let A1 = {type:'div',key:'A1'}
let B1 = {type:'div',key:'B1',return:A1};
let B2 = {type:'div',key:'B2',return:A1};
let C1 = {type:'div',key:'C1',return:B1};
let C2 = {type:'div',key:'C2',return:B1};
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;

function sleep(d){
  for(var t = Date.now();Date.now() - t <= d;){}
}

let nextUnitOfWork = null;// 下一个执行单元
function workLoop(deadLine){
  while((deadLine.timeRemaining() > 1 || deadLine.didTimeout) && nextUnitOfWork){
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
  }
  if(!nextUnitOfWork){
    console.log('render阶段执行结束')
  }else{
    requestIdleCallback(workLoop,{timeout:1000})
  }
}
// 开始遍历
function performUnitOfWork(fiber){
  beginWork(fiber)//处理fiber
  if(fiber.child){// 如果有儿子 返回大儿子
    return fiber.child
  }
  // 如果没有儿子 说明此fiber已经完成了
  while(fiber){
    completeUnitOfWork(fiber)
    if(fiber.sibling){
      return fiber.sibling//如果有弟弟就返回 亲弟弟
    }
    // 在找父亲的弟弟
    fiber = fiber.return
  }
}
function completeUnitOfWork(fiber){
  console.log('结束',fiber.key)
}
function beginWork(fiber){
  sleep(20)
  console.log('开始',fiber.key)
}
nextUnitOfWork = A1
requestIdleCallback(workLoop,{timeout:1000})

react渲染流程 && fiber 三个阶段

  • 调度(scheduleRoot)、调和(Reconcilation)、提交(commitRoot)
  • 只有调和是异步的

1、调度(scheduleRoot)

fiberconstructortranverse.jpg

  • 下面是个demo模板
  • 1、element通过babel编译成jsx语法,传给render方法,render会将element包装成一个fiber结构,进行调度根节点(scheduleRoot)
import React from './react';
import ReactDOM from './react-dom';
let style = { border:'3px solid red',margin:'5px'}
let element = (
  <div id='A1' style={style}>
    A1
    <div id='B1' style={style}>
      B1
      <div id='C1' style={style}>C1</div>
      <div id='C2' style={style}>C2</div>
    </div>
    <div id='B2' style={style}>B2</div>
  </div>
)

ReactDOM.render(
  element,
  document.getElementById('root')
)
  • 2、schedule(调度整体流程)
  • 根据下面3条规则能将react串联起来
  • a、遍历的规则
    • 先儿子,后弟弟,在叔叔,
  • b、完成链规则
    • 自己所有的子节点完成后完成自己
  • c、effect规则
    • 自己所有的子节点完成后完成自己

调和(Reconcilation)

  • 调和 把虚拟DOM转成Fiber节点的过程,以及每个fiber之间的关联,收集 effectList(需要更新的Fiber)
  • 这个阶段是异步 如果没有完成 下一个时间节点 重新收集 effectList

fibereffectlistwithchild.jpg

commit阶段

  • commit阶段处理 effectList(指调和阶段需要更新的DOM),此处的流程C1开始直接到A1结束,与调和阶段是反的,最后挂到root
  • 类比Git分支功能,从旧树中fork出来一份,在新分支进行添加、删除和更新操作,经过测试后进行提交

fiberCommit.jpg

DOM-DIFF

  • 在React17+中DOM-DIFF就是根据老的fiber树和最新的JSX对比生成新的fiber树的过程

React优化原则

  • 只对同级节点进行对比,如果DOM节点跨层级移动,则React不会复用
  • 不同类型的元素会产出不同的结构 ,会销毁老结构,创建新结构
  • 可以通过key标识移动的元素

单节点

  • 如果新的子节点只有一个元素的情况,key和type不同,在调和阶段,需要把老节点标记为删除,生产新的fiber节点 标记为插入
  • 在调和阶段,需要把老节点标记为删除

多节点

  • 如果新的节点有多个节点的话,多节点的时候会经历二轮遍历
    • 第一轮遍历主要是处理节点的更新,更新包括属性和类型的更新
    一一对比,都可复用,只需更新
        <ul>
        <li key="A">A</li>
        <li key="B">B</li>
        <li key="C">C</li>
        <li key="D">D</li>
        </ul>
        /*************/
        <ul>
        <li key="A">A-new</li>
        <li key="B">B-new</li>
        <li key="C">C-new</li>
        <li key="D">D-new</li>
        </ul>
    
    一一对比,key相同,type不同,删除老的,添新的
        <ul>
        <li key="A">A</li>
        <li key="B">B</li>
        <li key="C">C</li>
        <li key="D">D</li>
        </ul>
        /*************/
        <ul>
        <div key="A">A-new</div>
        <li key="B">B-new</li>
        <li key="C">C-new</li>
        <li key="D">D-new</li>
        </ul>
        
    一一key不同退出第一轮循环
        <ul>
        <li key="A">A</li>
        <li key="B">B</li>
        <li key="C">C</li>
        <li key="D">D</li>
        </ul>
        /*************/
        <ul>
        <li key="A">A-new</li>
        <li key="C">C-new</li>
        <li key="D">D-new</li>
        <li key="B">B-new</li>
        </ul>
    
    • 移动(这里和16版本的diff 是一样)

WechatIMG1012.jpeg