从0实现一个tiny react(一)

从0实现一个tiny react(一)

学习一个库的最好的方法就是实现一个, 注: 实际react的代码可能相去甚远。

支持JSX

react组件可以完全不用JSX, 用纯js来写。 JSX语法经过babel转化就是纯js代码, 譬如:

const hw = <div>Hello World</div>

const hw = React.createElement('div', null, "Hello World")

这两种是等效的。 babel 通过babylon 来把JSX转化为js
配置如下(transform-react-jsx):

{
  "presets": [
    "es2015"
  ],
  "plugins": [
    ["transform-react-jsx", {
      "pragma":  "createElement" // default pragma is React.createElement
    }]
  ]
}

所以对于react库本身的, 是不需要关心jsx语法的

渲染

react 中virtual-dom的概念, 使用一个 js的结构vnode来描述DOM 节点。 然后, 从vnode渲染出DOM树。
这个 vnode由3个属性描述:nodeName(div, Son...), props, children(vnode 组成的数组), 所以 createElement的最简实现

function createElement(comp, props, ...args) {
    let children = []
    for(let i = 0; i< args.length;i++){
        if(args[i] instanceof Array) {
            children = children.concat(args[i])
        } else {
            children.push(args[i])
        }
    }
    return {
        nodeName: comp,
        props: props || {},
        children
    }
}

从vnode 怎么渲染到dom? 先想一下我们在react里面书写下面的组件的时候

class Father extends Component {
    render() {
        return (<Son/>) // React.createElement(Son)  --> {nodeName: Son, props:{}, children:[]}
    }
}

class Son extends Component {
    render() {
        return (<Grandson/>) // React.createElement(Grandson) --> {nodeName: Grandson, props:{}, children:[]}
    }
}

/**
*React.createElement(
*                "div",
*                null,
*                "i",
*                React.createElement(
*                    "div",
*                    null,
*                    "am"
*                ),
*                React.createElement(GrandText, null)
*            );
*/
class Grandson extends Component { 
    render() {
        return (
            <div>
                i
                <div>am</div>
                <GrandText/>
            </div>
        ) 
    }
}

class GrandText extends  Component {
    render() {
        return (
            <div>grandson</div> // React.createElement(Grandson) 
        )
    }
}


render(<Father/>, document.getElementById('root'))

在react里, 最终渲染出来的就是一个i am grandson。
渲染的过程就是: 渲染Father的Vnode -> 渲染Son的Vnode -> 渲染Grandson的Vnode -> 渲染div -> 渲染i -> 渲染<div>am</div> -> 渲染GrandText。
显然这是一个递归的过程:递归的中止条件是 渲染html标签。

  1. 当 nodeName 是 html标签, 直接操作dom
  2. 当 nodeName 是 react组件 递归操作 组件render返回的vnode

暂时先不考虑 dom操作, 只考虑这个递归方法, 代码如下:

function renderVDOM(vnode) {
    if(typeof vnode == "string") { // 字符串 "i an grandson"
        return vnode
    } else if(typeof vnode.nodeName == "string") {
        let result = {
            nodeName: vnode.nodeName,
            props: vnode.props,
            children: []
        }
        for(let i = 0; i < vnode.children.length; i++) {   
            result.children.push(renderVDOM(vnode.children[i]))
        }
        return result
    } else if (typeof vnode.nodeName == "function") { // 如果是function
        let func = vnode.nodeName
        let inst = new func(vnode.props)
        let innerVnode = inst.render()
        return renderVDOM(innerVnode)
    }

执行上面的结构将返回 (jsfiddle演示地址)):

{
    "nodeName": "div",
    "props": {},
    "children": ["i", {"nodeName": "div", "props": {}, "children": ["am"]}, {
        "nodeName": "div",
        "props": {},
        "children": ["grandson"]
    }]
}

加入实际DOM操作, 代码如下:

function render(vnode, parent) {
    let dom
    if(typeof vnode == "string") {
        dom = document.createTextNode(vnode)
        parent.appendChild(dom)
    } else if(typeof vnode.nodeName == "string") {
        dom = document.createElement(vnode.nodeName)
        setAttrs(dom, vnode.props)
        parent.appendChild(dom)

        for(let i = 0; i < vnode.children.length; i++) {
            render(vnode.children[i], dom)
        }
    } else if (typeof vnode.nodeName == "function") {
        let func = vnode.nodeName
        
        let inst = new func(vnode.props)
        let innerVnode = inst.render()
        render(innerVnode, parent)
    }
}
function setAttrs(dom, props) {
    const allKeys = Object.keys(props)
    allKeys.forEach(k => {
        const v = props[k]

        if(k == "className") {
            dom.setAttribute("class", v)
            return
        }

        if(k == "style") {
            if(typeof v == "string") {
                dom.style.cssText = v
            }

            if(typeof v == "object") {
                for (let i in v) {
                    dom.style[i] =  v[i]
                }
            }
            return

        }

        if(k[0] == "o" && k[1] == "n") {
            const capture = (k.indexOf("Capture") != -1)
            dom.addEventListener(k.substring(2).toLowerCase(), v, capture)
            return
        }

        dom.setAttribute(k, v)
    })
}

