4. react-core

265 阅读6分钟

jsx

  • JSX其实只是一种语法糖,最终会通过babeljs转译成React.createElement语法
  • React.createElement会返回一个React元素
  • React元素事实上是普通的JS对象,用来描述你在屏幕上看到的内容
  • ReactDOM来确保浏览器中的真实DOM数据和React元素保持一致

babel并没有把jsx编译成虚拟dom,而是把jsx编译成React.createElement的方法调用,在浏览器执行的时候才会执行React.createElement,才会生成虚拟dom

jsx的实现

  • "start": "cross-env DISABLE_NEW_JSX_TRANSFORM=true react-scripts start",
  • DISABLE_NEW_JSX_TRANSFORM 不开启新的jsx转换
  • jsx转换器做了改变
    • react17之前 会转换成React.createElement
    • react17之后
    • import {jsx as _jsx} from 'react/jsx-runtime'
    • let element = _jsx(...)
    • _jsx 就相当于之前的 React.createElement
    • 原因是: 在编写代码的时候,如果不引入React, 因为编译之后会用到React.createElement,所以运行代码的时候会报错;如果引入React,因为在代码编写的时候实际上并没有用到React这个变量,有些工具如eslint会报错。这就有点矛盾。所以在react17之后做了改变,只要使用了jsx,编译时会自动引入'react/jsx-runtime'这个包,这样就不再需要在代码中引入React了

createElement方法

  • 创造一个虚拟dom,也就是一个react元素
  • 参数:
    • type, 元素的类型 span/p/div...
    • config, 配置对象 className style...
    • chidlren... 儿子 可能一个可能多个
  • 编译阶段 ref/key两个属性会放在config里, 最终会将ref/key两个属性放到元素上
  • ref 引入此元素
  • key 唯一标识一个元素
  • 返回一个对象 { type, ref, key, props} (children放在props里面)

render方法

  • 根据虚拟dom创建真实dom,并更新属性, 挂载到container上
  • 再处理儿子虚拟dom,同样创建真实dom,更新属性,挂载到父元素上
  • 同步递归创建

组件

  • 函数组件 image.png
  • 类组件
    • 定义状态有两种写法
        1. 在构造函数中 this.state = {}
        1. 直接在类中 state = {} 类的属性 这两种写法是等效的
    • state的更新可能是异步的,处于性能的考虑react可能会把多个setState合并成同一个调用
      • 在事件处理函数中,setState的调用会批量执行。setState并不会修改this.state,等事件处理函数结束后再进行更新
      • setState里面可以放置函数 当放置函数的时候,还是批量更新,两次打印出来的值还是0,但是,但是最终this.state.number = 2。因为函数会把老的状态作为参数,返回一个新状态,作为下一个函数的老状态
      • 在其他react不能管控的地方 setState就是同步执行(如setTimeout,原生dom事件,setInterval等)
      • 在react能管控的地方就是批量异步更新(如事件处理函数,生命周期函数)
handleClick = () => { 
    this.setState({ 
        number: this.state.number + 1,
    }); 
    console.log(this.state); // 0
    this.setState({ 
        number: this.state.number + 1,
    }); 
    console.log(this.state);  // 0
}
// state里的number变为1
// setState执行多次 每次用的都是原始的state的值,因为没有立即更改


handleClick = () => { 
    this.setState(state =>({ 
        number: state.number + 1,
    })); 
    console.log(this.state); // 0
     this.setState(state =>({ 
        number: state.number + 1,
    })); 
    console.log(this.state);  // 0
}




handleClick = () => { 
    this.setState({ 
        number: this.state.number + 1,
    }); 
    console.log(this.state); // 0
    this.setState({ 
        number: this.state.number + 1,
    }); 
    console.log(this.state);  // 0
    // 当前执行栈就结束了 setTimeout是下一个宏任务 所以会做一次批量更新 this.state.number变成1
    setTimeout(() => {
        this.setState({ 
            number: this.state.number + 1,
        }); 
        console.log(this.state); // 2
        this.setState({ 
            number: this.state.number + 1,
        }); 
        console.log(this.state);  // 3
    })
}
  • 每个组件都有一个更新器 Updater实例
    • 会将setState里的对象或者函数保存在一个队列
    • 将回调也保存在一个队列
