react

118 阅读9分钟

jsx到react的转化

混合js和html,将组件的结构,数据,样式聚合在一起定义组件。

这3张图片,可以看出来,首字母大写,会被定义为组件,小写会被标记为字符串,当成原本的标签,被编译。

image.png image.png image.png

createElement(type, config, children)

packages/react/src/ReactElement.js

创建元素的时候,会过滤ref和key,然后把别的config属性变成props添加上去,同时利用arguments 接收所有的children,利用type.defaultProps设置,用户没有主动为组件添加的属性的默认值。

export function createElement(type, config, children) {
  let propName;

  // Reserved names are extracted
  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
  }

  // Children can be more than one argument, and those are transferred onto
  // the newly allocated props object.
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // Resolve default props
  if (type && type.defaultProps) {
    const defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

react 元素的更改特性

react的元素在重新渲染的时候,内容变化的元素是会被替换的,内容固定的是不会被替换的。

const tick = () => {
  const ele = (
    <div>
      <p>开始</p>
      <span>{new Date().toLocaleTimeString()}</span>
      <p>结束</p>
    </div>
  )
  ReactDOM.render(ele, document.getElementById('time'))
}


componentDidMount() {
    setInterval(tick, 1000);
}

image.png 可以看出date时间1秒重新渲染一次,所以,render的时候,span标签和span里面的内容会,重新变化,别的内容固定的元素,不会重新被渲染。

虚拟节点转化成真实节点

渲染原生组件

const element = (
  <div className='title' style={{color: 'red'}}>
    <span>hello</span>
    world
  </div>
)

console.log(JSON.stringify(element, null, 2));

ReactDOM.render(element, document.getElementById('root'))

jsx经过createElement转化之后,会得到虚拟dom

{
  "type": "div",
  "key": null,
  "ref": null,
  "props": {
    "className": "title",
    "style": {
      "color": "red"
    },
    "children": [
      {
        "type": "span",
        "key": null,
        "ref": null,
        "props": {
          "children": "hello"
        },
        "_owner": null,
        "_store": {}
      },
      "world"
    ]
  },
  "_owner": null,
  "_store": {}
}

render

reder方法接收vDom(虚拟dom)和挂载容器,然后想办法得到真实的dom,vDom上主要包含props,type,children

  • type 是元素的标签
  • peops 元素的属性
  • children 递归递归调用render
function render(vDom, container) {
  const dom = createDom(vDom) // 创建真正的dom
  container.appendChild(dom)
}

创建真实的dom

  • 如果vDom只是字符串或者数字,创建创建文本节点
  • vDom包含html标签,创建元素节点
    • 为元素节点绑定,除children之外的props,特别是style
    • 判断children,递归调用render,为当前的标签,插入子元素
function createDom(vDom) {
  if (typeof vDom === 'string' || typeof vDom === 'number') {
    return document.createTextNode(vDom)
  }
  const { type, props } = vDom;
  // 原生组件可以直接渲染
  let dom = document.createElement(type)
  updateProps(dom, props)
  if (typeof props.children === 'string' || typeof props.children === 'number') {
    dom.textContent = props.children
  } else if (typeof props.children === 'object' && props.children.type) {
    // 单个children
    render(props.children, dom)
  } else if (Array.isArray(props.children)) {
    // 多个children
    reconcileChilren(props.children, dom)
  } else {
    dom.textContent = props.children ? props.children.toString() : ''
  }
  return dom
}

function reconcileChilren(childrenVdom, parentDom) {
  for(let i=0; i<childrenVdom.length; i++) {
    let childVdom = childrenVdom[i];
    render(childVdom, parentDom)
  }

}

function updateProps(dom, newProps) {
  for(let key in newProps) {
    if (key === 'children') continue;
    if (key === 'style') {
      let styleObj = newProps[key];
      for(let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else {
      dom[key] = newProps[key]
    }
  }
}

渲染函数组件

function Welcome(props) {
  return (
    <div className='title' style={{color: 'red'}}>
    <span>hello</span>
    { props.children }
   </div>
  )
}

ReactDOM.render(
<Welcome>
  <span>world</span>
</Welcome>, 
document.getElementById('root'))

函数组件的渲染需要在,原生组件的渲染上,加上一些判断

函数组件的vDom中的,type是声明好的函数

{
  "type": "Welcome",
  "key": null,
  "ref": null,
  "props": {
    "children": {
      "key": null,
      "ref": null,
      "props": {
        "children": "world"
      },
      "_owner": null,
      "_store": {}
    }
  },
  "_owner": null,
  "_store": {}
}

所以在创建dom的时候增加判断createDom(vDom)

if (typeof type === 'function') {
    dom = mountFunctionComponent(vDom)
  } else {
    dom = document.createElement(type)
  }

执行函数组件,得到函数组件的vDom,然后执行createDom(vdom)

// 获取函数组件的vDom
function mountFunctionComponent(vDom) {
  let {type: functionComponent, props} = vDom;
  const renderVdom = functionComponent(props);
  return createDom(renderVdom);
}

渲染类组件

类组件和函数组件的vDom中,type的类型都是function,在创建真实的dom的时候,需要在判断类型的时候,使用类组件里面的static isReactCompoment = true来识别当前是类组件,类组件的vDom就是render()的返回值。

  • 类组件继承的Component
import { createDom } from './react-dom'
class Component {
  static isReactComponent = true; // 标识当前是classComponent
  constructor(props) {
    this.props = props;
    this.state = {};
  }
  setState(partialState) {
    let state = this.state
    this.state = {...state, ...partialState}
    // 组件根据state 更新数据
    let newVdom = this.render();
    updateClassComponent(this,newVdom)
  }
}

function updateClassComponent(classInstance, newVdom) {
  let oldDom = classInstance.dom;
  let newDom = createDom(newVdom);
  oldDom.parentNode.replaceChild(newDom, oldDom);
  classInstance.dom = newDom;
}

export default Component;
  • createDom(vDom)识别类组件的虚拟dom,创建真实的dom
if (typeof type === 'function') {
    // 类组件
    if (type.isReactComponent) {
      return mountClassComponent(vDom)
    } else {
      return mountFunctionComponent(vDom)
    }
    } else {
    dom = document.createElement(type)
    }
}
  • 创建类组件的真实dom
function mountClassComponent(vDom) {
  const {type: ClassComponent, props} = vDom;
  const classInstance = new ClassComponent(props);
  const renderVdom = classInstance.render(); // 虚拟dom
  let dom = createDom(renderVdom);
  classInstance.dom = dom; // 保留当前的dom,同于下次更新,替换dom,获取dom的父元素
  return dom
}
  • updateProps(dom, props)处理onClick事件
if (key.startsWith('on')) {
  dom[key.toLocaleLowerCase()] = newProps[key];
}
  • 根据数据,更新元素 根据新的新的虚拟dom,创建真实的dom之后,需要根据之前mountClassComponent创建dom时,保存的dom,找到dom的父元素,替换成最新的真实的dom

这个过程发生在Component类中

setState更新了数据,同时调用updateClassComponent,更新dom

 setState(partialState) {
    let state = this.state
    this.state = {...state, ...partialState}
    // 组件根据state 更新数据
    let newVdom = this.render();
    updateClassComponent(this,newVdom)
}
function updateClassComponent(classInstance, newVdom) {
  let oldDom = classInstance.dom;
  let newDom = createDom(newVdom);
  oldDom.parentNode.replaceChild(newDom, oldDom);
  classInstance.dom = newDom;
}

updateClassComponent的组件目前是暴力替换元素的,消耗性能。之后的流程会优化

合成事件批量更新

setState事件在react的流程里面是会多个合并执行的,但是当放入异步事件里面就会安顺序执行。

handlerClick = () => {
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count)
    this.setState({
      count: this.state.count + 1
    })
    console.log(this.state.count)
    setTimeout(() => {
      console.log(this.state.count)
      this.setState({
        count: this.state.count + 1
      })
      console.log(this.state.count)
      this.setState({
        count: this.state.count + 1
      })
      console.log(this.state.count)
    }, 0)
}
// 0, 0, 1, 2, 3

把setTimeout换成promise得到结果也是一样的

手动实现批量更新/非批量更新

  • 手动实现立即更新
import { createDom } from './react-dom'
export let updateQueue = {
  isBatchUpdate: false, // 触发setState的时候,是否批量更新 false 不批量更新
  updaters: new Set()
}
// 更新组件
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);
    }
    if (updateQueue.isBatchUpdate) {
      // 批量更新
      updateQueue.updaters.add(this)
    } else {
      // 单独更新
      this.updateClassComponent()
    }
  }
  updateClassComponent() {
    let {classInstance, pendingStates, callbacks} = this;
    if (pendingStates.length > 0) {
      classInstance.state = this.getState() // 获取新的状态
      classInstance.forceUpdate()
      callbacks.forEach(callback => callback())
    }
  }

  getState() {
    let {classInstance, pendingStates } = this;
    let { state } = classInstance // 旧状态
    // pendingStates是新状态
    pendingStates.forEach((nextState) => {
      if (typeof nextState === 'function') {
        nextState = nextState(state)
      }
      state = {...state, ...nextState}
    })
    pendingStates.length = 0;
   
    return state;
  }
}
class Component {
  static isReactComponent = true; // 标识当前是classComponent
  constructor(props) {
    this.props = props;
    this.state = {};
    this.updater = new Updater(this);
  }
  setState(partialState, callback) {
    this.updater.addState(partialState, callback);
  }

  forceUpdate() {
    let newVdom = this.render();
    updateClassComponent(this, newVdom);
  }
}

function updateClassComponent(classInstance, newVdom) {
  let oldDom = classInstance.dom;
  let newDom = createDom(newVdom);
  oldDom.parentNode.replaceChild(newDom, oldDom);
  classInstance.dom = newDom;
}

export default Component;
  • 手动实现批量更新

    componnet.js

import { createDom } from './react-dom'
export let updateQueue = {
  isBatchingUpdate: false, // 触发setState的时候,是否批量更新 false 不批量更新
  updaters: new Set(), // 存放所有的更新Updater实例
  batchUpdate() { // 批量更新
    for(let updater of this.updaters) {
      updater.updateClassComponent()
    } 
    this.isBatchingUpdate = false; 
  }
}
// 更新组件
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);
    }
    if (updateQueue.isBatchingUpdate) {
      // 批量更新
      updateQueue.updaters.add(this)
    } else {
      // 单独更新
      this.updateClassComponent()
    }
  }
  updateClassComponent() {
    let {classInstance, pendingStates, callbacks} = this;
    if (pendingStates.length > 0) {
      classInstance.state = this.getState() // 获取新的状态
      classInstance.forceUpdate()
      callbacks.forEach(callback => callback())
      callbacks.length = 0; // 清空callback
    }
  }

  getState() {
    let {classInstance, pendingStates } = this;
    let { state } = classInstance // 旧状态
    // pendingStates是新状态
    pendingStates.forEach((nextState) => {
      if (typeof nextState === 'function') {
        nextState = nextState(state)
      }
      state = {...state, ...nextState}
    })
    pendingStates.length = 0;   
    return state;
  }
}
class Component {
  static isReactComponent = true; // 标识当前是classComponent
  constructor(props) {
    this.props = props;
    this.state = {};
    this.updater = new Updater(this);
  }
  setState(partialState, callback) {
    this.updater.addState(partialState, callback);
  }

