从 0 实现一个 tiny react(一)
注: 首发于 segmentfault, 现迁移到掘金...
学习一个库的最好的方法就是实现一个, 实际 react 的代码可能相去甚远.
支持 JSX
react 组件可以完全不用 JSX, 用纯 js 来写. JSX 语法经过 babel 转化就是纯 js 代码, 譬如:
这两种是等效的. babel 通过 babylon 来把 JSX 转化为 js 配置如下 ( transform-react-jsx ):
const hw = <div > Hello World < /div>
const hw = React.createElement('div', null, "Hello World")/
所以对于 react 库本身的, 是不需要关心 jsx 语法的
{
"presets": [
"es2015"
],
"plugins": [
["transform-react-jsx", {
"pragma": "createElement" // default pragma is React.createElement
}]
]
}
渲染
react 中 virtual-dom 的概念, 使用一个 js 的结构 vnode 来描述 DOM 节点. 然后, 从 vnode 渲染出 DOM 树. 这个 vnode 由 3 个属性描述:nodeName(div, Son...), props, children(vnode 组成的数组), 所以 createElement 的最简实现
从 vnode 怎么渲染到 dom? 先想一下我们在 react 里面书写下面的组件的时候
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
}
}
在 react 里, 最终渲染出来的就是一个 i am grandson. 渲染的过程就是: 渲染 Father 的 Vnode -> 渲染 Son 的 Vnode -> 渲染 Grandson 的 Vnode -> 渲染 div -> 渲染 i -> 渲染 am -> 渲染 GrandText. 显然这是一个递归的过程:递归的中止条件是 渲染 html 标签.
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'))
当 nodeName 是 html 标签, 直接操作 dom
当 nodeName 是 react 组件 递归操作 组件 render 返回的 vnode
暂时先不考虑 dom 操作, 只考虑这个递归方法, 代码如下:
执行上面的结构将返回 ( jsfiddle 演示地址 )):
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)
}
加入实际 DOM 操作, 代码如下:
{
"nodeName": "div",
"props": {},
"children": ["i", {"nodeName": "div", "props": {}, "children": ["am"]}, {
"nodeName": "div",
"props": {},
"children": ["grandson"]
}]
}
渲染实际 Hello World( jsfiddle 演示地址 ) 总结一下:
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)
})
}
createElement 方法负责创建 vnode
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
对于 state, 在组件的生命期内是可以修改的,当调用组件的 setState 方法的时候, 其实就是重新渲染 用一个新 DOM 树替换老的 DOM:
class Component {
constructor(props) {
this.props = props
}
}
parent.replaceChild(newdom, olddom)
, 比如当我在 GrandText 上调用 setState. 就是父 div 把 GrandText 渲染出来的 dom 替换一下.
所以
组件实例 必须有机制获取到 olddom
同时 render 方法的第二个参数是 parent. 组件实例必须有机制获取到 parentDOM 这 2 个问题其实是一个问题. parent = olddom.parentNode, 所以
olddom.parentNode.replaceChild(newdom, olddom)
. 现在的关键就是获取到 olddom, 这里采用的机制是 每个组件实例 记住 直接渲染出的组件实例/DOM(通过__rendered 属性). 下图:
代码实现:
其中 comp 参数代表 "我是被谁渲染的". 获取 olddom 的代码实现:
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
}
}
调用 setState 使用 olddom 替换老的 dom 代码如下:
function getDOM(comp) {
let rendered = comp.__rendered
while (rendered instanceof Component) { //判断对象是否是dom
rendered = rendered.__rendered
}
return rendered
}
拼凑一下以上功能, 完整代码实现:
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)
}
}
有状态组件 演示地址 , have fun!
///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 的引用链
其他
代码托管在 github . 觉得有帮助,点个 star.哈哈哈... 本文所讲的代码部分在 propsAndState 这个 tag 上:
git clone https: //github.com/ykforerlang/tinyreact.git
git branch[yourbranchname] propsAndState
来源: https://juejin.im/post/5a56fe856fb9a01cbe653b15