从0实现一个tiny 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语法的。 上面的配置已经将JSX编译为js,交给了react
渲染
react 中virtual-dom的概念, 使用一个 js的结构vnode来描述DOM 节点。 然后, 从vnode构渲染出DOM树。
这个 vnode由3个属性描述:nodeName(div,Son...),props,children(vnode 组成的数组),所以 createElement的最简实现
function createElement(comp,...args) { return { nodeName: comp,props: props || {},children: args || [] } }
从vnode 渲染到dom, 考虑下面的结构
class Grandson extends Component { render() { return React.createElement('div',"i am grandson") //<div>i am grandson</div> } } class Son extends Component { render() { return React.createElement(Grandson) // <Grandson/> } } class Father extends Component { render() { return React.createElement(Son)//<Son/> } }
最终渲染出来的就是一个DOM (一个 div 包含一个TextNode (i am grandson)), 渲染的过程就是递归的处理Component的render, 直到遇到html标签
当 nodeName 是 html标签, 直接操作dom
当 nodeName 是 react组件 递归操作 组件render返回的vnode
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") { let func = vnode.nodeName let inst = new func(vnode.props) let innerVnode = inst.render() return renderVDOM(innerVnode) }
执行上面的结构将返回 (jsfiddle演示地址)):
{ "nodeName": "div","props": {},"children": [ "i am 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演示地址)
总结一下:
createElement 方法负责创建 vnode
-
render 方法负责根据生成的vnode, 渲染到实际的dom的一个递归方法 (由于组件 最终一定会render html的标签。 所以这个递归一定是能够正常返回的)
props 和 state
f(props,state) => v 。 组件的渲染结果由 render方法, props, state决定。 基类Component 设置props
function render(vnode,parent) { ... } else if (typeof vnode.nodeName == "function") { let func = vnode.nodeName let inst = new func(vnode.props) let innerVnode = inst.render() // this.props render(innerVnode,parent) } ... } class Component { constructor(props) { this.props = props } }
对于 state,当调用组件的setState方法的时候,简单来说就是渲染一个新DOM树, 替换老的DOM。 所以
组件实例 必须有机制获取到 olddom
同时 render方法的第二个参数是 parent。 组件实例必须有机制获取到 parentDOM
这2个问题其实是一个问题。 parent = olddom.parentNode 。 这里采用的机制是 每个组件实例 记住 直接渲染出的组件实例/DOM。 下图:
代码实现:
function render (vnode,parent,comp) { let dom if(typeof vnode == "string") { ... comp && (comp.__rendered = dom) ... } else if(typeof vnode.nodeName == "string") { ... comp && (comp.__rendered = dom) ... } else if (typeof vnode.nodeName == "function") { ... comp && (comp.__rendered = inst) ... } }
其中 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,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,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,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) } } 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,olddom) } }
有状态组件 演示地址,have fun!
总结一下: render方法负责把vnode渲染到实际的DOM, 如果组件渲染的DOM已经存在, 就替换, 并且保持一个 __rendered的引用链
敬请期待
[从0实现一个tiny react(二) virtual-dom]()
[从0实现一个tiny react(三) 生命周期]()
原文链接:https://www.f2er.com/react/303058.html