  forceUpdate() {
    let newVdom = this.render();
    updateClassComponent(this, newVdom);
  }
}

function updateClassComponent(classInstance, newVdom) {
  let oldDom = classInstance.dom;
  let newDom = createDom(newVdom);
  oldDom.parentNode.replaceChild(newDom, oldDom);
  classInstance.dom = newDom;
}

export default Component;

event.js

import { updateQueue } from './component';

export function addEvent(dom, eventType, listener) {
  // listener 代表绑定的事件内容,即函数的主体
  // eventType 代表绑定的事件 例如onClick 
  let store = dom.store || (dom.store = {}); 
  store[eventType] = listener
  if (!document[eventType]) {
    document[eventType] = dispatchEvent; // 事件委托,把事件委托到document
  }
}

let syntheticEvent = {}; 

function dispatchEvent(event) {
  let { target, type } = event;
  // target上会有store属性,应为原型链机制
  let eventType = `on${type}`;
  updateQueue.isBatchingUpdate = true; // 打开批量更新
  createSyntheticEvent(event)
  while(target) {
    // while 实现冒泡机制
    let { store } = target;
    let listener = store && store[eventType];
    listener && listener.call(target, syntheticEvent);
    target = target.parentNode; 
  }
 
  for(let key in syntheticEvent) {
    syntheticEvent[key] = null;
  }
  updateQueue.batchUpdate() 
}

