学习并实现react (1)

搭建学习环境

npm install -g parcel-bundler

package.json

{
  "name": "myReact",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "parcel index.html",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "babel-preset-env": "^1.6.1",
    "babel-preset-es2015": "^6.24.1",
    "parcel-bundler": "^1.6.2"
  },
  "devDependencies": {
    "babel-plugin-transform-react-jsx": "^6.24.1"
  }
}

支持 JSX

在 js 文件中我们是不能写 jsx 语法的,必须使用一种 babel 插件 transform-react-jsx 才能使用。
新建.babelrc

{
  "presets": ["env"],
  "plugins": [
    ["transform-react-jsx", {
      "pragma": "createElement"
    }]
  ]
}

这样我们在写React 组件时 babel 会帮我们自动编译成

实现一个 createElement

.babelrc 文件中使用了transform-react-jsx 插件,告诉babel 解析 jsx 需要 createElement方法,也就是 babel 编译后的React.createElement

createElement 有三个参数

function createElement(type, props, ...args) {
    props = props || {}
    let children = []
    for (let i = 0; i < args.length; i++) {
        if (Array.isArray(args[i])) {
          children = [ ...children, ...args[i] ]
        } else {
          children = [ ...children, args[i] ]
        }
    }

  return { type, props, children }
}

然后我们来试验下createElement 结果

import { createElement } from './src/react'

const React = {}
React.createElement = createElement
React.Component = class Component {}

class App extends React.Component {
  render() {
    return (
      <div>
        <span>App</span>
        <span>component</span>
      </div>
    )
  }
}

const app = new App().render()
console.log(app)

new App().render() 这种格式跟 react 组件区别有些大,再实现一个renderVDOM(<App />) 的格式

function reactVDOM(vnode) {
    if (typeof vnode === 'string') return vnode // 文本节点
    if (typeof vnode.type === 'string') { // type 为标签名 - dom节点
        let ret = {
            type: ret.type,
            props: ret.props,
            children: []
        }
        for (let i = 0; i < vnode.children.length; i++) {
            ret.children.push(reactVDOM(vnode.children[i])) // 递归children
        }
        return ret 
    } 
    if (typeof vnode.type === 'function') { // type 为 class 组件
        let func = vnode.type
        let inst = new func(vnode.props) // 把 props 传入
        let innerVNODE = inst.render()
        return reactVDOM(innerVNODE) // 递归渲染后的组件
    }
}
import { createElement } from './src/react'
import { renderVDOM } from './src/renderVDOM'

const React = {}
React.createElement = createElement
React.Component = class Component {}

class App extends React.Component {
  render() {
    return (
      <div>
        <span>App</span>
        <span>component</span>
      </div>
    )
  }
}

const app = renderVDOM(<App />)
console.log(app) // 与 new App().render() 一样

父子组件

class ChildrenChild extends React.Component {
  render() {
    return (
      <div>
        children-child
      </div>
    )
  }
}

class Children extends React.Component {
  render() {
    return (
      <div>
        children
        <ChildrenChild />
      </div>
    )
  }
}

class App extends React.Component {
  render() {
    return (
      <div>
        <span>App</span>
        <span>component</span>
        <Children />
      </div>
    )
  }
}

const app = renderVDOM(<App />)

结果中组件的文本内容、dom、组件实例都在children 数组里,React.render 时只需要识别这些children 就可以做到真实渲染

实现 render

改写 renderVDMO 加入真实 dom 操作


function render(vnode, parent) {
  let dom
  if (typeof vnode === 'string') { // 文本节点直接渲染
    dom = document.createTextNode(vnode)
    parent.appendChild(dom)
  }
  
  if (typeof vnode.type === 'string') { // dom 节点
    dom = document.createElement(vnode.type)
    setAttrs(dom, vnode.props) // props 已经被createElement 解析成对象
    parent.appendChild(dom)

    for(let i = 0; i < vnode.children.length; i++) {
      render(vnode.children[i], dom) // 递归 render children
    }
  }

  if (typeof vnode.type === 'function') { // class 组件
    let func = vnode.type
    let inst = new func(vnode.props) // props 已经被createElement 解析成对象
    let innerVNode = inst.render()
    render(innerVNode, parent)
  }
}

function setAttrs(dom, props) {
  Object.keys(props).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') { // onClick of onClickCapture
      const capture = k.indexOf('Capture') !== -1
      dom.addEventListener(k.replace('Capture', '').substring(2).toLowerCase(), v, capture)
      return
    }

    dom.setAttribute(k, v)
  })
}

把上面例子换一下

// app.js
class ChildrenChild extends React.Component {
  render() {
    return (
      <div>
        children-child
      </div>
    )
  }
}

class Children extends React.Component {
  render() {
    return (
      <div>
        children
        <ChildrenChild />
      </div>
    )
  }
}

class App extends React.Component {
  render() {
    return (
      <div>
        <span>App</span>
        <span>component</span>
        <Children />
      </div>
    )
  }
}

