深入学习React,重塑了我对React的理解

深入学习React.js

这是一篇学习的笔记,很荣幸能听到那么好的课,虽然但是觉得这个课有点难懂,一遍遍 的重复着,慢慢的记笔记,但是正事这个课让我重塑了对React 印象。希望这篇笔记能帮到你们

React 为什么这么难学

Vue的体系/原理的相关内容百花齐放

但React 只是体系/原理的相关内容却屈指可数

React 带着jsx横空出世
  1. jsx 的本质是什么,它和js之间到底是什么关系

  2. 为什么要用jsx? 不用会用什么后果

  3. Jsx背后的功能模块是什么,这个功能模块都做了哪些事情

    Jsx 是javaScript 的一种语法扩展,它和模板语言很接近,但是它充分具备JavaScript的能力。 ------ React官网

    Jsx 语法是如何在JavaSript 中生效的,jsx 是一种扩展,浏览器不会天然的支持Jsx:需要使用Babel编译

    Babel 是一个工具链,主要用于将ECMAScript 2015+版本的代码转换为向后兼容的JavaScript 语法,以便能够运行在当前的旧版本的浏览器或其他环境中 --bable官网

    Jsx 会被编译为React.createElement(),React.createElement()将返回一个叫作 React Element的js对象

    // type 标记节点的类型  h1 div 
    // config 以对象形式传入,组件所有的属性都会以键值对的形式在config对象中className:'app'
    // children 以对象的形式传入,它记录的是组件标签之间的嵌套的内容
    export function createElement(type,config,children)
    

    createElement 返回的是一个ReactElement 对象实例,这个对象实例本质上是一个以JavaScript形式存在的对DOM 描述的,就是虚拟DOM

    ReactDOM.render()将虚拟DOM渲染处理真实DOM

    开发者编写JSX代码经过Babel 编译,React.createElement 调用,React.createElement 调用得到虚拟DOM,ReactDOM.render()将虚拟DOM渲染为真实DOM

总结:

开发者==> 编写JSX代码 >经过Babel 编译=>React.createElement调用得到===>ReactElement 就是===> 虚拟DOM =>作为参数传入>ReactDOM.render()===>渲染为真是的DOM

jsx 可以提高程序员的工作效率,增加了代码的可阅读行,使程序员不必使用React提供的 createElement 开发

React 生命周期
  1. 虚拟DOM:核心算法的基石

    组件的初始化阶段:render 方法生成虚拟DOM,经过ReactDOM.render方法得到真实DOM

    组件更新阶段:render方法生成新的虚拟DOM,diff算法,定位出两次虚拟DOM的差异, 实现精确更新

  2. 组件化,工程化事项在框架中的落地

    封闭特点:主要针对“渲染工作流”来说的,每个组件都只处理它内部的渲染逻辑

    开发特点:针对组件间通信来说的,React允许开发者基于 “单向数据流” 的原则完成组间的通信,而组件间的通信又将改变通信双方/某一方的数据, 进而对渲染结果造成影响

  3. 生命周期方法的本 质:组件的“灵魂”与躯干

    将React.Component 的render方法形容为React组件的“灵魂”,从宏观上建立对React生命周期的感性认知

    render方法 之外的生命周期方法可以理解为是组件的躯干

  4. React15的生命周期流程

    constructor // 构造方法,只在初始化的时候执行一次

    conponentWillReceiveProps // 并不是由Props 的变化触发的,而是父组件的更新触发的,很让人费解的名字,不必纠结这个生命周期,因为已经被废除了

    shouldComponentUpdata // 组件更新前触发, retrue true则渲染更新UI ,否则不更新

    conponentWillMount // 组件将要挂载时触发,只在初始化的时候执行一次

    componentDidUpdata // 在组件更新完之后触发

    componentDidMount // 在 render 之前触发,允许做一些不涉及真实DOM 的操作,只在初始化的时候执行一次

    render() 在执行过程中并不会去操作真实的DOM,它的职能是把需要渲染的内容返回出来

    componentwillUnmount 组件卸载时触发

  5. 组件的更新:

    React组件会根据shouldComponentUpdata来决定是否执行该方法之后的生命周期,进而决定是否对组件进行re-render(重渲染)

  6. 组件的卸载

    组件在父组件中被移除了

    组件中设置了key 属性,父组件在render的过程中,发现key值不一致。那么父组件就知道子组件已经发生了改变,那么react 就会直接卸载这个旧的组件,然后重新渲染新的组件

