17

mini-react 实现原理讲解 第二讲

 3 years ago
source link: https://segmentfault.com/a/1190000025185299
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
文章首发于我的博客 https://github.com/mcuking/bl...

相关代码请查阅 https://github.com/mcuking/bl...

v = f(props, state)
组件的渲染结果由 render,props,state 共同决定,上一讲只是讨论了 render,本讲开始讨论 props 和 state。

props

对于 props, 父组件传递过来, 不可变,由基类 Component 设置 props。

class Component {
    constructor(props) {
        this.props = props
    }
}

state

对于 state, 在组件的生命期内是可以修改的,当调用组件的 setState 方法的时候, 其实就是重新渲染,用一个新 DOM 树替换老的 DOM:

parent.replaceChild (newdom, olddom)

因此我们需要解决两个问题:

  1. 组件实例必须有机制获取到 olddom
  2. 组件实例必须有机制获取到 parentDOM

这 2 个问题其实是一个问题。 parent = olddom.parentNode, 所以上一行代码等价于:

olddom.parentNode.replaceChild (newdom, olddom)

现在的关键就是获取到 olddom,采用的机制是:

将每个组件实例直接渲染出的组件实例 / DOM 设置 为该组件实例的 rendered 属性,形成一个__rendered 链

例如上一讲的组件嵌套案例的__rendered 链如下:

Animal --__rendered--> Pet --__rendered--> Cat --__rendered--> div

通过以下代码实现完整的__rendered 链,其中 comp 参数代表 "我是被谁渲染的":

function render (vnode, parent, comp) {
    let dom
    if(typeof vnode == "string") {
        const dom = ...  // 创建文本节点
        comp && (comp.__rendered = dom)
        ...  // other op
    } else if(typeof vnode.nodeName == "string") {
        const dom = ... // 创建 dom 节点
        comp && (comp.__rendered = dom)
        ... // other op
    } else if (typeof vnode.nodeName == "function") {
        const inst = ... // 创建 组件实例
        comp && (comp.__rendered = inst)
        ... // other op
    }
}

当第一次渲染形成了完整的__rendered 链后,再次渲染(通过 setState 等)时,即可通过当前渲染的组件实例,沿着__rendered 链向下找到实际渲染的 dom 节点,即 olddom。从而获得 parent,即 olddom.parentNode。

// 找到当前组件实例渲染的的实际的 DOM 节点
function getDOM(comp) {
    let rendered = comp.__rendered
    // 通过__render 链向下找到第一个非组件的 dom 节点
    while (rendered instanceof Component) {
        rendered = rendered.__rendered
    }
    return rendered
}

进而调用 setState,使用 dom 替换 olddom,代码如下:

function render(vnode, parent, comp, olddom) {
    let dom
    if(typeof vnode == "string") {
        ...
        if(olddom) {
            parent.replaceChild(dom, olddom)
        } else {
            parent.appendChild(dom)
        }
        ...
    } else if(typeof vnode.nodeName == "string") {
        ...
        if(olddom) {
            parent.replaceChild(dom, olddom)
        } else {
            parent.appendChild(dom)
        }
        ...
    } else if (typeof vnode.nodeName == "function") {
        ...
        render(innerVnode, parent, inst, olddom)
    }
}

完整功能如下:

//Component
class Component {
    constructor(props) {
        this.props = props
    }

    setState(state) {
        setTimeout(() => {
            this.state = state
            const vnode = this.render()
            let olddom = getDOM(this)
            render(vnode, olddom.parentNode, this, olddom)
        }, 0)
    }
}


function getDOM(comp) {
    let rendered = comp.__rendered
    while (rendered instanceof Component) { // 判断对象是否是 dom
        rendered = rendered.__rendered
    }
    return rendered
}

//render
function render (vnode, parent, comp, olddom) {
    let dom
    if(typeof vnode == "string" || typeof vnode == "number") {
        dom = document.createTextNode(vnode)
        comp && (comp.__rendered = dom)
        parent.appendChild(dom)

        if(olddom) {
            parent.replaceChild(dom, olddom)
        } else {
            parent.appendChild(dom)
        }
    } else if(typeof vnode.nodeName == "string") {
        dom = document.createElement(vnode.nodeName)

        comp && (comp.__rendered = dom)
        setAttrs(dom, vnode.props)

        if(olddom) {
            parent.replaceChild(dom, olddom)
        } else {
            parent.appendChild(dom)
        }

        for(let i = 0; i < vnode.children.length; i++) {
            render(vnode.children[i], dom, null, null)
        }
    } else if (typeof vnode.nodeName == "function") {
        let func = vnode.nodeName
        let inst = new func(vnode.props)

        comp && (comp.__rendered = inst)

        let innerVnode = inst.render(inst)
        render(innerVnode, parent, inst, olddom)
    }
}

总结

render 方法负责把 vnode 渲染到实际的 DOM, 如果组件渲染的 DOM 已经存在就替换, 并且保持一个完整的 __rendered 的引用链


Recommend

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK