赞
踩
JSX是语法糖,通过babel转成
React.createElement
函数,在babel官网上可以在线把JSX转成React的JS语法
createElement
函数vnode
script标签中不添加
text/babel
解析jsx语法的情况下
<script>
const ele = React.createElement("h2", null, "Hello React!");
ReactDOM.render(ele, document.getElementById("app"));
</script>
JSX的本质是React.createElement()函数
createElement
函数返回的对象是ReactEelement
对象。
createElement
的写法如下
class App extends React.Component { constructor() { super() this.state = { } } render() { return React.createElement("div", null, /*第一个子元素,header*/ React.createElement("div", { className: "header" }, React.createElement("h1", { title: "\u6807\u9898" }, "\u6211\u662F\u6807\u9898") ), /*第二个子元素,content*/ React.createElement("div", { className: "content" }, React.createElement("h2", null, "\u6211\u662F\u9875\u9762\u7684\u5185\u5BB9"), React.createElement("button", null, "\u6309\u94AE"), React.createElement("button", null, "+1"), React.createElement("a", { href: "http://www.baidu.com" }, "\u767E\u5EA6\u4E00\u4E0B") ), /*第三个子元素,footer*/ React.createElement("div", { className: "footer" }, React.createElement("p", null, "\u6211\u662F\u5C3E\u90E8\u7684\u5185\u5BB9") ) ); } } ReactDOM.render(<App />, document.getElementById("app"));
实际开发中不会使用createElement
来创建ReactElement
的,一般都是使用JSX的形式开发。
ReactElement
在程序中打印一下
render() { let ele = ( <div> <div className="header"> <h1 title="标题">我是标题</h1> </div> <div className="content"> <h2>我是页面的内容</h2> <button>按钮</button> <button>+1</button> <a href="http://www.baidu.com">百度一下</a> </div> <div className="footer"> <p>我是尾部的内容</p> </div> </div> ) console.log(ele); return ele; }
react通过babel把JSX转成
createElement
函数,生成ReactElement
对象,然后通过ReactDOM.render函
数把ReactElement
渲染成真实的DOM
元素
为什么 React 使用 JSX
JavaScript
的语法扩展,结构类似 XML。React
元素,但 React 中并不强制使用 JSX
。即使使用了 JSX
,也会在构建过程中,通过 Babel 插件编译为 React.createElement
。所以 JSX 更像是 React.createElement
的一种语法糖Babel 插件如何实现 JSX 到 JS 的编译? 在 React 面试中,这个问题很容易被追问,也经常被要求手写。
它的实现原理是这样的。Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就可以将 JSX 的结构转换为 React.createElement
的函数。如下代码所示:
module.exports = function (babel) { var t = babel.types; return { name: "custom-jsx-plugin", visitor: { JSXElement(path) { var openingElement = path.node.openingElement; var tagName = openingElement.name.name; var args = []; args.push(t.stringLiteral(tagName)); var attribs = t.nullLiteral(); args.push(attribs); var reactIdentifier = t.identifier("React"); //object var createElementIdentifier = t.identifier("createElement"); var callee = t.memberExpression(reactIdentifier, createElementIdentifier) var callExpression = t.callExpression(callee, args); callExpression.arguments = callExpression.arguments.concat(path.node.children); path.replaceWith(callExpression, path.node); }, }, }; };
React.createElement源码分析
/** 101. React的创建元素方法 */ export function createElement(type, config, children) { // propName 变量用于储存后面需要用到的元素属性 let propName; // props 变量用于储存元素属性的键值对集合 const props = { }; // key、ref、self、source 均为 React 元素的属性,此处不必深究 let key = null; let ref = null; let self = null; let source = null; // config 对象中存储的是元素的属性 if (config != null) { // 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值 if (hasValidRef(config)) { ref = config.ref; } // 此处将 key 值字符串化 if (hasValidKey(config)) { key = '' + config.key; } self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面 for (propName in config) { if ( // 筛选出可以提进 props 对象里的属性 hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; } } } // childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度 const childrenLength = arguments.length - 2; // 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了 if (childrenLength === 1) { // 直接把这个参数的值赋给props.children 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 props.children = childArray; } // 处理 defaultProps if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } // 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数 return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, ); }
入参解读:创造一个元素需要知道哪些信息
export function createElement(type, config, children)
createElement 有 3 个入参,这 3 个入参囊括了 React 创建一个元素所需要知道的全部信息。
type
:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment
类型。config
:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。children
:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”React.createElement("ul", {
// 传入属性键值对
className: "list"
// 从第三个入参开始往后,传入的参数都是 children
}, React.createElement("li", {
key: "1"
}, "1"), React.createElement("li", {
key: "2"
}, "2"));
这个调用对应的 DOM 结构如下:
<ul className="list">
<li key="1">1</li>
<li key="2">2</li>
</ul>
createElement 函数体拆解
createElement 中并没有十分复杂的涉及算法或真实 DOM 的逻辑,它的每一个步骤几乎都是在格式化数据。
现在看来,
createElement
原来只是个“参数中介”。此时我们的注意力自然而然地就聚焦在了ReactElement
上
出参解读:初识虚拟 DOM
createElement
执行到最后会 return 一个针对 ReactElement 的调用。这里关于 ReactElement,我依然先给出源码 + 注释形式的解析
const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement $$typeof: REACT_ELEMENT_TYPE, // 内置属性赋值 type: type, key: key, ref: ref, props: props, // 记录创造该元素的组件 _owner: owner, }; // if (__DEV__) { // 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听 } return element; };
ReactElement
其实只做了一件事情,那就是“创建”,说得更精确一点,是“组装”:ReactElement
把传入的参数按照一定的规范,“组装”进了element
对象里,并把它返回给了eact.createElement
,最终React.createElement
又把它交回到了开发者手中
const AppJSX = (<div className="App">
<h1 className="title">I am the title</h1>
<p className="content">I am the content</p>
</div>)
console.log(AppJSX)
你会发现它确实是一个标准的 ReactElement
对象实例
这个 ReactElement
对象实例,本质上是以 JavaScript 对象形式存在的对 DOM
的描述,也就是老生常谈的“虚拟 DOM”(准确地说,是虚拟 DOM
中的一个节点)
----------@----------
目的是为了防止 XSS 攻击。因为 Synbol 无法被序列化,所以 React 可以通过有没有 $$typeof 属性来断出当前的 element 对象是从数据库来的还是自己生成的。
// 服务端允许用户存储 JSON let expectedTextButGotJSON = { type: 'div', props: { dangerouslySetInnerHTML: { __html: '/* 把你想的搁着 */' }, }, // ... }; let message = { text: expectedTextButGotJSON }; // React 0.13 中有风险 <p> { message.text} </p>
----------@----------
通过 JS 对象模拟 DOM 的节点
。在 Facebook 构建 React 初期时,考虑到要提升代码抽象能力、避免人为的 DOM 操作、降低代码整体风险等因素,所以引入了虚拟 DOMPlain Object
,以 React 为例,在 render
函数中写的 JSX
会在 Babel
插件的作用下,编译为 React.createElement
执行 JSX
中的属性参数React.createElement
执行后会返回一个 Plain Object
,它会描述自己的 tag
类型、props
属性以及 children
情况等。这些 Plain Object
通过树形结构组成一棵虚拟 DOM
树。当状态发生变更时,将变更前后的虚拟 DOM
树进行差异比较,这个过程称为 diff
,生成的结果称为 patch
。计算之后,会渲染 Patch
完成对真实 DOM
的操作。改善大规模
DOM操作的性能
、规避 XSS 风险
、能以较低的成本实现跨平台开发
。DOM
除了渲染页面,虚拟 DOM 还有哪些应用场景?
这个问题考验面试者的想象力。通常而言,我们只是将虚拟 DOM 与渲染绑定在一起,但实际上虚拟 DOM 的应用更为广阔。比如,只要你记录了真实 DOM 变更,它甚至可以应用于埋点统计与数据记录等。
SSR原理
借助虚拟dom,服务器中没有dom概念的,react巧妙的借助虚拟dom,然后可以在服务器中nodejs可以运行起来react代码。
----------@----------
类组件中的优化手段
PureComponent
作为基类。shouldComponentUpdate
生命周期函数来自定义渲染逻辑。方法组件中的优化手段
React.memo
高阶函数包装组件,React.memo
可以实现类似于 shouldComponentUpdate
或者 PureComponent
的效果useMemo
React.useMemo
精细化的管控,useMemo 控制的则是是否需要重复执行某一段逻辑
,而React.memo 控制是否需要重渲染一个组件
useCallBack
。其他方式
Suspense
和 lazy 进行懒加载,例如:import React, { lazy, Suspense } from "react"; export default class CallingLazyComponents extends React.Component { render() { var ComponentToLazyLoad = null; if (this.props.name == "Mayank") { ComponentToLazyLoad = lazy(() => import("./mayankComponent")); } else if (this.props.name == "Anshul") { ComponentToLazyLoad = lazy(() => import("./anshulComponent")); } return ( <div> <h1>This is the Base User: { this.state.name}</h1> <Suspense fallback={ <div>Loading...</div>}> <ComponentToLazyLoad /> </Suspense> </div> ) } }
----------@----------
在 Redux 的整个工作过程中,数据流是严格单向的
。这一点一定一定要背下来,面试的时候也一定一定要记得说
为什么要用redux
在
React
中,数据在组件中是单向流动的,数据从一个方向父组件流向子组件(通过props
),所以,两个非父子组件之间通信就相对麻烦,redux
的出现就是为了解决state
里面的数据问题
Redux设计理念
Redux
是将整个应用状态存储到一个地方上称为store
,里面保存着一个状态树store tree
,组件可以派发(dispatch
)行为(action
)给store
,而不是直接通知其他组件,组件内部通过订阅store
中的状态state
来刷新自己的视图
如果你想对数据进行修改,
只有一种途径:派发 action
。action 会被 reducer 读取,进而根据 action 内容的不同对数据进行修改、生成新的 state(状态),这个新的 state 会更新到 store 对象里,进而驱动视图层面做出对应的改变。
Redux三大原则
整个应用的state都被存储到一个状态树里面,并且这个状态树,只存在于唯一的store中
state
是只读的,唯一改变state
的方法就是触发action
,action
是一个用于描述以发生时间的普通对象
使用纯函数来执行修改,为了描述
action
如何改变state
的,你需要编写reducers
从编码的角度理解 Redux 工作流
createStore 来完成 store 对象的创建
// 引入 redux
import {
createStore } from 'redux'
// 创建 store
const store = createStore(
reducer,
initial_state,
applyMiddleware(middleware1, middleware2, ...)
);
createStore 方法是一切的开始,它接收三个入参:
reducer;
初始状态内容;
指定中间件
reducer 的作用是将新的 state 返回给 store
一个 reducer 一定是一个纯函数,它可以有各种各样的内在逻辑,但它最终一定要返回一个 state:
const reducer = (state, action) => {
// 此处是各种样的 state处理逻辑
return new_state
}
当我们基于某个 reducer 去创建 store 的时候,其实就是给这个 store 指定了一套更新规则:
// 更新规则全都写在 reducer 里
const store = createStore(reducer)
要想让 state 发生改变,就必须用正确的 action 来驱动这个改变。
const action = {
type: "ADD_ITEM",
payload: '<li>text</li>'
}
action 对象中允许传入的属性有多个,但只有 type 是必传的。type 是 action 的唯一标识,reducer 正是通过不同的 type 来识别出需要更新的不同的 state,由此才能够实现精准的“定向更新”。
action 本身只是一个对象,要想让 reducer 感知到 action,还需要“派发 action”这个动作,这个动作是由 store.dispatch 完成的
。这里我简单地示范一下:
import { createStore } from 'redux' // 创建 reducer const reducer = (state, action) => { // 此处是各种样的 state处理逻辑 return new_state } // 基于 reducer 创建 state const store = createStore(reducer) // 创建一个 action,这个 action 用 “ADD_ITEM” 来标识 const action = { type: "ADD_ITEM", payload: '<li>text</li>' } // 使用 dispatch 派发 action,action 会进入到 reducer 里触发对应的更新 store.dispatch(action)
以上这段代码,是从编码角度对 Redux 主要工作流的概括,这里我同样为你总结了一张对应的流程图:
Redux源码
let createStore = (reducer) => { let state; //获取状态对象 //存放所有的监听函数 let listeners = []; let getState = () => state; //提供一个方法供外部调用派发action let dispath = (action) => { //调用管理员reducer得到新的state state = reducer(state, action); //执行所有的监听函数 listeners.forEach((l) => l()) } //订阅状态变化事件,当状态改变发生之后执行监听函数 let subscribe = (listener) => { listeners.push(listener); } dispath(); return { getState, dispath, subscribe } } let combineReducers=(renducers)=>{ //传入一个renducers管理组,返回的是一个renducer return function(state={ },action={ }){ let newState={ }; for(var attr in renducers){ newState[attr]=renducers[attr](state[attr],action) } return newState; } } export { createStore,combineReducers};
聊聊 Redux 和 Vuex 的设计思想
首先两者都是处理全局状态的工具库,大致实现思想都是:全局
state
保存状态---->dispatch(action)
------>reducer
(vuex
里的mutation
)----> 生成newState
; 整个状态为同步操作;
最大的区别在于处理异步的不同,vuex里面多了一步
commit
操作,在action
之后commit(mutation)
之前处理异步,而redux
里面则是通过中间件处理
redux 中间件
中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer 。这种机制可以让我们改变数据流,实现如异步 action ,action 过 滤,日志输出,异常报告等功能
常见的中间件:
redux-logger
:提供日志输出;redux-thunk
:处理异步操作;redux-promise
: 处理异步操作;actionCreator
的返回值是 promise
redux中间件的原理是什么
applyMiddleware
为什么会出现中间件?
function applyMiddleware(middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
)
return Object.assign({
}, store, {
dispatch })
}
上面的
middleware(store)(dispatch)
就相当于是const logger = store => next => {}
,这就是构造后的dispatch,继续向下传递。这里middlewares.reverse()
,进行数组反转的原因,是最后构造的dispatch
,实际上是最先执行的。因为在applyMiddleware
串联的时候,每个中间件只是返回一个新的dispatch
函数给下一个中间件,实际上这个dispatch
并不会执行。只有当我们在程序中通过store.dispatch(action)
,真正派发的时候,才会执行。而此时的dispatch
是最后一个中间件返回的包装函数。然后依次向前递推执行。
action、store、reducer分析
redux的核心概念就是store、action、reducer,从调用关系来看如下所示
store.dispatch(action) --> reducer(state, action) --> final state
// reducer方法, 传入的参数有两个 // state: 当前的state // action: 当前触发的行为, {type: 'xx'} // 返回值: 新的state var reducer = function(state, action){ switch (action.type) { case 'add_todo': return state.concat(action.text); default: return state; } }; // 创建store, 传入两个参数 // 参数1: reducer 用来修改state // 参数2(可选): [], 默认的state值,如果不传, 则为undefined var store = redux.createStore(reducer, []); // 通过 store.getState() 可以获取当前store的状态(state) // 默认的值是 createStore 传入的第二个参数 console.log('state is: ' + store.getState()); // state is: // 通过 store.dispatch(action) 来达到修改 state 的目的 // 注意: 在redux里,唯一能够修改state的方法,就是通过 store.dispatch(action) store.dispatch({ type: 'add_todo', text: '读书'}); // 打印出修改后的state console.log('state is: ' + store.getState()); // state is: 读书 store.dispatch({ type: 'add_todo', text: '写作'}); console.log('state is: ' + store.getState()); // state is: 读书,写作
store
store
在这里代表的是数据模型,内部维护了一个state
变量store
有两个核心方法,分别是getState
、dispatch
。前者用来获取store
的状态(state
),后者用来修改store
的状态// 创建store, 传入两个参数
// 参数1: reducer 用来修改state
// 参数2(可选): [], 默认的state值,如果不传, 则为undefined
var store = redux.createStore(reducer, []);
// 通过 store.getState() 可以获取当前store的状态(state)
// 默认的值是 createStore 传入的第二个参数
console.log('state is: ' + store.getState()); // state is:
// 通过 store.dispatch(action) 来达到修改 state 的目的
// 注意: 在redux里,唯一能够修改state的方法,就是通过 store.dispatch(action)
store.dispatch({
type: 'add_todo', text: '读书'});
action
redux
里是一个普通的js
对象action
必须有一个type
字段来标识这个行为的类型{
type:'add_todo', text:'读书'}
{
type:'add_todo', text:'写作'}
{
type:'add_todo', text:'睡觉', time
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。