inpm
! dlrow olleH

手写一个精简版React

2018-12-30 React
Word count: 899 | Reading time: 4min

先贴原文,respect!以下是总结思考

200行代码实现简版react


React = Component + jsx + vdom

  • Component = (class component) + (functional component)

    element(typeof type=’string’?’dom节点’:’组件实例’)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Button(props) {}

    {
    type: 'button'||Button
    props: {
    className: 'button button-blue',
    children: {
    type: 'button-children',
    props: {
    children: 'nb!'
    }
    }
    }
    }

    提问:React如何将jsx语法转换为纯对象?
    答案:组件类返回值经过Babel(DiyReact.createElement)
    输出var foo = createElement('div',{id:'foo'},'nb!');

    1
    2
    3
    4
    5
    6
    7
    8
    // 转换为element对象
    function createElement(type, props, ...children) {
    props = Object.assign({},props);
    props.children = [].concat(...children)
    .filter(child => child != null && child !== false)
    .map(child => child instanceof Object ? child : createTextElement(child))
    return {type, props}
    }

    element && dom

    dom

  • ViewModel = vdomTree + diffAlgori

    vdomTree

    creatElement()函数最终输出一个对象,那我们可以对其中的props或者state进行监听与更新(观察者模式)
    整个页面对应一颗可以实时更新状态的虚拟dom节点树🌲

    diffAlgori(避免删除重建dom节点的开销)

    diff算法

  • ReactDOM.render()
    react入口即是第一帧,所以此后的帧只会在挂载期diff
    如:compentDidMount、componentWillUnmount

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // rootInstance用来缓存一帧vdom
    let rootInstance = null,这里即是第一帧
    function render(element, parentDom) {
    // prevInstance指向前一帧
    const prevInstance = rootInstance // 这里实现的仅是ReactDOM.render,非组件render
    // element参数指向新生成的vdomTree
    const nextInstance = reconcile(parentDom, prevInstance, element) // 对比前后帧
    // 调用完reconcile算法(即diff算法)后将rootInstance指向最新一帧
    rootInstance = nextInstance
    }

    提问:你这里用instance表示帧,那跟前面的element有什么关系?
    答案:instance = element + dom
    在对比前后帧调用reconcile()进行diff比较时需要更新应用到真实dom
    由此还需要实现一个instantiate函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    function instantiate(element) {
    const {type, props={}} = element
    const isDomElement = typeof type === 'string'
    // 区分函数式组件与类组件
    const isClassElement = !!(type.prototype && type.prototype.isReactComponent)
    if(isDomElement) {
    // 创建dom
    const isTextElement = type === TEXT_ELEMENT
    const dom = isTextElement ?
    document.createTextNode('') :
    document.createElement(type)
    // 设置dom属性,绑定dom事件
    updateDomProperties(dom, [], element.props)
    const children = props.children || []
    const childInstances = children.map(instantiate) // 子节点递归
    const childDoms = childInstances.map(childInstance => childInstance.dom)
    childDoms.forEach(childDom => dom.appendChild(childDom))
    const instance = {element, dom, childInstances}
    return instance
    } else if(isClassElement) {
    const instance = {}
    const publicInstance = createPublicInstance(element, instance)
    const childElement = publicInstance.render()
    const childInstance = instantiate(childElement) // 子节点递归
    Object.assign(instance, {dom: childInstance.dom, element, childInstance, publicInstance})
    reuturn instance
    } else {
    const childElement = type(element.props)
    const childInstance = instantiate(childElement) // 子节点递归
    const instance = {
    dom: childInstance.dom,
    element,
    childInstance
    }
    return instance
    }
    }

    提问:你说这么多我也不想看啊,最核心的对比前后帧是怎么实现的呢?
    答案:reconcile()
    由stack算法(递归实现,阻塞线程)升级为React16中fiber架构(requestIdleCallback)
    reconcile

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
      function reconcile(parentDom, instance, element) {
    if (instance === null) {
    const newInstance = instantiate(element);
    // componentWillMount
    newInstance.publicInstance
    && newInstance.publicInstance.componentWillMount
    && newInstance.publicInstance.componentWillMount();
    parentDom.appendChild(newInstance.dom);
    // componentDidMount
    newInstance.publicInstance
    && newInstance.publicInstance.componentDidMount
    && newInstance.publicInstance.componentDidMount();
    return newInstance;
    } else if (element === null) {
    // componentWillUnmount
    instance.publicInstance
    && instance.publicInstance.componentWillUnmount
    && instance.publicInstance.componentWillUnmount();
    parentDom.removeChild(instance.dom);
    return null;
    } else if (instance.element.type !== element.type) {
    const newInstance = instantiate(element);
    // componentDidMount
    newInstance.publicInstance
    && newInstance.publicInstance.componentDidMount
    && newInstance.publicInstance.componentDidMount();
    parentDom.replaceChild(newInstance.dom, instance.dom);
    return newInstance;
    } else if (typeof element.type === 'string') {
    updateDomProperties(instance.dom, instance.element.props, element.props);
    instance.childInstances = reconcileChildren(instance, element);
    instance.element = element;
    return instance;
    } else {
    if (instance.publicInstance
    && instance.publicInstance.shouldcomponentUpdate) {
    if (!instance.publicInstance.shouldcomponentUpdate()) {
    return;
    }
    }
    // componentWillUpdate
    instance.publicInstance
    && instance.publicInstance.componentWillUpdate
    && instance.publicInstance.componentWillUpdate();
    instance.publicInstance.props = element.props;
    const newChildElement = instance.publicInstance.render();
    const oldChildInstance = instance.childInstance;
    const newChildInstance = reconcile(parentDom, oldChildInstance, newChildElement);
    // componentDidUpdate
    instance.publicInstance
    && instance.publicInstance.componentDidUpdate
    && instance.publicInstance.componentDidUpdate();
    instance.dom = newChildInstance.dom;
    instance.childInstance = newChildInstance;
    instance.element = element;
    return instance;
    }
    }

    function reconcileChildren(instance, element) {
    const { dom, childInstances } = instance;
    const newChildElements = element.props.children || [];
    const count = Math.max(childInstances.length, newChildElements.length);
    const newChildInstances = [];
    for (let i = 0; i < count; i++) {
    newChildInstances[i] = reconcile(dom, childInstances[i], newChildElements[i]);
    }
    return newChildInstances.filter(instance => instance !== null);
    }

Author: inpm.cy@gmail.com

Link: https://inpm.top/2018/12/30/React-stack/

Copyright: All articles in this blog are licensed under inpm unless stating additionally.

< PreviousPost
React16-fiber
NextPost >
关于团队
CATALOG
  1. 1. 先贴原文,respect!以下是总结思考
  2. 2. React = Component + jsx + vdom
    1. 2.1. element(typeof type=’string’?’dom节点’:’组件实例’)
    2. 2.2. element && dom
    3. 2.3. vdomTree
    4. 2.4. diffAlgori(避免删除重建dom节点的开销)