function createSyntheticEvent(nativeEvent) {
  for(let key in nativeEvent) {
    syntheticEvent[key] = nativeEvent[key] 
  }
}

react-dom.js

function updateProps(dom, newProps) {
  for(let key in newProps) {
    if (key === 'children') continue;
    if (key === 'style') {
      let styleObj = newProps[key];
      for(let attr in styleObj) {
        dom.style[attr] = styleObj[attr];
      }
    } else if (key.startsWith('on')) {
      addEvent(dom, key.toLocaleLowerCase(), newProps[key])
    } else {
      dom[key] = newProps[key]
    }
  }
}

总结:

  • 组件被创建的时候,会在Componnet内部同步创建一个Updater的实例,这个Updater实例里面会保存,当前的组件实例,组件的state,和相关callback,同时这个new Updater实例也会被挂载在Component上。可以看出ComponentUpdater相互都拥有对方的实例信息。
  • 更新发生在setState的时候,在setState被触发的时候,会调用Updater里面的addState来收集当前的statecallback,都放入相应的数组中
  • 批量更新实现,主要靠updateQueueupdateQueue里搜集到了所有的Updater,每次触发setState的时候,都根据updateQueue.isBatchingUpdate来判断当前是批量更新还是单个更新,当updateQueue.isBatchingUpdate=true的时候,需要批量更新,当前的Updater就被存放在updateQueue.updaters里,当需要更新的时候,调用updateQueue.batchUpdate() 来循环所有的Updater,更新组件的state执行setState方法中的callback函数和dom结构。
  • 一个组件对应一个Updater
  • 事件委托的机制,可以抹平,平台间的差异。并且在用操作的事件里,添加一个额外的操作,例如设置updateQueue.isBatchingUpdate=true开启批量更新。因为批量更新是react内部控制的,根据js的事件循环机制,可以知道,setTimeout和promise内部写setState都是执行单个更新的流程。

react的dom-diff

dom-diff的比较是同级比较的,并且按照顺序由前向后

组件更新的时候,比较新旧的vdom,按照如下的规则

  • oldVdom,newVdom都不存在,不做处理
  • oldVdom存在,newVdom不存在,通过oldVdom查找当前真实dom节点,然后删除当前节点
  • oldVdom不存在,newVdom存在,通过newVdom创建一个真实的dom,在判断后面有没有兄弟节点,有的话,插入兄弟节点的前面,没有的话,直接插入
  • oldVdom,newVdom都存在,但是type类型不一致,根据newVdom创建真实dom,并且替换旧的dom
  • oldVdom,newVdom都存在,但是type类型一致,直接从oldVdom上获取真实的dom, 然后判断一个type的类型
    • string类型,更新props和children
    • function类型,在判断是类,还是函数
      • 类通过render获取vdom调用实例上的updater.emitUpdate最终更新props和state
      • 函数执行函数获取vdom,然后继续比较孩子

ref

React.createRef

得到一个current属性为空的对象,在创建dom的时候,从虚拟节点上获取ref,并把真实的dom赋值给ref

function createRef() {
  return { current: null }
}
if (ref) {
    ref.current = dom;
}

React.useRef

值被保存在hookState中,所以useRef的值永远只想同一个地址

export function useRef(initVal) {
  hookState[hookIndex] = hookState[hookIndex] || {current: initVal}
  return  hookState[hookIndex++]
}

React.forwardRef

让函数组件使用ref属性,内部返回一个继承了Component组件的类组件, Component的组件实例上挂有ref