class Updater { 
    constructor(classInstance) { 
        this.classInstance = classInstance;  //组件实例
        this.pendingStates = []; // 
        this.callbacks = []; 
    } 
    addState(partialState, callback) { 
        this.pendingStates.push(partialState);///等待更新的或者说等待生效的状态 
        if (typeof callback === 'function') 
        this.callbacks.push(callback);//状态更新后的回调 回调也放入数组
        this.emitUpdate();
    } 
    emitUpdate() { 
        this.updateComponent(); 
    } 
    updateComponent() { 
        let { classInstance, pendingStates } = this; 
        if (pendingStates.length > 0) { 
            shouldUpdate(classInstance, this.getState()); 
        } 
    } 
    getState() { 
        let { classInstance, pendingStates } = this; 
        let { state } = classInstance; 
        pendingStates.forEach((nextState) => { 
            if (typeof nextState === 'function') { 
                nextState = nextState(state); 
            } 
            state = { ...state, ...nextState }; 
        }); 
        pendingStates.length = 0; 
        return state; 
    }
}
function shouldUpdate(classInstance, nextState) { 
    classInstance.state = nextState; 
    classInstance.forceUpdate();
}


export class Component { 
    static isReactComponent = true; 
        constructor(props) { 
        this.props = props; 
        this.state = {}; 
        this.updater = new Updater(this); 
    } 
    setState(partialState, callback) { 
        this.updater.addState(partialState, callback); // 更新状态加到数组中
    } 
    forceUpdate() { 
        let oldRenderVdom = this.oldRenderVdom; // 上一次类组件render方法得到的虚拟dom
        let oldDOM = findDOM(oldRenderVdom); // 上一次虚拟Dom对应的真实dom
        let newRenderVdom = this.render(); 
        compareTwoVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);
        this.oldRenderVdom = newRenderVdom; 
    }
}

合成事件与批量更新

  • 采用小驼峰 onClick onMouseMove
  • 原生事件里传函数名字符串 在react中传一个函数的引用地址 真实的函数定义
  • 为了判断是批量异步更新还是同步更新, 引入了一个全局变量 updateQueue更新队列
    • 在事件函数执行之前 把isBatchingUpdate= true
    • 事件函数本身代码逻辑执行 setState 都添加到了数组当中
    • 事件函数执行完成之后 调用updateQueue.batchUpdate批量更新方法,在方法中会再次将isBatchingUpdate = false image.png
  • 为了在事件函数执行之前将isBatchingUpdate= true,就用到了合成事件

合成事件

  • 合成事件的实现原理是事件委托
    • 如要向button上绑定click事件,实际不向button上进行绑定,而是绑定到document上
    • 这样做的好处是
        1. 可以实现在事件开始的时候设置isBatchingUpdate= true,结束的时候注入批量更新的逻辑
        1. 可以做一些浏览器的兼容性处理。不同浏览器的api不一样,把不同的事件对象处理成处理成标准化的事件对象,提供标准化的api供用户使用
    • 事件委托
      • react17之前,事件都委托到了document文档对象
      • react17之后,事件都委托到了容器上
      • 这样做是为了在一个页面上可以存在多个react应用(以前的方式是不行的)
        • <div id="root1"/> ReactDDOM.render(<h1/>, root1);
        • <div id="root2"/> ReactDOM.render(<h2/>, root2);
    • 给元素绑定事件函数的时候执行的是addEvent方法
      • store是在要绑定事件函数的dom上的一个自定义属性 用于存放这个dom都绑定了哪些事件以及对应的函数
      • 如果很多元素都绑定了onClick事件,只会往文档对象document上绑一次onClick

image.png

ref

为dom添加ref

  • 可以使用 ref 去存储 DOM 节点的引用
  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性

为 class 组件添加 Ref

  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性

Ref转发

  • 你不能在函数组件上使用 ref 属性,因为他们没有实例
  • Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧
  • Ref 转发允许某些组件接收 ref,并将其向下传递给子组件
  • 通过React.forwardRef可以将ref在函数组件中作为参数向下传递,使用在原生dom上 image.png

生命周期