参考:
原文:
2017年基于 老 react:medium.com/hexacta-eng...
-
对应的github仓库github.com/pomber/dida...
-
对应的视频,react官网推荐:www.youtube.com/watch?v=_MA...
2019年基于React16.8 hooks机制pomb.us/build-your-...
-
对应的github仓库github.com/pomber/dida...
-
Talk version,对应视频:www.youtube.com/watch?v=8Kc...
中文版参考(基于 老 react,实现了fiber,无hooks):
一、vdom的定义和数据结构
怎么用 js 生成这样的页面? /Users/wuyanbiao/Documents/demos/frontend-framework-exercize/legacy-vdom/index.html
vdom 全称 virtual dom,用来声明式的描述页面,现代前端框架很多都基于 vdom。前端框架负责把 vdom 转为对真实 dom 的增删改,也就是 vdom 的渲染。
tips: React-native也使用vdom,不过是转为原生UI的渲染,而非dom。
那么 vdom 是什么样的数据结构?又是怎么渲染的呢?
dom 主要是元素(文本)、属性,
vdom 也是一样,type表示元素类型,props表示属性。props.children比较特别,表示子元素数组,设置属性的时候需要额外处理。

javascript
/**判断是否为文本 */
function isTextVdom(vdom) {
return typeof vdom == 'string' || typeof vdom == 'number';
}
/**判断是否为元素 */
function isElementVdom(vdom) {
return typeof vdom == 'object' && typeof vdom.type == 'string';
}
// vue 则使用以下数据结构
const vdom = {
tag:
props:
children:
}
const vdom = {
type: 'ul',
props: {
className: 'list',
children: [
{
type: 'li',
props: {
className: 'item',
style: {
background: 'blue',
color: '#fff'
},
onClick: function() {
alert(1);
},
children: [
'aaaa'
]
},
},
{
type: 'li',
props: {
className: 'item',
children: [
'bbbbddd'
]
},
},
{
type: 'li',
props: {
className: 'item',
children: [
2
]
},
}
]
},
};
React官方vdom数据结构:github.com/facebook/re...
其他数据结构定义思路:
(1)将children 打平,在type\props\children一层
css
// 其他人可能这么实现https://github.com/QuarkGluonPlasma/frontend-framework-exercize
const vdom = {
type: 'ul',
props: {
className: 'list',
},
children : [] // 注意children不在props里面了!!!
}
(2)文本使用对象表示
比如carter将被表示为:
css
{
type: "span",
props: {
children: [
{
type: "TEXT_ELEMENT",
props: {
nodeValue: 'carter',
children: [],
},
}
],
},
}

二、vdom如何渲染到浏览器
javascript
// function mountWithParent(el, parent = null) {
// return parent ? parent.appendChild(el) : el; // 返回值都是el本身(appendChild也是返回el本身)
// }
const render = (vdom, parent = null) => {
const mount = parent ? (el => parent.appendChild(el)) : (el => el); // 可以换成上面👆🏻注释的函数mountWithParent,节省内存,不用每次都创建函数mount
if (isTextVdom(vdom)) {
return mount(document.createTextNode(vdom));
} else if (isElementVdom(vdom)) {
/***** 生成节点;挂载子节点和递归子节点;设置属性 */
for (const child of vdom.children) {
render(child, dom); // 递归挂载所有的元素和文本
}
for (const prop in vdom.props) {
setAttribute(dom, prop, vdom.props[prop]);
}
const dom = mount(document.createElement(vdom.type));
return dom;
} else {
throw new Error(`Invalid VDOM: ${vdom}.`);
}
};
// 入口
render(vdom, document.getElementById('root'));
实现细节(一) 如何设置属性?
stackoverflow.com/questions/6...
(1)使用properties
有效的属性才会校验通过。
无效属性无法正确被设置。
ini
function render(element, parentDom) {
const { type, props } = element;
const dom = document.createElement(type);
const isListener = name => name.startsWith("on");
Object.keys(props).filter(isListener).forEach(name => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, props[name]);
});
const isAttribute = name => !isListener(name) && name != "children";
Object.keys(props).filter(isAttribute).forEach(name => {
dom[name] = props[name];
});
const childElements = props.children || [];
childElements.forEach(childElement => render(childElement, dom));
parentDom.appendChild(dom);
}
(2) 使用attributes
vbnet
const setAttribute = (dom, key, value) => {
if (isEventListenerAttr(key, value)) {
const eventType = key.slice(2).toLowerCase(); // 这里不能直接使用el[key.toLowerCase()],类似onClick,因为onClick只能存一个事件,后来的事件会覆盖之前的https://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick
dom.addEventListener(eventType, value);
} else if (isStyleAttr(key, value)) {
Object.assign(dom.style, value);
} else if (isPlainAttr(key, value)) {
dom. setAttribute (key, value);
}
}
实现细节(二)如何提升性能,减少dom 绘制带来的layout 消耗?
将创建好的dom树,最后append到root节点上。
内存中的dom node节点,比如const dom = document.createElement('span'); 不会触发渲染layout。
三、jsx 编译成 vdom
上面的 vdom 改为 jsx 来写就是这样的:
ini
const jsx =
<ul className="list">
<li className="item" style={{ background: 'blue', color: 'pink' }} onClick={() => alert(2)}>aaa</li>
<li className="item">bbb</li>
</ul>
明显比直接写 vdom 紧凑了不少,但是需要做一次babel编译。
编译产物:
php
const jsx = createElement("ul", {
className: "list"
}, createElement("li", {
className: "item",
style: {
background: 'blue',
color: 'pink'
},
onClick: () => alert(2)
}, "aaa"), createElement("li", {
className: "item"
}, "bbb"));
render(jsx, document.getElementById('root'));
为啥不直接是 vdom对象,而是函数的执行返回对象呢?
因为函数是可以动态变化的,这样会有一次执行的过程,可以放入一些动态逻辑。createElement函数里可以自定义增强一些功能表现。
ini
/*** 在这里自定义createElement,而不使用React.createElement */
const createElement = (type, props, ...children) => {
// 可以额外执行一些逻辑,增加新的vdom属性,兼容地递增 增加新的功能。
if (props === null) props = {};
return {
type,
props: {
...props,
children,
}
};
};
四、声明式 vs 命令式
先说结论:
-
声明式代码,开发体验更好,更容易维护。
-
命令式代码比声明式代码运行得更快。
(1)JQuery和原生dom操作是命令式的。


(2)React和Vue都是声明式框架。

由于声明式代码中,只需要找出需要更新的vdom,进行dom的更新。找出差异的时间消耗B,如果比A数量级小,那么基本就可以认为A =~ A + B

ops/s表示每秒的操作数Operation per second。