react 16以来的生命周期
废弃了componentWillReceiveProps,新增了get DerivedStateFromProps
  1. 废弃了componentWillMount,新增了,不是为了替换,而是进化,更新,进步的表现,与componentDidUpdata一起,这个新的生命周期涵盖了过时componentWillReceiveProps的所有用例,官方试图代替componentWillReceiveProps

  2. getDerivedStateFromProps有且仅有一个用途

    使用props来派生/更新state

  3. static getDerivedStateFromProps(props,state)在更新和挂载两个阶段都会触发,返回一个对象形式的返回值,必须需要return 一个对象

    是一个静态方法,静态方法不依赖组件实例的存在,因此在这个方法的内部,是访问不到this

    此方法不是对state 的更新动作,并非“覆盖”式的更新,而是针对某个属性的定向更新

    // 比如
    return {
    	testMessage:"这是getDerivedStateFromProps"
    } 
    // 再子组件中的state
    constroctor(){
    	this.state={
    	  test:"子组件文本"
    	}
    }
    // 最的state的结果是
    console.log(this.state)
    {
      test:"子组件文本",
      testMessage:"这是getDerivedStateFromProps"
    }
    
  4. 此方法要求返回一个对象格式的返回值。不然会被警告

  5. 这个生命周期的一个常见 bug。 setState 和 forceUpdate 也会触发这个生命周期,所以内部 state 变化后,又会走 getDerivedStateFromProps 方法,并把 state 值更新为传入的 prop。

    // 错误
    Class ColorPicker extends React.Component {
        state = {
            color: '#000000'
        }
        static getDerivedStateFromProps (props, state) {
            if (props.color !== state.color) {
                return {
                    color: props.color
                }
            }
            return null
        }
        ... // 选择颜色方法
        render () {
            .... // 显示颜色和选择颜色操作
        }
    }
    // 正确
    Class ColorPicker extends React.Component {
        state = {
            color: '#000000',
            prevPropColor: ''
        }
        static getDerivedStateFromProps (props, state) {
            if (props.color !== state.prevPropColor) {
                return {
                    color: props.color
                    prevPropColor: props.color
                }
            }
            return null
        }
        ... // 选择颜色方法
        render () {
            .... // 显示颜色和选择颜色操作
        }
    }
    

为什么要用getDerivedStateFromProps 代替componentWillRecerveProps

于componentDidUpdate 一起,这个新的生命周期涵盖过时componentwillReceiveProps 的所有用例

  • getDerivedStateFromProps 是作为试图代替componentWillReceiveProps 的API 而出现的
  • getDerivedStateFromProps 不能和componentWillReceiveProps 划等号
  • getDerivedStateFromProps 比componentWillReceiveProps 功能更少,显得更加的专注、
  • getDerivedStateFromProps 做了合理的减法,从被定义为static 方法这件事就可见一斑
消失的componentWillUpdate 与新增的getSnapshotBeforeUpdate
getSnapshotBeforeUpdate(prevProps,prevState){
	// ...
}
  1. getSnapshotBeforeUpdate 的返回值会作为第三个参数给到componentDidUpdate,它的执行时机是在render方法之后,但是在DOM更新之前,同时获得到更新前的真是DOM和更新前后的state&props 信息
  2. Fiber进行异步渲染,Fibel架构的重要特征就是可以被打断的异步渲染模式,根据“能否被打断”这一标准,React 16 的生命周期被划分为了render 和commit 两个阶段
  3. render 阶段是允许暂停,终止和重启的,重启的形式是重复执行整个任务
废弃的生命周期
废弃的生命周期
  1. componentWillMout
  2. componentWillUpdate
  3. componentWillReceiveProps
废弃的生命周期特性
  1. 这些生命周期的共性都是:他们都处于render阶段,都可能重复被执行,由于这些API 常年被滥用,再重复执行的过程中,都隐藏着不可小嘘的风险
  2. 在这些will开头的生命周期中,你可能会
    • setState
    • fetch 发起异步请求
    • 操作真实的DOM
  3. will 开头的生命周期问题
  • 完全可以在其他生命周期里去做
  • 首次渲染依然在数据返回之前执行,很多同学因为太年轻,以为这样做(在这些will开头的生命周期里发起异步请求),就可以让异步请求回来的早一点,从而避免首次渲染白屏,可惜你忘了,异步请求,在怎么快也快不过同步的生命周期,conponentWillMount结束后, render 会迅速地被触发,所以说,首次渲染依然会在数据返回之前执行,这样子,不仅不能达到你预想的目的,还会导致服务端渲染下的冗余请求等场景下的问题(服务端渲染时,will开头的生命周期也会在服务端执行一遍)。
  • 在Fiber 带来的异步渲染机制下,可能导致非常严重的bug,由于render 阶段里的生命周期都可以被重复执行,在componentWillXXX被打断 + 重复多次后就会发出多个付款请求
  • 避免开发者触碰this,就是在避免各种危险的骚操作
  • 即使你没有开启异步,React 15 下也有不少人能把自己“玩死”,React16改造生命周期的主要动机,就是为了配合Fiber 架构带来的异步渲染带来的异步渲染机制
  • 确保了Fiber机制下数据和视图的安全性
  • 同时也确保了生命周期方法的行为更加纯粹,可控,可预测
数据是如何在组件间流动
基于props 的单向数据流

组件,从概念上类似于javaScript 函数,它接受任意入参(即props)并返回用于描述页面展示内容的React 元素 —props

单向数据流

当组件的state以props的形式流动时,只能流向组件树中比自己层级更低的组件

父 - 子组件通信

React 的数据流是单向的,父组件可以直接把this.props 传入子组件,实现父-子之间通信

子-父组件通信

父组件传递给子组件的是一个绑定了自身上下文的函数,那么子组件在调用改函数时,就可以将想要交给父组件的数据以函数入参的形式给出去

兄弟组件通信

兄弟组件之间通信,是将自个把需要的传递的信息,放在共同的父组件中,这样就把兄弟组件通信转为了两个子组件和父组件之间的通信

为什么不推荐用props解决其他场景的需求
  1. 层层递归的优点是非常简单,用已有知识就能解决
  2. 问题是会很多代码,非常繁琐
利用“发布-订阅”模式驱动数据流

发布订阅模式可谓是解决通信类问题的“万金油”

  1. socket.io 模块就是一个典型的跨端发布-订阅模式的实现
  2. Node.js,许多原生模块也是以EventEmitter 为基类实现的
  3. Vue.js 中作为常规操作被推而广之的“全局时间总线”EventBus
理解事件的发布-订阅机制

发布订阅模式,早期的应用应该是在浏览器中DOM事件中,监听事件的位置和触发事件的位置是不受限的,非常适合用于任意组件的通信了

// 创建一个事件监听器(这动作就是订阅,当这个DOM事件被触发时,就是发布)
target.addEventListener(type,listener,useCapture)
el.addEventLisstener('click',function,false)

发布订阅机制的优点在于,监听事件的位置和触发事件的位置是不受限制的,只要他们在同一个上下文里,就能感知

发布订阅模型,API 设计思路

发布订阅有两个关键的动作,事件的监听(订阅)和事件的触发(发布)

  1. on():负责注册事件的监听器,指定事件的触发是的回调函数
  2. emit():负责触发事件,可以通过传参使其在触发的时候携带数据
  3. of():负责监听器的删除
发布订阅的代码实现
class MyEventEmitter {
  constructor() {
    // eventMap 用来存储事件和监听函数之间的关系
    this.eventMap = {}
  }
  // 负责事件注册
  on(type, handler) {
    //hanlder 必须是一个函数
    if (!handler instanceof Function) {
      throw new Error("handler 必须是一个函数")
    }
    // 判断type在对应的队列是否存在
    if (!this.eventMap[type]) {
      // 如果不存在,新建改队列
      this.eventMap[type] = []
    }
    // 若存在,直接往队列里推入handler
    this.eventMap[type].push(handler)
  }
  // 负责发布事件
  emit(type, params) {
    // 假设该事件是有订阅(对应的事件队列存在)
    if (this.eventMap[type]) {
      // 将事件队列里的handler 依次放在执行出队
      this.eventMap[type].forEach((handler, index) => {
        handler(params)
      })
    }
  }
 // 负责删除事件监听, >>> 运算
  off(type, handler) {
    if (this.eventMap[type]) {
      this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1)
    }
  }
}

测试

const EventView = () => {
  const myEvent = new MyEventEmitter()
  const testHandler = function (params) {
    console.log("test 事件触发了,testHandler 接受到的入参是", params);
  }
  // 监听test 事件
  myEvent.on('test', testHandler)
  // 触发test 事件的同时,传入希望testHandler 感知的参数
  myEvent.emit('test', "helle world")
  return (
    <div>
      手写发布订阅模式,数据传输
    </div>
  )
}
// print: test 事件触发了,testHandler 接受到的入参是 helle world
如何实现发布

对于任意两个组件A和B,假如我希望实现双方之间的简单通信,借助EventEmitter 来做就很简单

在App.js中挂载

import { MyEventEmitter } from './globalEvent'

// 挂载全局发布订阅 MyEventEmitter
const myEvent = new MyEventEmitter()
window.myEvent = myEvent

两个简单的A,B 组件中使用myEvent

数据是如何在组件间流动 Context API 维护全局状态

Context API 是React 官方提供的一种组件树全局通信的方式

在React 16.3 之前,Context API 由于存在种种局限性,并不被官方提倡开发者使用,开发者更多的是把它作为一个概念来探讨,而从V 16.30开始,React 对Context API 进行了改进新的Context API 具备更强的可用性

const AppContext = React.creactContext()
新的Context API 解决了什么问题

即便组件的shouldCponentUpdata 返回false, 它依然可以“穿透”组件向后代组件进行传播,进而确保了数据生产者和数据消费者之间数据的一致性,其实和Redux的一样,都是有action对象描述修改的内容 ,reducer纯函数修改真正的数据。

初探Redux

Redux 是JavaSCript 状态容器,它提供可预测的状态管理,Redux 和 JavaScript 的关系,证明React 可以用,Vue可以用,原生个JavaScript 也可以使用Redux.

Redux 是如何帮助React 管理数据的
  1. store 是一个单一的数据源,而且是只读的

  2. action 是对变化的描述

    const action = {
    	type : "Add_ITEM",
    	payload:'
  3. text
  4. '
    }
  5. reducer 是一个函数,负责对变化进行分发和处理,最终把新的数据返回给Store

注意:

在Redux 的整个工作过程中,数据流是严格单向的

Redux 是如何帮助React管理数据的