渲染实际Hello World(jsfiddle演示地址)
总结一下:

  1. createElement 方法负责创建 vnode
  2. render 方法负责根据生成的vnode, 渲染到实际的dom的一个递归方法 (由于组件 最终一定会render html的标签。 所以这个递归一定是能够正常返回的)

    • vnode是字符串的是, 创建textNode节点
    • 当vnode.nodeName是 字符串的时候, 创建dom节点, 根据props设置节点属性, 遍历render children
    • 当vnode.nodeName是 function的时候, 获取render方法的返回值 vnode', 执行render(vnode')

props 和 state

v = f(props, state)。 组件的渲染结果由 render方法, props, state共同决定,之前只是讨论了render, 现在引入 props, state。

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

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

对于 state, 在组件的生命期内是可以修改的,当调用组件的setState方法的时候, 其实就是重新渲染 用一个新DOM树替换老的DOM:
parent.replaceChild (newdom, olddom ) ,
比如当我在 GrandText 上调用setState。 就是父div 把GrandText渲染出来的dom 替换一下。
所以

  1. 组件实例 必须有机制获取到 olddom
  2. 同时 render方法的第二个参数是 parent。 组件实例必须有机制获取到 parentDOM

这2个问题其实是一个问题。 parent = olddom.parentNode, 所以 olddom.parentNode.replaceChild (newdom, olddom ) 。 现在的关键就是获取到olddom,
这里采用的机制是 每个组件实例 记住 直接渲染出的组件实例/DOM(通过__rendered属性)。 下图:
实例引用关系

代码实现:

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
    }
}

其中 comp 参数代表 "我是被谁渲染的"。 获取olddom的代码实现:

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

调用 setState 使用olddom替换老的dom 代码如下:

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)
    }
}

有状态组件 演示地址, have fun!

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

其他

代码托管在github。 觉得有帮助,点个star。哈哈哈。。。
本文所讲的代码部分在 propsAndState 这个tag上:

git clone https://github.com/ykforerlang/tinyreact.git
git branch [yourbranchname] propsAndState

相关文章

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,我们需要定义tiny语言的语法规则。假设tiny语言是一种简单的命令式编程语言,它具有以下语法规则: 1. 一个程序由多个语句组成,语句之间用分号(;)分隔。 2. 语句可以是赋值语句、条件语句、循环语句或输出语句。 3. 赋值语句的语法为:变量名 = 表达式 4. 表达式可以是一个常量、一个变量或一个简单的算术表达式,支持加、减、乘、除四种运算。 5. 条件语句的语法为:if (条件表达式) 语句1 else 语句2 6. 循环语句的语法为:while (条件表达式) 语句 7. 输出语句的语法为:print (表达式) 根据上述规则,我们可以使用自顶向下的语法分析方法来构建tiny语言的语法分析器。 以下是一个简单的基于Python的实现代码: ``` class Parser: def __init__(self, lexer): self.lexer = lexer self.current_token = self.lexer.get_next_token() def error(self): raise Exception('Invalid syntax') def eat(self, token_type): if self.current_token.type == token_type: self.current_token = self.lexer.get_next_token() else: self.error() def factor(self): token = self.current_token if token.type == 'INTEGER': self.eat('INTEGER') elif token.type == 'LPAREN': self.eat('LPAREN') self.expr() self.eat('RPAREN') else: self.error() def term(self): self.factor() while self.current_token.type in ('MUL', 'DIV'): token = self.current_token if token.type == 'MUL': self.eat('MUL') elif token.type == 'DIV': self.eat('DIV') self.factor() def expr(self): self.term() while self.current_token.type in ('PLUS', 'MINUS'): token = self.current_token if token.type == 'PLUS': self.eat('PLUS') elif token.type == 'MINUS': self.eat('MINUS') self.term() def parse(self): self.expr() ``` 在这个实现中,我们首先定义了一个Parser类,它接受一个Lexer对象作为参数,并初始化当前token为第一个token。 然后,我们定义了一些辅助方法,如eat方法用于消耗当前token,如果当前token的type与传入的type不一致,则抛出异常;error方法用于处理语法错误。 接下来,我们定义了一些方法来解析tiny语言的不同类型的语句,如factor方法用于解析数字或括号中的表达式,term方法用于解析乘除法表达式,expr方法用于解析加减法表达式。 最后,我们定义了一个parse方法,它调用expr方法来解析整个表达式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值