33 行代码实现React

英文原版链接

React

  • 一函数传入状态参数,返回值是虚拟dom(只是js对象树而已)
  • 它在浏览器把虚拟dom渲染为真实dom
  • 你改了状态,函数再执行,返回新的虚拟dom
  • 它高效的更新了真实DOM,以便与新的虚拟DOM匹配

我们忽视掉其他不重要的细节。
这篇post我要实现上述所有功能,实现一个最简单的React 。
这些实例应用,日历选择, 贪吃蛇游戏, 就是用这个33代码的库做的。

画圈打叉游戏

我们来实现这个游戏
我们看一下下边的代码,同样你可以看一下源码

let currentPlayer = 'o'
let winner = null
const g = [['', '', ''], ['', '', ''], ['', '', '']]  // grid

const move = (value, i, j)=>{
    // ... game logic goes here
    renderNoughts()
}

const Cell = (value, i, j)=>m('button.cell',
    {onclick: ()=>move(value, i, j)}, value
)
const Noughts = ()=>m('',
    winner
        ? m('marquee', `winner: ${winner}`)
        : m('h3', `current player: ${currentPlayer}`),
    m('table', g.map(
        (row, i)=>m('tr', row.map(
            (value, j)=>m('td', {class: value}, Cell(value, i, j)))))),
)

const renderNoughts = ()=>m.render(
    document.getElementById('noughts'),
    {children: [Noughts()]},
)
renderNoughts()

不错,我们接下来呢?

首先,我们定义一些状态

let currentPlayer = 'o'
let winner = null
const g = [['', '', ''], ['', '', ''], ['', '', '']]  // grid

它保存了游戏的状态,让我们来改变它

const move = (value, i, j){...}

这个方法在游戏移动一下,他接收x 或者 o ,同时2个整数坐标为参数,它将改变状态从而影响整个游戏。然后,它调用 renderNoughts 方法,这是重新渲染游戏的方法,我们稍后会讲到

然后我们定义生成虚拟dom的方法,
m 方法需要以下参数

  • 一个标签名和类名,如li.b
  • 一个字符串属性对象,这个标签的所有特性
  • 一个任意嵌套的子列表(虚拟dom或者文本)
    然后返回虚拟dom元素,比如调用Noughts方法返回如下
{
    tag: 'div',
    attrs: {},
    classes: [],
    children: [
        {
            tag: 'h3',
            attrs: {},
            classes: [],
            children: [
                'current player: x'
            ]
        },
        {
            tag: 'table',
            attrs: {},
            classes: [],
            children: [
                {
                    tag: 'tr',
                    attrs: {},
                    classes: [],
                    children: [
...

然后我们写一个renderNoughts方法,当调用时,再调用Noughts方法,然后会高效的把真实dom渲染到 document.getElementById(‘noughts’)

下边是源码

// Calling with:
//
//     m(
//         'li.some.classes',
//         {onclick: f},
//         'text',
//         otherNode,
//         null,
//         [['nested'], 'stuff'],
//     )
//
// Would return a virtual DOM node like:
//
//     {
//         __m: true,
//         tag: 'li',
//         attrs: {onclick: f},
//         classes: ['some', 'classes'],
//         children: [
//             'text',
//             otherNode,
//             'nested',
//             'stuff',
//         ],
//     }
//
const m = (...args)=>{
    // munge our args, to get: tag, attrs, classes
    let [attrs, [head, ...tail]] = [{}, args]
    let [tag, ...classes] = head.split('.')
    if (tail.length && !m.isRenderable(tail[0])) [attrs, ...tail] = tail
    if (attrs.class) classes = [...classes, ...attrs.class]
    // shallow copy attrs and delete the "class" value, we've already used that above
    attrs = {...attrs}; delete attrs.class
    // make array of children, recursively flatten the tail into this array
    // and remove nulls while we're at it
    const children = []
    const addChildren = v=>v === null? null : Array.isArray(v)? v.map(addChildren) : children.push(v)
    addChildren(tail)
    return {__m: true, tag: tag || 'div', attrs, classes, children}
}

// We can render:
// - nulls
// - strings
// - numbers
// - virtual DOM nodes
// - Arrays of the above
m.isRenderable = v =>v === null || ['string', 'number'].includes(typeof v) || v.__m || Array.isArray(v)

// Call with a (real) DOM element and a virtual one.
// Sets the attributes and classes on the real element to those of the
// virtual one - but only as it needs to.
m.update = (el, v)=>{
    // if it's a text element, set the data if we have to
    if (!v.__m) return el.data === `${v}` || (el.data = v)
    // set the class names
    for (const name of v.classes) if (!el.classList.contains(name)) el.classList.add(name)
    for (const name of el.classList) if (!v.classes.includes(name)) el.classList.remove(name)
    // set the attributes
    for (const name of Object.keys(v.attrs)) if (el[name] !== v.attrs[name]) el[name] = v.attrs[name]
    for (const {name} of el.attributes) if (!Object.keys(v.attrs).includes(name) && name !== 'class') el.removeAttribute(name)
}

// Given a virtual DOM element, make a real element,
// else make a real textNode of the value.
m.makeEl = v=>v.__m? document.createElement(v.tag) : document.createTextNode(v)

// Given a real DOM element and a virtual one:
//
// a) Get the children of each: olds and news
// b) Remove the excess olds
// c) For each of the new virtual elements:
//   1) Get the matching old element (by index),
//      if there isn't one, make a new element
//   2) If there wasn't a matching old element,
//      append the new one to the parent
//   3) If there is a mismatch (either the tag
//      name doesn't match, or there's an
//      element != textNode), make a new element
//      and replace the matching one on the parent
//   4) Update the attributes/classes on the element
//   5) Recurse through the child's children etc.
//
// Note that this makes appending to lists of elements
// very efficient, but prepending will be O[n]
m.render = (parent, v)=>{
    const olds = parent.childNodes || []  // a)
    const news = v.children || []  // a)
    for (const _ of Array(Math.max(0, olds.length - news.length))) parent.removeChild(parent.lastChild)  // b)
    for (const [i, child] of news.entries()){  // c)
        let el = olds[i] || m.makeEl(child)  // 1)
        if (!olds[i]) parent.appendChild(el)  // 2)
        const mismatch = (el.tagName || '') !== (child.tag || '').toUpperCase()  // 3)
        if (mismatch) (el = m.makeEl(child)) && parent.replaceChild(el, olds[i])  // 3)
        m.update(el, child)  // 4)
        m.render(el, child)  // 5)
    }
}
发布了43 篇原创文章 · 获赞 14 · 访问量 1003

猜你喜欢

转载自blog.csdn.net/qq_29334605/article/details/105520251
33