function forwardRef(FunctionComponent) {
  return class extends Component {
    render() {
      if (FunctionComponent.length < 2) {
        console.error('===props, ref 参数缺一不可')
      }
      return FunctionComponent(this.props, this.ref);
    }
  }
}

React.useReducer

useReducer接受一个reducer函数,和初始值,内部返回一个dispatch函数体,这个函数内部,调用reducer函数,并且传入对应的参数

export function useReducer(reducer, initVal) {
  hookState[hookIndex] = hookState[hookIndex] || (typeof initVal === 'function' ? initVal() : initVal);
  let currentIndex = hookIndex;
  function dispatch(action) {
    // useState调用useReducer的时候,dispatch肯能接受的是一个函数
    let lastState = hookState[currentIndex];
    let nextState;
    if(typeof action === 'function') {
      nextState = action(lastState)
    } else {
      nextState = action
    }
    if (reducer) {
      nextState = reducer(nextState, action)
    }
    hookState[currentIndex] = nextState;
    scheduleUpdate(); // 状态改变之后,更新应用
  }
  return [hookState[hookIndex++], dispatch]
}

React.useState

return useReducer(null, initVal)

React.useEffect

  • 接受函数,当前hookState里面没有对应的值,就执行函数(这个函数要放在宏任务中,保证在dom加载完成之后执行),把函数的返回值和依赖保存在hookstate中
  • 当前hookState里面有对应的值,判断依赖有没有改变,改变了,执行返回值函数,重新保存新的callback产生的结果函数和依赖,以来没改变只需更新hookIndex
export function useEffect(callback, deps) {
  if (hookState[hookIndex]) {
    let [destoryFactory, lastDeps] = hookState[hookIndex]
    const same = deps && deps.every((item, index) => (item === lastDeps[index]))
    if (same) {
      hookIndex++
    } else {
      destoryFactory && destoryFactory();  // effect 的 return 函数
      setTimeout(() => {
        let destoryFactory = callback()
        hookState[hookIndex++] = [destoryFactory, deps]
      })
    }

  } else {
    // setTimeout 是为了模拟宏任务,让effect在dom加载完毕之后执行
    setTimeout(() => {
      let destoryFactory = callback()
      hookState[hookIndex++] = [destoryFactory, deps]
    })
    
  }
}

React.useMemo/React.memo

React.memo

减少函数组件的更新次数,当子组件用memo包裹的时候,当自己的数据没有变化的时候,就不会更新,要配和useMemo使用

function memo(FunctionComponent) {
  return class extends PureComponent {
    render() {
      return FunctionComponent(this.props)
    }
  }
}

React.useMemo

可以缓存变量,hookState里面有对应的值的时候,依赖项不变的话,数据不变,依赖改变,传入的函数。重新调用

export function useMemo(factory, deps) {
  if (hookState[hookIndex]) {
    let [lastMemo, lastDeps] = hookState[hookIndex]
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      hookIndex++;
      return lastMemo;
    } else {
      let newMemo = factory();
      hookState[hookIndex++] = [newMemo, deps];
      return newMemo
    }
  } else {
    let newMemo = factory();
    hookState[hookIndex++] = [newMemo, deps];
    return newMemo
  }
}

React.useCallback

可以缓存方法

export function useCallback(callback, deps) {
  if (hookState[hookIndex]) {
    let [lastCallback, lastDeps] = hookState[hookIndex]
    let same = deps.every((item, index) => item === lastDeps[index]);
    if (same) {
      hookIndex++;
      return lastCallback;
    } else {
      hookState[hookIndex++] = [callback, deps];
      return callback
    }
  } else {
    hookState[hookIndex++] = [callback, deps];
    return callback
  }
}

useMemo保存函数体执行之后的值,而useCallback保存函数体

React.createContext

获取用户传入的props.value值,并且绑定在context上。

function createContext(initVal = {}) {
  let context = { Provider, Consumer }
  function Provider(props) {
    context._currentValue = context._currentValue || initVal
    Object.assign(context._currentValue, props.value)
    return props.children;
  }

  function Consumer(props) {
    return props.children(context._currentValue)
  }

  return context;
}

React.useContext

只是返回React.createContext()_currentValue

function useContext(context) {
  return context._currentValue;
}