1 Redux--js
应用的状态容器
a. 用户通过dispatch
一个action
,来提交对数据的修改。
store.dispatch(action)
b. store
自动调用reducers
,并且传入两个参数:旧state
和action
,reducers
会返回新的state
newState = reducers(state,action)
c. state
一旦有变化,store
就会调用监听函数
store.subscribe(listener)
d. 触发重新渲染view
1.1 创建store
在src文件夹下新建store.js
// ./src/store.js
import { createStore, applyMiddleware } from './redux'
function Reducer1(state = 0, { type, payload=1 }) {
switch(type){
case 'ADD':
return state + payload
case 'MUNIS':
return state - payload
default:
return state
}
}
function Reducer2 (state = {num:0}, {type, payload=1}){
switch(type){
case 'ADD2':
return {...state, num:state.num + payload}
default:
return state
}
}
// 创建store,combineReducers函数组合多个reducer。
// applyMiddleware 中间件函数,执行副作用,比如异步请求数据,延迟执行等
// thunk是加强函数,为中间件服务
const store = createStore(
combineReducers({count:Reducer1, count2:Reducer2}),
applyMiddleware(thunk)
)
export default store
// next 为上次返回的函数,action为接收的参数
// 如果action为函数,则执行的同时,传递俩参数,如果不是,则直接执行next
function thunk({dispatch, getState}) {
return next => action => {
if(typeof action === "function"){
return action(dispatch,getState)
}
next(action)
}
}
// 聚合多个reducers,hasChange通过新旧state比对,及新旧state个数的比对来判断是否更新state
function combineReducers(reducers) {
return function combination(state = {}, action) {
let newState = {}
let hasChange = false
for(let key in reducers) {
const reducer = reducers[key]
newState[key] = reducer(state[key],action)
hasChange = hasChange || newState[key] !== state[key]
}
hasChange = hasChange || Object.keys(newState).length !== Object.keys(state).length
return hasChange ? newState : state
}
}
在src文件夹下新建index.js文件
subscribe
监听state
变化dispatch
提交更新getState
获取状态值forceUpdate
刷新状态
// ./src/index.js
import React,{PureComponent} from 'react'
import {render} from 'react-dom'
import store from './store.js'
class App extends PureComponent{
componentDidMount(){
store.subscribe(() => {
this.forceUpdate()
})
}
count = () => {
store.dispatch({
type:"ADD",
payload:100
})
}
asynccount = () => {
store.dispath(() => {
setTimeout(() => {
store.dispatch({
type:"ADD",
payload:100
})
},1000)
})
}
render(){
return <div>
<p>{store.getState().count}</p>
<button onClick={this.count}>ADD</button>
<button onClick={this.asynccount}>asyncADD</button>
</div>
}
}
render(<App />,document.getElementById('root'))
在src文件夹下新建redux.js文件
// ./src/redux.js
import createState from './createState'
import applyMiddleware from './applyMiddleware'
export { createState, applyMiddleware }
在src文件夹下新建createState.js文件
// ./src/createState.js
export default function createState(reducer, enhance){
if(enhance) {
// 如果有加强函数,比如applyMiddleware,则执行,并且传入函数createState,reducer
return enhance(createState)(reducer)
}
let currentState // 当前状态值
let scribeList = [] // 监听列表
const getState = () => {
return currentState
}
const subscribe = (back) => {
scribeList.push(back)
// 返回取消监听函数
return () => {
const index = scribeList.indexof(back)
scribeList.splice(index, 1)
}
}
const dispatch = ({type, payload}){
currentState = reducer(currentState, {type, payload})
scribeList.map(lister => lister())
}
// 初始化state值
dispatch({type:"REDUXXXXXXXX/0000000"})
return { getState, subscribe, dispatch}
}
在src文件夹下新建文件applyMiddleware.js
// ./src/applyMiddleware.js
export default function applyMiddleware(...applyMiddles){
return createState => reducer => {
// 获取store
const store = createState(reducer)
// 获取需要传递的参数
const midApi = {
getState: store.getState,
dispatch: action => store.dispatch(action)
}
// 执行传递给中间件的函数,并生成函数数组
const applyMiddlewareChain = applyMiddles.map(applyMiddle =>
applyMiddle(midApi)
)
// 组合执行函数数组,并且把dispatch传递进去
const dispatch = compose(...applyMiddlewareChain)(store.dispatch)
// 返回加强版dispatch
return {...store,dispatch}
}
}
function compose(...funs){
if(funs.length === 0)return args => args
if(funs.length === 1){
return funs[0]
}
// 使用reduce函数,组合执行传递进来的函数,并传递参数,最终返回一个函数
return funs.reduce((a,b) => args => a(b(args)))
}
2 React-Redux
Provider
为后代组件提供store
connect
为组件提供数据和更新方法
修改src文件夹下的index.js文件
import React,{PureComponent} from 'react'
import {render} from 'react-dom'
import store from './store.js'
import {Provider, connect, bindActionCreators } from './ReactRedux.js'
// @为装饰器
// connect用于React与store的连接,返回一个新的已经与store连接的组件类
@connect(
// mapStateToProps函数,返回数据
state => ({count:state.count, count2:state.count2}),
// mapDispatchToProps函数,返回更新方法
// bindActionCreators函数,一个对象的多个方法传入dispatch,并执行
(dispatch) => {
let creators = {
Add: () => ({type:"ADD"}),
Add2: () => ({type:"ADD2"})
}
creators = bindActionCreators(creators, dispatch)
// 返回dispatch方法,使之在class里可以使用;
// 返回所有的更新方法
return {dispatch, ...creators}
}
)
class App extends PureComponent{
count = () => {
this.props.dispatch({
type:"ADD",
payload:100
})
}
asynccount = () => {
this.props.dispath(() => {
setTimeout(() => {
this.props.dispatch({
type:"ADD",
payload:100
})
},1000)
})
}
render(){
const { count, count2, Add, Add2} = this.props
return <div>
<p>{count}</p>
<button onClick={this.count}>点击</button>
<button onClick={Add}>ADD</button>
<button onClick={Add2}>{count2.num}</button>
<button onClick={this.asynccount}>asyncADD</button>
</div>
}
}
render(<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'))
在文件夹src下新建ReactRedux.js文件
// ./src/ReactRedux.js
import React from 'react'
// 创建跨组件通信Context
const Context = React.createContext()
export function Provider({store, children}){
return <Context.Provider value={store}>{children}</Context.Provider>
}
export const connect = (
mapStateToProps=state => state,
mapDispatchToProps
) => WrapperComponent => props => {
const store = React.useContext(Context)
const { getState, dispatch} = store
let dispatchProps = {dispatch}
const stateProps = mapStateToProps(getState())
if(typeof mapDispatchToProps === "object"){
dispatchProps = bindActionCreators(mapDispatchToProps,dispatch)
}else if(typeof mapDispatchToProps === "function"){
dispatchProps = mapDispatchToProps(dispatch)
}
// 创建一个自更新函数foreUpdate
const [ignored,foreUpdate] = React.useReducer(x=>x+1,0)
// 同步执行副作用,及监听函数
React.useLayoutEffect(() => {
const unsubscribe = store.subscribe(() => {
foreUpdate()
})
// 组件卸载时,清除监听函数
return () => {
if(unsubscribe)unsubscribe()
}
},[store])
return <WrapperComponent {...props} {...dispatchProps} {...stateProps} />
}
export function bindActionCreators(creators,dispatch){
const obj = {}
for(let key in creators){
obj[key] = actionCreator(creators[key],dispatch)
}
return obj
}
function actionCreator(creator,dispatch){
return (...args) => dispatch(creator(...args))
}
3 虚拟DOM
3.1 为什么需要虚拟DOM
呢,先来看看webkit
引擎的处理流程,如下图:
3.2 处理流程分为5步:
1,用HTML
分析器,分析html
元素,创建一颗DOM
树;
2,用css
分析器,分析css
文件和inline
样式,创建一张样式表;
3,将DOM
树和样式表关联起来,生成Render
树;
4,浏览器开始布局,为每个Render
树上的节点,确定一个在显示屏上出现的精确坐标值。
5,调用每个节点的paint
方法,将节点显示出来。
当你操作真实的DOM
时,即使一个微小的操作,代价也是高昂的,举个栗子:
打印一个简单的div
元素,查看它的属性
var div = document.createElement("div");
var str = "";
var allNum = 0
for(var key in div){
str = str + key + " ";
allNum++
}
console.log(str,allNum)
打印结果如下图
3.3 什么是虚拟DOM
虚拟DOM
是用js对象表示DOM
的信息和结构,是UI
的虚拟化表现显示,被保存于内存中,并通过如ReactDOM
等类库使之与真实的DOM
同步。当需要修改UI
的时候,重新渲染这个js
对象。
3.4 为什么使用虚拟DOM
真实DOM
操作很昂贵,消耗大。轻微的操作都可能导致页面重新排版。相对于虚拟DOM
处理js
对象,js
处理起来更快,更简单。通过diff
算法对比新旧js
对象,可以批量的,最小化的执行dom
操作,从而提高性能。
3.5 生成虚拟DOM
Babel-loader
通过React.createElement
(…)转换jsx
代码,生成vdom
,即虚拟dom
。JSX
代码如下:
// ./src/index.js
import React from './myreact/'
import ReactDOM from './myreact/react-dom'
const jsx = <div className="border">
<p>HELLO</p>
<a href="#">WORLD</a>
</div>
ReactDOM.render(jsx,document.getElementById("root"))
新建文件夹myreact,并在文件夹里新建index.js和react-dom.js文件
// ./src/myreact/index.js
// 创建render函数
function render(vnode,container){
console.log('vnode-----',vnode)
}
export default { render }
// ./src/myreact/react-dom
// 创建createElement函数
function createElement(type, config, ...children){
const props = {
...config,
children: children.map(child => typeof child === 'Object' ? child :
createTextNode(child))
}
return {
type,
props
}
}
//处理成对象,方便统一处理
function createTextNode(text){
return {
type:"TEXT",
props:{
children: [],
nodeValue: text
}
}
}
export default { createElement }
通过上面的代码打印出来的虚拟DOM树如图:
4 React 三大API(render,createElement,component
)
4.1 render
函数将虚拟DOM
转换成真实DOM
,并且插入到页面中
// ./src/myreact/react-dom.js
// 创建render函数
function render(vnode,container){
console.log('vnode-----',vnode)
const node = createNode(vnode) // 把虚拟DOM转化成真实DOM
container.appendChild(node) // 把node插入到container中
}
// 生成真实的dom节点
function createNode(vnode){
let node = null
const { type, props } = vnode
if(type === 'TEXT'){
// 如果是文本,则创建文本节点
node = document.createTextNode("")
}else if(typeof type === 'string'){
// 证明就是html节点,比如div,span,p等标签,直接创建标签节点
node = document.createElement(type)
}
// 循环遍历子元素
reconcileChildren(props.children, node)
// 添加属性
updateNode(node,props)
return node
}
function reconcileChildren(children, node){
for(let i=0,len=children.length; i<len; i++ ){
let child = children[i]
// child是vnode,那需要把vnode转化成真实dom节点,然后插入父节点node中
render(child,node)
}
}
function updateNode(node, nodeValue){
//过滤掉children
Object.keys(nodeValue).filter(k => k!=='children')
.forEach(k => {
//把props的属性添加到节点上
node[k] = nodeValue[k]
})
}
export default { render }
到此,节点及文本就显示在页面里了。在src/index.js里添加类组件
// ./src/index.js
import React from './myreact/'
import ReactDOM from './myreact/react-dom'
import Component from './myreact/Component'
//class类组件,使用到Component API
class ClassComponent extends Component {
render(){
return <div className="class border">{this.props.name}</div>
}
}
function FunctionComponent({name}){
return <div className="function border">{name}</div>
}
const jsx = <div className="border">
<p>HELLO</p>
<a href="#">WORLD</a>
<ClassComponent name="class component" />
<FunctionComponent name="function component" />
</div>
ReactDOM.render(jsx,document.getElementById("root"))
在myreact文件夹里新建文件Component.js
// ./src/myreact/Component.js
class Component{
static isClassComponent={}
coustructor(props){
this.props = props
}
}
export default Component
在文件./src/myreact/react-dom.js的createNode函数中添加代码
// ./src/myreact/react-dom.js
// 生成真实的dom节点
function createNode(vnode){
let node = null
const { type, props } = vnode
if(type === 'TEXT'){
// 如果是文本,则创建文本节点
node = document.createTextNode("")
}else if(typeof type === 'string'){
// 证明就是html节点,比如div,span,p等标签,直接创建标签节点
node = document.createElement(type)
}else if(typeof type === 'function'){
//通过Component类里定义的静态参数,判断是类还是函数
node = type.isClassComponent ?
updateClassComponent(vnode) : updateFunctionComponent(vnode)
}
// 循环遍历子元素
reconcileChildren(props.children, node)
// 添加属性
updateNode(node,props)
return node
}
// 处理类组件
function updateClassComponent(vnode){
const {type, props} = vnode
//类需要new来运行
const cmp = new type(props)
//类的节点在render函数里
const vvnode = cmp.render()
//返回真实的dom节点
const node = createNode(vvnode)
return node
}
// 处理函数组件
function updateFunctionComponent(vnode){
const {type, props} = vnode
const vvnode = type(props)
const node = createNode(vvnode)
return node
}
5 diff
算法
React
通过执行Render
函数就会生成一颗树,state
或者props
变化后,Render
函数又会生成另一颗树。React需要基于两颗树之间的差异来判断如何有效率的更新UI,以保证UI和最新的树保持一致。
有一些通用的算法,可以实现从一颗树到另一颗树的最小操作数。然而即使最前沿的算法,该算法的复杂度也是O(n³),n为元素个数。如果展示1000个元素,计算量在十亿级别的,消耗过于高昂。
于是React
在一下两个假设下,提出一个O(n)的启发式算法:
1,两个不同类型的元素,会产生不同的树。
2,开发者通过key
,prop
来暗示哪些子元素在不同的渲染下能保持稳定。
diffing算法,应运而生,算法复杂度为O(n)
diff策略
1,同级比较,Web UI中的DOM
跨层级的移动操作非常少,可以忽略不计。
2,拥有不同类型的两个组件,将会生成不同的树型结构。
3,开发者可以通过key,prop
来暗示哪些子元素在不同的渲染下能保持稳定。
diff过程
对比两个虚拟dom
时,会有三种操作删除,替换,更新
vnode
是现在的虚拟dom,newVnode
是新的虚拟dom
1,删除:newVnode
不存在时
2,替换:vnode
和newVnode
类型不同或key不同
3,更新:有相同的类型和key
,但是vnode
和newVnode
不同时
6,Fiber
6.1,为什么需要fiber
1,对于大型项目来说,组件树会很大,这个时候遍历递归的成本就会很大,会造成主线程被持续占用,结果就是主线程上的布局,动画等周期性任务就无法立即得到处理,造成视觉上的卡顿,影响用户体验。
2,任务分解的意义,解决上面的问题。
3,增量渲染(把渲染任务拆分成块,均到多帧)
4,更新时能够暂停,终止,复用渲染任务。
5,给不同类型的更新赋予优先级
6,并发方面新的基础能力
7,更流畅
6.2,实现fiber
window.requestIdleCallback()
方法能够在浏览器空闲时间段调用函数排队。
6.3,fiber的数据结构-单向链表
以fiber
为起点,第一个子biber
为child
,child
的兄弟元素为sibling
,每个元素都有return
,指向父元素。
6.4,fiber的数据结构解析:
type
:标记当前节点的类型key
:标记唯一性props
:属性child
:第一个子元素sibling
:下一个兄弟元素return
:指向父元素node
:真实的dom
节点base
:记录下当前的节点effectTag
:“PLACEMENT”,“UPDATE”,“DELETION” 对节点的操作类型
// ./kreact/react-dom
// 下一个要执行的fiber, 数据结构就是fiber
let nextUnitOfWork = null;
// work in propgress 正在进行当中的,结构类型是fiber
let wipRoot = null;
function render(vnode, container) {
wipRoot = {
node: container,
props:{
children: [ vnode ]
},
base: null
}
nextUnitOfWork = wipRoot
// const node = createNode(vnode)
// console.log('vdom----', vnode)
// container.appendChild(node)
}
// 创建节点
function createNode(vnode) {
let node = null
const { type, props } = vnode
if (type === 'TEXT') {
node = document.createTextNode('')
} else if (typeof type === 'string') {
node = document.createElement(type)
}
//else if (typeof type === 'function') {
// node = type.isClassComponent
// ? updateClassComponent(vnode)
// : updateFunctionComponent(vnode)
// }
// reconcileChildren(props.children, node)
updateNode(node, props)
return node
}
// 添加属性到真实dom里
function updateNode(node, nodeValue) {
Object.keys(nodeValue)
.filter((k) => k !== 'children')
.forEach((k) => {
node[k] = nodeValue[k]
})
}
// 遍历一下子节点-old
//function reconcileChildren(children, node) {
// for (let i = 0, len = children.length; i < len; i++) {
// render(children[i], node)
// }
//}
// 协调一下子节点
// 1,给workInProgress添加一个child节点,就是children的第一个子节点形成的fiber
// 2,形成fiber架构,把children里节点遍历下,形成fiber链表状
function reconcileChildren(workInProgress, children) {
let prevSibling = null // 保存每次循环生成的fiber
for (let i = 0, len = children.length; i < len; i++) {
let child = children[i];
// 组装fiber结构
let newFiber = {
type:child.type,
props:child.props,
node:null,
base:null,
return:workInProgress,
effectTag:"PLACEMENT" //PLACEMENT,UPDATE,DELETION
}
if(i === 0){
workInProgress.child = newFiber
}else{
prevSibling.sibling = newFiber // 上一次fiber的sibling指向新生成的fiber
}
prevSibling = newFiber //储存当前生成的fiber
}
}
// 协调class组件
function updateClassComponent(fiber) {
const {type,props} = fiber
const cmp = new type(props)
const children = [cmp.render()]
reconcileChildren(fiber,children)
}
// 协调函数组件
function updateFunctionComponent(fiber) {
const {type,props} = fiber
const children = [type(props)]
reconcileChildren(fiber,children)
}
// 协调原生元素
function updateHostComponent(fiber){
if(!fiber.node){
fiber.node = createNode(fiber)
}
const {children} = fiber.props
reconcileChildren(fiber,children)
}
function performUnitOfWork(fiber){
// step1:执行更新当前fiber,
const {type} = fiber
if(typeof type === "function"){
type.isClassComponent ?
updateClassComponent(fiber) :
updateFunctionComponent(fiber)
}else{
// 原生标签的更新函数
updateHostComponent(fiber)
}
// step2:并且返回下一个要执行的fiber
// 原则就是:先看下有没有子元素
if(fiber.child){
return fiber.child
}
// 没有子元素,找兄弟元素
let nextFiber = fiber
while(nextFiber){
if(nextFiber.sibling){
return nextFiber.sibling
}
nextFiber = nextFiber.return
}
}
// 浏览器空闲时段执行的回调函数
function workloop(deadline){
while(nextUnitOfWork && deadline.timeRemaining()>1){
// 执行更新当前fiber,并且返回下一个要执行的fiber
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
if(!nextUnitOfWork && wipRoot){
//没有下一个任务了,执行提交
commitRoot()
}
}
requestIdleCallback(workloop)
}
// 浏览器空闲时段执行
requestIdleCallback(workloop)
// 提交函数
function commitRoot(){
commitWorker(wipRoot.child)
wipRoot = null //当前的节点置为空,避免死循环
}
function commitWorker(fiber){
if(!fiber)return
// parentNode是fiber的离的最近的父或祖先节点
let parentNodeFiber = fiber.return
while(!parentNodeFiber.node){
parentNodeFiber = parentNodeFiber.return
}
let parentNode = parentNodeFiber.node
// fiber 有node节点
if(fiber.effectTag === "PLACEMENT" && fiber.node !== null){
parentNode.appendChild(fiber.node)
}
// 添加子元素
commitWorker(fiber.child)
// 添加兄弟元素
commitWorker(fiber.sibling)
}
export default { render }
参考资料:
1,京东内部项目沉淀。
2,开课吧-前端全栈工程师,培训内容之React篇。