假如你的项目使用了React,你知道怎么做性能优化吗?
你知道为什么React让你写shouldComponentUpdate或者React.PureComponent吗?
你知道为什么React让你写Immutable Data Structures吗?
你知道为什么React让你在渲染列表时,一定要给每个子项加一个key吗?
你知道为什么React让你在条件渲染时,不写if而写&&操作符或三元操作符吗?
一切的答案都在Virtual DOM上!
只要你跟着我完成了这个手写Virtual DOM的系列,上面的所有问题你都将得到解答,从此进入react高手的阵营!
上集回顾
上一集我们介绍了什么是VDOM,为什么要用VDOM,以及我们要怎样来实现一个VDOM。我们再来看一下这张蓝图,今天我们要实现的是这张图的左半部分。
package.json
{ "name": "vdom", "version": "1.0.0", "description": "", "scripts": { "compile": "babel index.js --out-file compiled.js" }, "author": "", "license": "", "devDependencies": { "babel-cli": "^6.23.0", "babel-plugin-transform-react-jsx": "^6.23.0" } }
这里主要主要两点:
devDependencies
中依赖babel-cli和babel-plugin-transform-react-jsx这两个库,前者提供Babel的命令行功能,后者主要帮我们把jsx转化成js。scripts
中我们指定了一条命令:complile
,每次当我们在当前目录下的命令行中敲npm run compile
时,babal就会将我们的index.js
转化后新建一个compile.js
文件。
完成后,在命令行中输入npm install
安装下依赖。
.babelrc
{ "plugins": [ ["transform-react-jsx", { "pragma": "h" // default pragma is React.createElement }] ] }
在babel的配置文件中,我们指定transform-react-jsx
这个插件将转化后的函数名设置为h
。默认的函数名是React.createElement
,我们不依赖react,所以显然换个自己的名字更合适。这里不清楚h
是干什么的不要紧,等会看到代码你就知道了。
index.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>VDOM</title> <style> body { margin: 0; font-size: 24; font-family: sans-serif } .list { text-decoration: none } .list .main { color: red } </style> </head> <body> <script src="compiled.js"></script> <div id="app"></div> <script> var app = document.getElementById('app') render(app) </script> </body> </html>
这个HTML还是很直观的,类似React,我们有一个根节点id是app。然后我们render函数最终生成的DOM会插入到app这个根节点里。注意我们引用的compile.js文件是babel根据等会要写的index.js文件自动生成的。
index.js
首先,我们用JSX来编写“模板”:
function view() { return <ul id="filmList" className="list"> <li className="main">Detective Chinatown Vol 2</li> <li>Ferdinand</li> <li>Paddington 2</li> </ul> }
接下来,我们要将JSX编译成js, 也就是hyperscript。我们先用Babel编译一下,看这段JSX转成js会是什么样子,打开命令行,输入npm run compile
,得到的compile.js:
function view() { return h( "ul", { id: "filmList", className: "list" }, h( "li", { className: "main" }, "Detective Chinatown Vol 2" ), h( "li", null, "Ferdinand" ), h( "li", null, "Paddington 2" ) ); }
可以看出h
函数接收的参数,第一个参数是node的类型,比如ul
,li
,第二个参数是node的属性,之后的参数是node的children,假如child又是一个node的话,就会继续调用h
函数。
清楚了Babel会将我们的JSX编译成什么样子后,接下来我们就可以继续在index.js中来写h
函数了。
function flatten(arr) { return [].concat(...arr) } function h(type, props, ...children) { return { type, props: props || {}, children: flatten(children) } }
我们的h
函数主要的工作就是返回我们真正需要的hyperscript对象,只有三个参数,第一个参数是节点类型,第二个参数是属性对象,第三个是子节点的数组。
这里主要用了ES6的rest, spread参数,不清楚代码中两个...
分别是什么意思的可以先去看我的介绍ES6文章30分钟掌握ES6/ES2015核心内容(上)。简单来说,rest就是上面的...children
,它将函数多余的参数放到一个数组里,所以children此时变成了一个数组。而spread则是rest的逆运算,也就是上面的...arr
,它将一个数组转为用逗号分隔的参数序列。
flatten(children)
这个操作是因为children这个数组里的元素有可能也是个数组,那样就成了一个二维数组,所以我们需要将数组拍平成一维数组。[].concat(...arr)
是ES6写法,传统的写法是[].concat.apply([], arr)
我们现在可以先来看一下h
函数最终返回的对象长什么样子。
function render() { console.log(view()) }
我们在render函数中打印出执行完view()的结果,再npm run compile后,用浏览器打开我们的index.html,看控制台输出的结果。
可以,很完美!这个对象就是我们的VDOM了!
下面我们就可以根据VDOM, 来渲染真实DOM了。先改写render函数:
function render(el) { el.appendChild(createElement(view(0))) }
createElement函数生成DOM,然后再插入到我们在index.html中写的根节点app。注意render函数式在index.html中被调用的。
function createElement(node) { if (typeof(node) === 'string') { return document.createTextNode(node) } let { type, props, children } = node const el = document.createElement(type) setProps(el, props) children.map(createElement) .forEach(el.appendChild.bind(el)) return el } function setProp(target, name, value) { if (name === 'className') { return target.setAttribute('class', value) } target.setAttribute(name, value) } function setProps(target, props) { Object.keys(props).forEach(key => { setProp(target, key, props[key]) }) }
我们来仔细看下createElement函数。假如说node,即VDOM的类型是文本,我们直接返回一个创建好的文本节点。否则的话,我们取出node中类型,属性和子节点, 先根据类型创建相应的目标节点,然后再调用setProps
函数依次设置好目标节点的属性,最后遍历子节点,递归调用createElement方法,将返回的子节点插入到刚刚创建的目标节点里。最后返回这个目标节点。
还需要注意的一点是,jsx中class的写成了className,所以我需要特殊处理一下。
大功告成,complie后浏览器打开index.html看看结果吧。
今天我们成功的完成了蓝图的左半部分,将JSX转化成hyperscript,再转化成VDOM,最后根据VDOM生成DOM,渲染到页面。明天,我们迎接挑战,开始处理数据变动引起的重新渲染,我们要如何DIFF新旧VDOM,生成补丁,修改DOM。