英文原版链接
React
我们忽视掉其他不重要的细节。
这篇post我要实现上述所有功能,实现一个最简单的React 。
这些实例应用,日历选择, 贪吃蛇游戏, 就是用这个33代码的库做的。
我们来实现这个游戏,下边是实现这个游戏的代码
<style>
.o{background:red;}
.x{background:blue;}
.cell{height:4em;width:4em;border:1px solid black;}
</style>
<div id="noughts"></div>
<script>
let currentPlayer = 'o'
let winner = null
const g = [['', '', ''], ['', '', ''], ['', '', '']] // grid
const move = (value, i, j)=>{
if (value !== '') return
g[i][j] = currentPlayer
currentPlayer = currentPlayer === 'x'? 'o' : 'x'
const winners = [
...[0, 1, 2].map(i=>[g[i][0], g[i][1], g[i][2]].join('')),
...[0, 1, 2].map(j=>[g[0][j], g[1][j], g[2][j]].join('')),
[g[0][0], g[1][1], g[2][2]].join(''),
[g[2][0], g[1][1], g[0][2]].join(''),
]
if(winners.includes('xxx')) winner = 'x'
if(winners.includes('ooo')) winner = 'o'
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()
<script>
不错,我们接下来呢?
首先,我们定义一些状态
let currentPlayer = 'o'
let winner = null
const g = [['', '', ''], ['', '', ''], ['', '', '']] // grid
它保存了游戏的状态,让我们来改变它
const move = (value, i, j){...}
这个方法在游戏移动一下,他接收x 或者 o ,同时2个整数坐标为参数,它将改变状态从而影响整个游戏。然后,它调用 renderNoughts 方法,这是重新渲染游戏的方法,我们稍后会讲到
然后我们定义生成虚拟dom的方法,
m 方法需要以下参数
{
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)
}
}