render(<App />, document.getElementById('app'))

// index.html
<body>
  <div id="app"></div>
  <script src="./app.js"></script>
</body>
结果

实现props 和state

class Color extends React.Component {
  render() {
    return (
      <div style={{ color: this.props.color }}>color is: {this.props.color}</div>
    )
  }
}

const colorArr = ['red', 'blue', 'black', 'green', 'gray']
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      color: 'black'
    }
  }
  handleClick() {
    console.log("handleClick")
    this.setState({
      color: colorArr[Math.random() * 5 | 0]
    })
  }
  render() {
    return (
      <div onClick={this.handleClick.bind(this)}>
        <Color color={this.state.color} />
      </div>
    )
  }
}
render(<App />, document.getElementById('app'))

目的: 我们要通过点击 App 组件中的元素来改变 Color 文字的颜色
步骤: 把新的state 传入 this.setState 来更新组件 - 调用render 方法

setState

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

  setState(state) {
    setTimeout(() => {
      this.state = state
      // ...render()
    })
  }
}

回忆 render 函数有两个参数,vnodeparentvnode 我们可以使用 this.render() 获取当前组件,但我们要知道需要更新dom 内容的 parent 就需要在首次render 时记录。

改写 render

给render 增加参数,comp(当前更新组件), olddom(当前组件曾经的dom)
拿首次渲染举例: parent - document.getElementById('app'), comp - <App />, olddom - 当App组件更新时就是App 首次渲染的dom

export function render(vnode, parent, comp, olddom) {
  let dom
  if (typeof vnode === 'string') { // 文本节点直接渲染
    dom = document.createTextNode(vnode)
    comp && (comp.__rendered = dom)

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

  if (typeof vnode.type === 'string') { // dom 节点
    dom = document.createElement(vnode.type)

    comp && (comp.__rendered = dom)
    setAttrs(dom, vnode.props) // props 已经被createElement 解析成对象

    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) // 递归 render children
    }
  }

  if (typeof vnode.type === 'function') { // class 组件
    let func = vnode.type
    let inst = new func(vnode.props) // props 已经被createElement 解析成对象

    comp && (comp.__rendered = inst) // 这里记录的是 Component 实例

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

在这里我们每次render 的时候都会判断这次render 是否是class 组件触发的render,如果是组件触发的render 我们就会在这个组件comp 上增加 __rendered 记录当前渲染的 dom 或 当前渲染的组件 (组件追溯到顶层也是dom) ,这时候我们需要一个方法来获得olddom

function getDOM (comp) {
  let rendered = comp.__rendered
  while (rendered instanceof Component) {
    rendered = rendered.__rendered
  }
  return rendered
}
实现 setState
import { getDOM } from './util'
import { render } from './render'

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

  setState(state) {
    setTimeout(() => {
      this.state = state
      const vnode = this.render()
      let olddom = getDOM(this) // 获取渲染此实例的 olddom
      render(vnode, olddom.parentNode, this, olddom)
    })
  }
}
实现效果
import { render, createElement, Component } from './src/code1/react'

const React = {}
React.createElement = createElement
React.Component = Component


class Color extends React.Component {
  render() {
    return (
      <div style={{ color: this.props.color }}>color is: {this.props.color}</div>
    )
  }
}

const colorArr = ['red', 'blue', 'black', 'green', 'gray']
class App extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      color: 'black'
    }
  }
  handleClick() {
    console.log("handleClick")
    this.setState({
      color: colorArr[Math.random() * 5 | 0]
    })
  }
  render() {
    return (
      <div onClick={this.handleClick.bind(this)}>
        <Color color={this.state.color} />
      </div>
    )
  }
}

render(<App />, document.getElementById('app'))

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,529评论 6 524
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,683评论 3 406
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 172,905评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,318评论 1 303
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,325评论 6 401
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,754评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,081评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,088评论 0 280
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,653评论 1 327
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,651评论 3 347
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,766评论 1 355
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,359评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,083评论 3 341
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,491评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,654评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,367评论 3 383
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,836评论 2 367

推荐阅读更多精彩内容

  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,846评论 1 18
  • HTML模版 之后出现的React代码嵌套入模版中。 1. Hello world 这段代码将一个一级标题插入到指...
    ryanho84阅读 6,250评论 0 9
  • 原文地址:Learning React.js is easier than you think原文作者:Samer...
    sunshine小小倩阅读 4,232评论 3 41
  • 说在前面 关于 react 的总结过去半年就一直碎碎念着要搞起来,各(wo)种(tai)原(lan)因(le)。心...
    陈嘻嘻啊阅读 6,881评论 7 41
  • 一件一件,一堆一堆,一箱一箱,重复是最简单的坚持。是谁剥夺了你的时间,禁锢了你的自由,让你脑袋麻木,思维停止运转?...
    升级者阅读 179评论 0 0