对一个React视图的所有数据,都来自Store,如果你想对数据进行修改,只有一种途径,派发Action,Action 会被Reducer 读取,进而对Action 的不同内容,进行修改,生成新的state,这个state 会被更新到store里,进而驱动视图层面,作出对应的改变。

Redux 通过提供一个统一的状态容器,使得数据能够有序地在任意组件之间穿梭

从编码的角度理解Redux 的工作流
inport {creactStore} from "redux"

//创建store
const store = createStore(
	reducer, // 必须传
  initial_state,
   	applyMiddleware(midlleware1,middleware2)
)
// reducer 
const reducer = (state,action)=>{
    return new_state
}
// action 
const action = {
    type:"ADD_ITEM",
    yload:'
  • text
  • '
    } // 更新规则全都写在reducer 里 const store = createStore(reducer) // 使用dispatch 派发action ,action 会进入到reducer里,触发对应的更新 store.dispatch(action)
    React Hooks 设计动机

    React-Hooks 自16.8 以来才真正被推而广之,对我们每一个老React 开发来说,它都是一个新事物,它是React 团队在这刀真枪的React 组件开发实践中逐渐认知到的一个改进点,背后涉及了对类组件和函数组件两种组件形式的思考和侧重。

    为了确保Hooks在每次渲染时都保持同样的执行循序
    1. 只能在React函数中调用Hook
    2. 不要在循环,条件或者嵌套函数中调用Hooks
    分析React-Hooks 的调用链路

    React-Hooks 在首次渲染和更新阶段是不同的,

    首次渲染的过程:

    useState >resolveDispatcher 获取dispatcher>调用dispatcher.useState==>调用mountState==>返回目标数组(如[state,setState])

    更新阶段的过程:

    useState >resolveDispatcher 获取dispatcher>调用dispatcher.useState==>调用updataState==>updataReducer==>返回目标数组(如[state,useState])

    函数组件和类组件的对比,无关“优劣”,之谈 “不同”
    1. 类组件需要基础Class,函数组件不用
    2. 类组件可以访问生命周期方法,函数组件不能
    3. 类组件中可以获取到实例后的this,并基于这个this做各种各样的事情,而函数组件不可以
    4. 类组件中可以定义并维护state(状态),而函数组件不可以

    这是不是意味着类组件比函数组件更好呢?答案当然是否定的。

    在React-Hooks 出现之前的世界里,类组件的能力边界明显强于函数组件,但要进一步推导类组件强于函数组件,未免有些牵强。

    类组件是面向对象编程思想的一种表征,有两个特点

    1. 封装,将一类属性和方法,聚拢到一个Class里去
    2. 继承,新的Class 可以通过继承现有的Class,实现对一类属性和方法的复用
    了解虚拟DOM

    研发模式的不断演进的背后,恰恰蕴含着前端人对“DOM操作”这一核心动作的持续思考和改进。

    为什么我们需要虚拟DOM

    “DOM操作是很慢的,而JS却可以很快,直接操作DOM可能会导致频繁的回流和重绘,js不存在这些问题。因此虚拟DOM 比原生DOM 更快”?

    虚拟DOM (Virtual DOM )

    本质是JS 和DOM 之间的一个映射缓存在形态上的表现。是一个能够描述DOM 结构及其属性信息的JS对象

    挂载阶段

    React 将结合JSX的描述,构建出虚拟DOM树,然后通过ReactDOM.render实现虚拟DOM到真实DOM 的映射(触发渲染流水线)

    更新阶段

    页面的变化会优先作用于虚拟DOM,虚拟DOM 将在JS 层借助算法先对比出具体有哪些真实的DOM需要被改变,然后将这些改变作用于真实的DOM

    历史中的DOM 操作解决方案
    1. 原生JS 支配下的"人肉DOM"时期

      前端页面“展示”的属性远远强于交互的属性这就导致了JS的定位只能是“辅助”

      前端工程师们会花费大量的时间去实现这些静态的DOM,待一切结束后,在补充少量的JS

    2. 解放生产力的jQuery 时期

      大量的DOM操作需求带来的前端开发工作量的激增,jQuery 首先解决的就是”API 不好使“这个问题

      将DOM API封装为相对简单和优雅的形式,同时一口气做掉了跨浏览器的兼容工作,并且提供了链式API调用、插件扩展等一系列能力用于进步解放生产力

    3. 民只初启:早期模板引擎方案

      jQuery 并不能从根本上解决DOM 操作量过大情况下前端的压力

      模板引擎方案更倾向于点对点解决繁琐DOM操作的问题,它在能力和定位上既不能够、也不打算替换掉jQuery,两者是和谐共存的。因此这里不存在“模板引擎时期”。只有模板引擎方案

      数据:

      const staffs = [
          {
            name: "修言",
            career: "前端"
          },
          {
            name: "翠翠",
            career: "编辑"
          }
      ]
      

      使用模板语法

      <table>
          {staffs.forEach((staffs) => {
            <tr>
              <td>{% staffs.name %}</td>
              <td>{% staffs.career %}</td>
            </tr>
          })}
        </table>
      

      模板渲染流程:

      1. 读取HTM模板并且解析它,分离出其中JS信息
      2. 将解析出的内容拼接成字符串,动态生产JS 代码
      3. 运行动态生成的JS代码,返回“目标HTML”
      4. 将“目标HTMl”赋值给innerHTML,触发渲染流水线,完成真实DOM 的渲染

      使用模板引擎方案来渲染数据,关注的仅仅是数据和数据变化的本身。在思想上具有了先进性

    4. 走“数据驱动视图”这条基本道路

      模板引擎的数据驱动视图方案,核心问题在于对于真实的DOM过于大刀阔斧,导致了DOM的操作范围过大,频率过高,进而导致了糟糕的性能,因为魔板引擎会把整个页面都重新渲染。那么操作虚拟DOM 不就行了吗

    5. 虚拟DOM

      虚拟DOM 和Redux 一样,不依附于任何具体的框架。当进行DOM操作时,会对比前后两次虚拟DOM 树,用过diff 算法,得到补丁集(需要更新的内容),把补丁替换(patch)到真实DOM中,实现精准的的差量更新。

    模板渲染&虚拟DOM渲染

    模板渲染:

    动态的生成HTM(构建新的真实DOM) ,旧的DOM元素被新的DOM 元素替换

    虚拟DOM渲染:

    构建新的虚拟DOM树,通过diff 对比出新旧两颗树的差异,差量更新DOM

    为什么选用虚拟DOM ,这点是为了更好的性能吗

    在数据内容变化非常大(或者说整个发生了改变),促使差量更新计算出来的结果和全量更新极为接近(或者说完全一样)。虚拟DOM的劣势主要在于JS 计算的耗时。但是DOM操作的能耗和JS计算的能耗根本不在一个量级

    更新少量的数据,每次setState 的时候只修改少量的数据,模板渲染和虚拟DOM之间的DOM 操作量级的差距就完全来开了。虚拟DOM将在性能上具备绝对的优势。

    虚拟DOM 的价值不在性能:

    1. 提高了开发体验,研发效率;虚拟DOM的出现为数据驱动视图这一思想提供了高度可用的载体。是前端开发能基于函数式UI编程方式,实现高效的声明式编程的基础。

    2. 跨平台:虚拟DOM是对真实渲染的一层抽象 。如果没有这一层抽象,那么视图层将会和渲染平台紧密的偶合在一起。为了描述相同描述同样的视图内容,你可能要在web 端,native 端,iOS界面,安卓界面,小程序界面。写多套完成不同的代码。

    3. 除了差量更新“批量更新”也是虚拟DOM在性能方面所做的重要努力,批量更新在通用虚拟DOM库里是由batch 函数处理的。在差量更新非常快的情况下,用户实际上只能看到最后的一次更新。前面几次的更新

      动作虽然意义不大,但是都会触发重渲染流程,带来大量不必要的高耗能操作。这个时候就要请batch帮忙了,batch的作用是缓存每次生成的补丁集,它会把收集到的多个补丁集暂存到队列中,再将最终的结果交给渲染函数,实现集中化的道理批量更新。

    栈调和(Stack Reconciler)
    调和(Reconciliation) 过程与Diff 算法
    调和又译为协调:

    Virtual DOM 是一种概念。在这个概念里,UI是一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过ReactDOM 等类库使之与“真实的”DOM同步。这一过程叫作调和。即调和就是将虚拟DOM映射到真实DOM的过程,严格来说调和过程不能和Diff 划等号。

    调和是使一致的过程

    Diff是找不同的过程

    React将源码划分三大板块:

    Core 、Renderer、Reconciler:

    Reconciler(调和器)所做的工作包括组件的挂载、卸载、更新等过程,其中更新过程涉及Diff 算法的调用。如今大众的认知里,但我们讨论调和的时候,其实就是在讨论Diff,这样的认知确实有一些合理性,因为Diff确实是在调和的过程中最具代表的一环

    根据Diff算法实现形式的不同,调和算法被划分为了以React15为代表的栈调和以及React16以内的Fiber 调和。

    Diff的设计思想

    传统的计算方法是通过循环递归进行树节点的一一对比,找出两个树结构的不同。这一过程的复杂度是O(n3)。这对浏览器来说,意味着一场性能的灾难。

    优化 将O(n3)复杂度转换为O(n)复杂度,React 付出了很多的努力
    1. 若两个组件属于同一类型,它们将拥有相同的DOM 树形结构
    2. 处于同一层级的一组子节点,可用通过设置Key 作为唯一标识,从而维持各个节点在不同的渲染过程中的稳定性
    3. 从实践中发现。DOM节点的跨层级操作并不多,同层级操作是主流。
    Diff 逻辑的拆分解读
    1. Diff 算法性能突破的关键点在于:分层对比

      DOM 节点之间的跨层级操作并不多,同层级操作是主流,React 的Diff 过程直接放弃了跨层级的比较,Diff过程中只针对相同层级的节点作对比。这样一来,只需要将DOM树从上到下的一次遍历,就可以完成对整棵树的对比。这是对降低复杂度量级的最重要设计

      需要注意的是,栈调和虽然将传统树对比算法优化为了分层级的对比,但整个算法仍然是以递归的形式运转的,分层递归,还是递归。

      如果真的发生了跨出级的操作会发生什么呢。假如B和C同级,把B移动到C下面,那么Diff算法认为是,B消失了,然后在C下面重新创建一个新的B节点。销毁到创建的代价是十分昂贵的,所以在实际开发中,避免跨层级的操作

    2. 类型一致的节点才有继续Diff 的必要性

      React认为,若两个组件属于同一类型,那么它们将拥有相同的DOM树形结构。实际上也是,在开发中很难遇到结构完全一致,而类型不一致的概率十分低。所以React 认为只有同类型的组件,才有进一步对比的必要性,入参比较的两个组件类型不一致,那么将直接放弃比较,不继续递归下去。

    3. key 属性的设置,可以帮我们尽可能重用同一层级类的节点

      key属性能够帮助维持节点的稳定性。key 是用来帮助React 识别那些内容被更改、添加、或者移动。key需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因此如果key值发生了变更,React 则会触发UI的重渲染。这是一个非常有用的特性。

      key试图解决的是:同一层级下,同一节点的重用问题。key 就像一个记号一样,帮助React “记住”某一个节点,从而在后续的更新中实现对这个节点的追踪。

    栈调和总结

    Diff 算法的源码链路很长,但若真的把源码中的逻辑要点作提取,你消化它们可能也就不过一杯茶的功夫。

    把握住 React15下的Diff 过程下,“树递归”这个特征,这就够了

    如果你学有余力,可以了解一下React 16 对调和实现

    SetState是异步还是同步的

    当你入门React 的时候,接触的第一波API 里一定有setState ,数据驱动视图,而当你项目的数据流乱作一团的时候,始作俑者也往往是setState,工作机制太复杂,文又不说清楚

    setState是异步批量更新的。

    reduce方法里的setState 竟然是同步更新的(这里是肯定句)。

    setState 异步的动机和原理,批量更新的艺术

    一个setstate触发将会导致一系列的生命周期函数的发生:

    setState > shouldComponentUpdata >componenWillUpdata>render>componentDidUpdata

    一个完整的更新流程,涉及了包括rerender的多个生命周期函数,rerender本身会涉及对DOM 的操作,它会带来很大的性能开销。

    **假如说:**一次setState就触发一个完整的更新流程,那么每一次setState 就触发一次rerender ,那么我们的视图没有触发几次就卡死了。

    批量更新:

    每来一个setState,就把它塞进一个队列里,等时机成熟,再把多个state 结果合并最后只针对最新的state值走一次更新的流程。

    setState 的工作流

    setTimeout中的setState会是同步的,并不是setTimeout改变了setState,而是setTiemout 帮助了setState 逃脱了React 对它的管控,只要是在React 管控下,setState一定是异步的。

    源码中isBachingUpdatas 管控批量更新机制:

    1. 首次渲染时调用batchedUpadata,因为在组件是渲染过程中,会按照顺序调用各个生命周期函数。开发者可能会在生命周期函数中调用setState。因此需要开启batch确保所有的更新都进入队列,进而确保所有setState 都是生效的。
    2. 当我们在事件中绑定了事件监听,事件中也有可能是调用setState。为了确保每一次的setState都生效,React同样会在这里手动开始批量更新。
    3. isBachingUpdatas 这个变量,在React 的生命周期函数以及合成事件执行前,已经被React悄悄修改了true,这个时候我们所做setState 操作当然不会立即生效,事件执行完之后,事务的close方法isBachingUpdatas 改为fasle.
    同步现象:

    在isBachingUpdatas 的约束下,setSate 只能是异步的。isBatchingUpdates先不执行更新操作,当时事件执行完毕之后,再把锁解开。

    increment  = ()=>{
      // 进来先锁上,不立即执行setState
      inBatchingUpdates =  true
      this.setState({
        count:this.state.count + 1
      })
      // 执行完函数再放开
      inBatchingUpdates = false
    }
    

    就像前面所说,setTimeout方法并不是改变setState把异步变成了通过,只是setState在setTimeout 帮助下了,规避了React 的批量更新操作。当setTimeout从中作祟时,isBatchingUpdates不生效,因为isBatchingUpdate是同步执行的,setTimeout是在异步执行的,当同步代码已经执行完之后才执行异步代码,这个时候isBatchingUpdates是false了,这就isBatchingUpdates对setTimeout中执行的代码完全没有约束力。

    increment  = ()=>{
      // 进来先锁上,不立即执行setState
      isBatchingUpdates =  true
      setTimeout(()=>{
        this.setState({
          count:this.state.count + 1
        })
      },0)
      // 执行完函数再放开
      inBatchingUpdates = false
    }
    
    总结:

    setState 的变现会因调用场景的不同而不同

    1. 在React 生命周期函数及合成事件中,他表现为异步
    2. 在setTimeout ,setInterval,包括在DOM原生事件中,它都表现为同步,这些情景下的this.setStaet 会从React的异步管控下逃脱掉,从而表现出同步的效果

    这种差异本质上是由React 的事务机制和批量更新机制的工作方式来决定的。以上内容都是基于React15的解读,React16以来,整个React 核心算法被重写,setState也不避免的被“Fiber化”

    Fiber 架构的迭代动机与设计思想
    对React 的定位

    我们认为,React 是用javaScript构建的快速响应的大型Web 应用程式的首选方式。它在FaceBook 和Instagram上表现优秀。

    然而随着事件推移和业务复杂度的提升,React曾被人们津津乐道的State Recondiler 也渐渐在体验方面显出疲态。为了进一步贯彻快速响应的原则,React团队在16.x 的版本中将其最为核心的Diff算法整个重写,使其Fiber Reconciler 的全新面貌示人。

    1. Stack Reconciler 的局限性
    2. 为什么不从Stack Reconciler下层做出改变而选择重写了整个Diff算法?
    3. Fiber架构又是何方神圣
    前置知识

    单线程的JavaScript与多线程的浏览器。多线程的浏览器出来要处理JavaScript 线程以外,还需要处理各种各样的任务,比如事件线程,定时器,延时器网络请求的等等,其中还要处理DOM的UI 渲染线程,而JavaScript 线程是可以操作DOM的。

    如果渲染线程和JavaScript 线程同时在工作,那么渲染结果必然难以预测的,比如渲染线程刚绘制还的画面,可能接下来就被JavaScript 改得面目全非。

    所以JavaScript 线程和渲染线程必须是互斥:当其中一个线程在执行时,另一个线程只能挂起等待。具有相似特征的还有事件线程,浏览器的Event Loop机制,决定了事件任务是由一个事件队列来维持的。当事件被触发时,对应的任务不会立刻被执行,而是由事件线程把它添加到任务队列的末尾,等待JavaScript 的同步代码执行完毕后,在空闲的时间里执行出队。

    在这种情况下,若JavaScript 线程长时间占用了主线程,那么渲染层面的更新长时间地等待,界面长时间不更新,带给用户的体验就是所谓的卡顿。当卡顿时更多的用户会点击页面,希望浏览器给出响应,遗憾的是触发事件之后也将难以被响应,因为事件线程也在等待JavaScript的响应。

    为什么会卡顿

    javaScript 对主线程的超时占用问题:Stack Reconciler是一个同步的递归过程,同步的递归过程意味着不到南墙不回头,一旦更新开始根本停不下来。

    栈调和机制下的Diff算法,其实就是树的深度优先遍历的过程,而树的深度优先遍历总是和递归脱离不了关系。调和器会重复父组件调用子组件 的过程,直到最深的一层节点更新完毕,才慢慢向上返回。整个过程的致命性在于它是同步的,不可以被打断。

    Stack Reconciler 需要的调和时间会很长,这就意味着javaScript 线程将长时间霸占主线程,进而导致我们上文中所描述的渲染卡顿/卡死、交互长时间无响应等问题。

    什么是Fiber

    在计算机科学中,我们有进程和线程之分,而Fiber就是比线程还要纤细的一个过程,也就是所谓的纤程纤程的出现意在对渲染过程实现更加精细的控制

    从架构角度来看,Fiber 是对React 核心算法的重写

    从编码角度来看,Fiber 是React 内部所定义的一种数据结构

    从工作流的角度来看,Fiber 节点保存了组件需要更新的状态和副作用

    Fiber是如何解决问题的

    Fiber 架构的应用目的是实现增量渲染,实现增量渲染的目的,是为了实现任务的可中断、可恢复,并给不同的任务赋予不同的优先级,最终达成更加顺滑的用法体验。

    核心是:可中断,可恢复,于优先级 三层架构

    1. Scheduler 调度更新的优先级,每一个更新任务都会被赋予一个优先级,更高的优先级任务,会被更快的调度到Reconciler 层。若发现B的优先级高于当前任务A ,那么当前处于Reconciler 层的A任务就会被中断。之前被中断的A任务会被重新推入到Reconciler 层,这便是可恢复。

    2. Reconciler 找不同,这一层负责对比出新老DOM 之间的变化

    3. Renderer 渲染不同,这一层负责将变化的部分,应用道视图上

      从Reconciler 到Renderer 这一过程是严格同步的。

    Fiber 架构对生命周期的影响

    React 16生命周期分为三个阶段

    1. Render phase

      Render 阶段纯净而没有副作用,可能会被React 暂停,终止或者重新启动。

    2. Pre-commit phase

      Pre-commit 可以读取DOM

    3. commit phase

      可以使用DOM,运行副作用,安排更新

    其中Pre-commit、commit 从大阶段上来看,都是commit阶段。在Render阶段,React 主要在内存中做计算,明确DOM的更新点,commit阶段则负责把Render 阶段生成的更新真正的执行。

    React15 : render 开始 >停不下来的递归计算(同步)=>commit 提交渲染

    React16: render开始 ===> 工作单元|工作单元|工作单元…(异步) ===> commit 提交渲染

    React16 的工作单元有着不同的优先级,React 可以根据优先级的高低去实现工作单元的打断和恢复。

    由于render阶段是操作对于用户来说是不可见的,所以就算被打断和恢复,对用户来说,也是零感知。但是工作单元的重启,将会伴随着对部分生命周期的重复执行:

    1. componentWillMount
    2. componentWillUpdata
    3. shouldComponentUpdata
    4. componentWillRecerverProps

    因此,尽量不要再willxxxx的生命周期函数中操作

    总结
    1. React 16 中Fiber 架构的架构分层和宏观视角下的工作流
    2. 关于Fiber Reconciler 还有太多的谜题需要我们一一去探索
      • React 16在所有的情况下都是异步渲染的吗
      • Fiber架构中的可中断可恢复到底是如何实现的
      • Fiber 树和传统的虚拟DOM树有何不同
      • 优先级调度又是如何实现的

    ReactDOM.render 是如何串联渲染链路的(上)
    ReactDOM.render

    以首次渲染为切入点,差解FIber 架构下的ReactDOM.render 所触发的渲染链路,结合源码理解整个链路中所涉及的初始化,render和commit等阶段

    初始化阶段

    完成FIber 树中基本实体的创建.

    1. 请求当前Fiber 节点的优先级
    2. 结合优先级,创建当前Fiber节点的updata 对象,并将其入队
    3. 调度点前的节点

    render阶段的任务就是完成Fiber 树的构建,它是整个渲染链路中最核心的一环。

    Fiber 架构带来的异步渲染时是React16 的亮点,可是ReactDOM.render 触发的首次渲染是一个同步的过程。

    那是因为 React有几种启动模式

    1. legacy 模式 同步:ReactDOM.render(,nodeDode), React App 的启动模式,部分功能未开启
    2. blockong 模式:ReactDOM.createBlockingRoot(rootNode).render(),作为concurrent 的一个过渡步骤
    3. concurrent 模式 异步:ReactDOM.createRoot(rootNode).render(),目前在实验当中,未来稳点之后将作为React的默认开发模式。这个模式,开启了所以的新功能。

    在这三种模式当中,我们常用的ReactDOM .render 对应的是legacy 模式,它实际触发的仍然是同步的渲染链路。blockong模式可以理解为legecy 和concurrent模式的一种过渡形态,之所以有这个模式是因为React希望提供一个渐进的迁移策略,帮助我们更加顺滑的过渡到concurrent模式。

    concurrent模式 是React 的终极目标,也是其创作团队使用Fiber 架构重写核心算法的动机所在

    关于异步模式下的首次渲染链路

    如果想要开启异步渲染,我们需要调用ReactDOM .createRoot方法来启动应用。

    平时我们使用的模式就是ReactDOM.render模式,也是create React App 默认的模式

    ReactDOM.render(
      <App />,
      document.getElementById('root')
    );
    

    在不同的渲染模式在挂载阶段的差异,不是工作流的差异,其工作流涉及初始化,render,commit这个步骤。本质上来说是mode,mode 属性决定这个工作流是一气呵成(同步)的还是分片执行(异步)的

    React 16 如果没有开启Concurrent 模式

    那么它还能叫Fiber架构吗

    从动机上来看,Fiber架构的设计确实是为了Concurrent 而存在的,但是Fiber架构在React 中并不能够和异步渲染划上严格的等号,它是一种同时兼容了同步渲染与异步

    ReactDOM.render 是如何串联渲染链路的(中)
    render 阶段和commit阶段

    render 阶段可以认为是整个渲染过程链路中最为核心的一环,因为我们反复强调找不同的过程,恰恰就是在这个阶段发生的。

    React15下的栈调和过程是一个递归的过程,在ReactDOM.render 触发的同步模式下它仍然是一个深度优先搜索过程,在这个过程中,biginWork(调用栈里的一个方法) 将创建新的的Fiber 节点。而completeWorkl则负责将Fiber 节点映射为DOM 节点。

    总结:掌握beginWork 的实现原理、理清Fiber 节点的创建链路,最终串联起了Fiber 树的宏观构建过程

    ReactDOM.render 是如何串联渲染链路的(下)

    completeWork

    寻觅Fiber树和DOM树之间的关联

    completeWork 的工作原理

    completeWork 的工作内容:负责处理Fiber 节点到DOM节点的映射逻辑,其内部有3个关键动作:

    1. 创建DOM 节点(CreateInstance)

    2. 将DOM节点插入到DOM 树中(AppendAllChildren)

    3. 为DOM 节点设置属性(FinalizeInitialChildren)

      创建好的DOM 节点会被赋值给workInProgress 节点的StateNodes 属性

      实际就是将子Fiber 节点说对应的DOM 节点,挂载到其父Fiber节点所对应的DOM 节点里去

    React 事件和原生事件有何不同
    React 有着自成一派的世间系统

    就React 事件系统来说,它涉及的源码量不算小,相关逻辑也不够内聚,整体理解成本相对较高。幸运的是,无论是面试场景下还是在实际的开发中,React事件相关的问题,都更倾向于考验我们对事件工作流、事件特征等逻辑层面问题的理解,而非对源码细节的把握。

    回顾原生DOM下的事件流

    想要理解好React事件机制,就必须对原生DOM事件流有扎实的掌握。

    你可能感兴趣的:(JavaScript,react.js,javascript,学习)