1. 什么是React
React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库,由Facebook和一个大型开发者社区共同维护。使用 React 可以将一些简短、独立的代码片段组合成复杂的 UI 界面,这些代码片段被称作“组件”。
2. React的特点
声明式设计 - React采用声明范式,可以轻松描述应用。
高效 - React通过对DOM的模拟,最大限度地减少与DOM的交互。
灵活 - React可以与已知的库或框架很好地配合。
JSX - JSX是JavaScript语法的扩展。
单向响应式的数据流 - React实现了单向相应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。
组件 - 通过React构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
3. React的核心
真正去了解React还是要去深入学习它的核心东西,核心内容主要包括以下几点:
JSX:即Javascript XML,一个看起来很像 XML 的 JavaScript 语法扩展。用自己的话来说就是可以在js里面直接写DOM元素。
虚拟DOM:一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式(将真实的DOM结构映射成一个JSON数据结构 )被保存于内存中。并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调。
Diff算法:上面说到的虚拟DOM与“真实的” DOM同步的问题,也就是协调,协调需要采用Diff算法实现。普通diff的复杂度对于大量dom对比会出现严重的性能问题,React团队对diff算法进行了优化,主要有三个方面:tree diff、component diff、element diff。
组件化:Web Component通过自定义元素的方式实现组件化,React的组件元素被描述成纯粹的JSON对象,由三部分组成——属性(props),状态(state)以及生命周期方法。组件化的目的就是为了能够进行复用,减少代码的冗余。
4. JSX
const element = <h1>Hello, world!</h1>;
这个有趣的标签语法既不是字符串也不是 HTML。它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式(JSX 可以生成 React “元素”)。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。
-
在 JSX 中嵌入表达式
在 JSX 语法中,你可以在大括号内放置任何有效的 JavaScript 表达式。
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
ReactDOM.render(
element,
document.getElementById('root')
);
-
JSX 也是一个表达式
你可以在
if
语句和for
循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
- JSX 特定属性
可以通过使用引号,来将属性值指定为字符串字面量:
const element = <div tabIndex="0"></div>;
也可以使用大括号,来在属性值中插入一个 JavaScript 表达式:
const element = <img src={user.avatarUrl}></img>;
注意:在属性中嵌入 JavaScript 表达式时,不要在大括号外面加上引号。因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase
(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。
- JSX 防止注入攻击
const title = response.potentiallyMaliciousInput;
// 直接使用是安全的:
const element = <h1>{title}</h1>;
React DOM 在渲染所有输入内容之前,默认会进行转义。它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容。所有的内容在渲染之前都被转换成了字符串。这样可以有效地防止 XSS(cross-site-scripting, 跨站脚本)攻击。
-
JSX 表示对象
Babel 会把 JSX 转译成一个名为
React.createElement()
函数调用。实际上,JSX 仅仅只是React.createElement(component, props, ...children)
函数的语法糖,以下两种示例代码完全等效:
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
// 实际上React.createElement创建了一个这样的对象:
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
这些对象被称为 “React 元素”。它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。将 React 元素渲染为 DOM,通过ReactDOM.render()实现
// “根” DOM 节点,因为该节点内的所有内容都将由 React DOM 管理。
<div id="root"></div>
// 想要将一个 React 元素渲染到根 DOM 节点中,只需把它们一起传入 ReactDOM.render():
const element = <h1>Hello, world</h1>;
ReactDOM.render(element, document.getElementById('root'));
- 解析React.createElement()
每次新增一个组件页面,都需要引入React,而实际在代码中并未使用到,删除React又会报错,其实在渲染元素的时候调用了React.createElement(),只要我们使用了JSX,就要引入React。React.createElement()到底干了些什么事情?
<h1 id="title" className="title_class" style={{color: 'red'}}>
hello
<span>world</span>
</h1>
// 当我们将上面的代码经过babel转译后,调用React.createElement(component, props, ...children),如下:
React.createElement("h1", {
id: "title",
className: "title_class",
style: {
color: 'red'
}
}, "hello", React.createElement("span", null, "world"));
// React.createElement创建了一个这样的对象:
const element = {
type: 'h1',
props: {
id: 'title'
className: 'title_class',
style: {
color: 'red'
}
children: ['Hello, world!',React.createElement("span", null, "world")]
}
};
// React.createElement("span", null, "world") => {type: 'span',props: {},children: 'world'}
//方法接受三个参数,第一个参数是组件类型,第二个参数是要传递给组件的属性,第三个参数是children。方法最终会返回一个具有以下属性的对象
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {}; // 等同于config
let key = null;
let ref = null;
let self = null;
let source = null;
// config对象是dom的属性总和
if (config != null) {
if (hasValidRef(config)) { // 判断config自身是否有ref的属性
ref = config.ref;
}
if (hasValidKey(config)) { // 判断config自身是否有key的属性,参考for循环添加的key属性
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
for (propName in config) {
if (hasOwnProperty.call(config, propName) &&!RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
const childrenLength = arguments.length - 2; // 获取到子元素
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
5. 虚拟DOM
virtual dom是通过JSX编译生成的一个JavaScript对象,正如上面所讲的调用React.createElement(component, props, ...children)返回的结果,这个结果其实就是虚拟DOM。
// 虚拟dom
const vdom = {
type: 'h1',
props: {
id: 'title'
className: 'title_class',
style: {
color: 'red'
}
children: ['Hello, world!',React.createElement("span", null, "world")]
}
};
-
传统web应用和React web应用
传统web应用:
React web应用:
-
虚拟DOM的原理(下节课跟diff算法一起再详细介绍)
虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象
状态变更时,记录新树和旧树的差异
-
最后把差异更新到真正的dom中
-
虚拟DOM的优点
最终表现在DOM上的修改只是变更的部分,可以保证非常高效的渲染。
-
虚拟DOM的缺点
首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
引入虚拟DOM也是React提升性能的一种手段,React非常快速是因为它从不直接操作真实的DOM。虚拟DOM是在DOM的基础上建立了一个抽象层,对数据和状态所做的任何改动,都会被自动且高效的同步到虚拟DOM,最后再批量同步到DOM中。我们开发项目不用接触这个虚拟DOM,但是理解其运行机制不仅有助于更好的理解React组件的生命周期,而且对于进一步优化 React程序也有很大帮助。
6. 渲染DOM
我们拿到了虚拟DOM,最终要渲染到页面中去,生成真实的DOM,具体是怎么实现的呢?
- 创建元素createElement
export function createElement(
type: string, // 标签名
props: Object, // 属性
rootContainerElement: Element | Document, // 根节点
parentNamespace: string,
): Element {
let isCustomComponentTag;
// 获取ownerDocument
const ownerDocument: Document = getOwnerDocumentFromRootContainer(
rootContainerElement,
);
let domElement: Element;
let namespaceURI = parentNamespace;
if (namespaceURI === HTML_NAMESPACE) {
namespaceURI = getIntrinsicNamespace(type);
}
// 命名空间是否等于W3C规范 http://www.w3.org/1999/xhtml
if (namespaceURI === HTML_NAMESPACE) {
if (type === 'script') { // 创建script标签
const div = ownerDocument.createElement('div');
div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
const firstChild = ((div.firstChild: any): HTMLScriptElement);
domElement = div.removeChild(firstChild);
} else if (typeof props.is === 'string') {
domElement = ownerDocument.createElement(type, {is: props.is});
} else {
domElement = ownerDocument.createElement(type);
// 对select标签做处理
if (type === 'select') {
const node = ((domElement: any): HTMLSelectElement);
if (props.multiple) {
node.multiple = true;
} else if (props.size) {
node.size = props.size;
}
}
}
} else {
domElement = ownerDocument.createElementNS(namespaceURI, type);
}
return domElement;
}
- 配置属性、样式以及渲染内容
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {
for (const propKey in nextProps) {
if (!nextProps.hasOwnProperty(propKey)) {
continue;
}
const nextProp = nextProps[propKey];
if (propKey === STYLE) {
// 配置style里面的样式
setValueForStyles(domElement, nextProp); // 下文有贴出源码setValueForStyles
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
// 富文本渲染 dangerouslySetInnerHTML属性
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {
setInnerHTML(domElement, nextHtml);
}
} else if (propKey === CHILDREN) {
if (typeof nextProp === 'string') {
// 单纯文本渲染
const canSetTextContent = tag !== 'textarea' || nextProp !== '';
if (canSetTextContent) {
setTextContent(domElement, nextProp); // 下文有贴出源码setTextContent
}
} else if (typeof nextProp === 'number') {
// 遇到数字转成字符串再渲染
setTextContent(domElement, '' + nextProp);
}
}else if (registrationNameDependencies.hasOwnProperty(propKey)) {
if (nextProp != null) {
if (propKey === 'onScroll') {
listenToNonDelegatedEvent('scroll', domElement);
}
}
} else if (nextProp != null) {
// setAttribute 设置属性以及对应的内容
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
export function setValueForStyles(node, styles) {
const style = node.style;
for (let styleName in styles) {
if (!styles.hasOwnProperty(styleName)) {
continue;
}
const isCustomProperty = styleName.indexOf('--') === 0;
const styleValue = dangerousStyleValue(
styleName,
styles[styleName],
isCustomProperty,
);
if (styleName === 'float') {
styleName = 'cssFloat';
}
if (isCustomProperty) {
style.setProperty(styleName, styleValue);
} else {
style[styleName] = styleValue;
}
}
}
const setTextContent = function(node: Element, text: string): void {
if (text) {
const firstChild = node.firstChild;
if (firstChild && firstChild === node.lastChild && firstChild.nodeType === TEXT_NODE) {
firstChild.nodeValue = text;
return;
}
}
node.textContent = text;
};