一次学习,多端受用
react中抽象了一层虚拟dom,所以我们可以频繁的修改状态,但是更改的都是虚拟dom。当虚拟dom发生变化后,会集中更新到真实的dom,因为虚拟dom的存在,只要替换掉底层的渲染引擎,就可以突破浏览器。pureComponent
class PureComponent extends Component {
shouldComponentUpdate (nextProps, nextState) {
const { state, props } = this
function shdowCompare(a, b) {
return a === b || Object.keys(a).every(k => a[key] === b[k])
}
return shdowCompare(state, nextState) && shdowCompare(props, nextProps)
}
}
react中使用createElement和JSX来实例化组件
componentWillReceiveProps 一般用来将新的props同步到state。
为了提升性能,react会把多次setState合并成一次。组件和事件
react事件是天生的事件代理,看起来散落在元素上,其实react仅仅在根元素绑定事件,所有事件通过事件代理响应。
事件中的事件对象并不是原生事件对象,而是封装过的,屏蔽了浏览器的差异。react也提供了访问原生事件的方式。
class EventEmitter {
constructor () {
this.eventMap = {}
}
sub (name, cb) {
const eventList = this.eventMap[name] = this.eventMap[name] || []
eventList.push(cb)
}
pub(name, ...data) {}
}
- 高阶组件
function HOC1(innerComponent) {
return class WrapComponent extends Components {
render () {
return ({{ this.props.children }} )
}
}
}
function HOC2(innerComponent) {
return class WrapComponent extends innerComponent{
}
}
function SetTimeoutHOC (InnerComponent) {
return class wrapComponent extends InnerComponent {
componentwill
}
}
通过ref调用react组件以及组件中的方法,同时也可以调用原生dom
react会对输出的内容进行xss过滤,但有时不需要,比如这个接口返回html片段的情况下,通过dangerousSetInnerHTML可以将HTML片段直接设置到DOM上。
- 初识store
let store = {
dispatch,
getState,
subscribe,
replaceReducer
}
dispatch: 派发action
subscribe(listener): 订阅页面数据状态,即store中state的变化
getState: 获取当前页面状态树
replaceReducer(nextReducer): 社区一些热更新或者代码分离技术可能会使用到。
- 编写reducer函数更新数据
const updateReducer (preState, action) {
switch (action.type) {
case 'case1':
return newState1
default:
return preState
}
}
当无法匹配action时,默认返回preState
当页面数据状态更新之后,如何促使页面发生UI更新?实际上是store.subscribe(cb)订阅数据更新,并由cb完成UI更新。
合理拆分reducer函数
普通变量存在于栈内存中,但是对象其实存在于堆内存。当我们获取对象时,首先获取栈内存的引用地址,然后根据引用地址从堆内存中获取所需要的值。deepClone
const deepClone = data => {
let t = type(data), o, i, length;
// 创建新的数组和对象
if (t === 'array) {
o = []
} else if (t === 'object') {
o = {}
} else { return data }
if (t == 'array') {
data.forEach(v => o.push(deepClone(v)))
return o
}
}
实际开发中,如果数据有多层。深拷贝对于开发性能并不友好。
Redux 中间件和异步
中间件就是在派发action和执行reducer之间,添加自定义扩展功能。
中间件可以在action到达reducer之前进行日志记录,中断action触发,甚至修改action。
- redux-thunk中间件
假如有一个异步需求,比如需要派发一个网络请求action,在网络请求返回之后再派发一个action返回数据渲染页面。
设想一下:如果dispatch可以接收一个函数作为参数,在函数体内进行异步操作,并在异步完成后再派发action。
store.dispatch(fetchNewBook('learnRedux'))
function fetchNewBook (book) {
return (dispatch) => {
dispatch({})
ajax({}).then(v => { dispatch({}) })
}
}
给dispatch函数传一个异步函数fetchNewBook,这就是redux-thunk中间件对dispatch功能的增强,注意:中间件参数顺序有讲究。
react和redux的衔接点
root组件需要获取页面的状态数据,并向下进行派发。这样,store.getState()的返回值就需要传递给root组件,作为props的存在。同时又需要在组件中调用dispatch方法。再通过store.subscribe()订阅state的改变。使用react-redux库
容器组件:指的是数据状态和逻辑的容器。它并不负责展示,只维护内部状态,进行数据分发和派发action。
展示组件:只负责接收数据并展示。
那么react-redux怎么生成容器组件?
Connect(mapStateToProps, mapDispatchToProps, mergeProps, options)(component)
connect用来连接容器组件和展示组件。connect的核心是将开发者定义的组件包装转换成容器组件。所生成的容器组件能使用store中的哪些数据,全由connect参数决定。
connect是典型的柯里化函数,第一次设置参数,第二次接收一个组件,并在该组件的基础上返回一个容器组件。第一个参数是一个函数,它的返回值将设置给组件的props,默认情况下dispatch会注入到组件的props上。
那么connect是如何获取store中的内容呢?
一般做法是将provider作为整个应用的根组件,并获取store作为它的prop。
深入理解redux
- redux源码探索-store的实现
store = {
dispatch,
getState,
subscribe,
replaceReducer
}
const render = () => {
document.body.innerText = store.getState()
}
store.subscribe(render)
render()
接下来,我们思考如何实现store。
const createStore = (reducer) => {
let state
let listeners = []
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach(listener => listener())
}
const subscribe = (listener) => {
listeners.push(listener)
return () => {
listeners = listeners.filter (item => item !== listener)
}
}
return {
getState,
dispatch,
subscribe
}
}
subscribe返回一个函数用于取消订阅,其实现方式是数组的filter方法。
createStore被调用后,Redux就会设置一个初始的空状态,我们只需要在createStore方法中加入如下一行触发一个空action即可。
- combineReducers的实现
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
// 进入不存在的action会返回原来的state
nextState[key] = reducers[key] (state[key], action)
return nextState
}, {})
}
}
combineReducers返回一个rootReducers, rootReducers返回经过各个reducer处理后的全新数据状态,既更新后的state。
为了获取所有reducer的计算结果,我们使用Object.keys对Object进行遍历,这样我们就可以根据数组的每一项进行state的计算,因为这个数组的每一项都是自定义的reducer函数。
- dispatch的改造 - 实现记录日志
dispatch实质就是一个函数,它负责调用reducer方法,依靠reducer的执行进行状态变更,接着依次执行各监听函数,目的是实现视图的更新。
这样我们的入手点就在dispatch函数的入手前后。
创建一个addLogToDispatch函数,用来生成取代原始的dispatch方法。这个函数对store中的dispatch进行拦截,并记录原始的dispatch为rawDispatch,addLogToDispatch在行为应予原始dispatch保持一致。
const addLogToDispatch = (store) => {
const rawDispatch = store.dispatch
return (action) => {
console.log('pre state:', store.getState())
const returnvalue = rawDispatch(action)
console.log('next state:', store.getState())
return returnvalue
}
}
- dispatch的改造 - 识别Promise
异步场景原理是令dispatch接收一个函数,在这个函数中进行异步操作,既然dispatch可以接收一个函数,那么也可以接收一个promise
思路是dispatch接收一个promise对象,在这个promise对象resolve后,我们使用原始的dispatch进行触发。
const addLogToDispatch = (store) => {
const rawDispatch = store.dispatch
return (action) => {
if (typeof action.then == 'function') {
return action.then(rawDispatch)
}
rawDispatch(action)
}
}
这里使用一种比较投机的方式,检查action参数是否有then方法,且这个方法是函数类型,既判断action是否是一个thenable对象,如果是就会等待promise resolve,生成一个js对象,这个对象就是标准的action。代码执行时,每一个中间件获得的dispatch都已经被改造,声明一个中间件·数组是一个更好的方式,redux的思想就是先将dispatch增强改造函数保存起来,然后提供给redux,每一个中间件都对dispatch进行改造,并将改造后的dispatch,既next向下传递。
中间件的执行过程依赖中间件数据和dispatch
const wrapDispatchM = (store, middleware) => {
middleware.forEach(m =>store.dispatch = m(store)(store.dispatch))
}
const promise = (store) => (next) => (action) => {
if (typeof action.then === 'function') {
return action.then(next)
}
return next(action)
}
m数组的执行顺序与我们预期执行的顺序相反
const wrapDispatchM = (store, middleware) => {
middleware.slice().reverse().forEach(m =>store.dispatch = m(store)(store.dispatch))
}
中间件实际上就是action在到达reducer之前,增加的一个中间环节。
function applyMiddleware (...middlewares) {
return (next) =>
(reducer, initialState) => {
let store = next(reducer, initialState)
let dispatch = store.dispatch
let chain = []
let middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(m => m(middlewareAPI))
dispatch = compose(...chain, store.dispatch)
return { ...store, dispatch }
}
}
function compose (...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return func[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
A.B.C.store.dispatch => A(B(C(store.dispatch)))
A.B.C.D => A(B(C(D)))
[A.B.C.D].reduce((a. b) => a(b)) = A(B(C(D)))
let listeners = []
let state = ;
function dispatch (action) {
state = reducer(state, action)
listeners.slice().forEach(i => i())
return action
}
function subscribe (listener) {
listeners.push(listener)
return function (i) {
listeners = listeners.filter (i => i !== listener)
}
}
- react-redux 究竟是什么
provider组件
connect组件
- 通过context获取provider的store,因此它具有了访问store.state
揭秘react同构应用
一个完整的应用除了纯粹的静态内容,还包括各种响应事件,用户交互等。这就意味浏览器还要执行js脚本,以完成绑定事件、处理异步交互等工作。浏览器进一步渲染的过程中,判断已有的dom结构和即将渲染的结构是否相同,若相同,则不重新渲染dom结构,只进行事件绑定即可。
renderToString生成的HTMl字符串的每个DOM节点都有一个data-react-id属性,根节点会有一个data-checksum. 如果两个组件有相同的props和DOM结构,那么data-checksum属性是一样的。浏览器通过data-checksum判断react组件是否只渲染一次。
使用ReactDOM.hydrate 会最大限度的保留服务器端渲染的结构。
react16还提供了renderToNodeStream 方法实现服务器端渲染。该方法将持续产生字节流,最终通过流的形式返回HTML字符串,这样有利于页面初始加载和首屏时间。
- 因为在服务器端不支持组件挂载的浏览器环境,所以react组件只有componentDidMount之前的生命周期方法,所以我们最好将依赖浏览器的特性放到componentDidMount中处理。
因为服务器不存在ajax的概念,所以可以试用isomorphic-fetch实现一致性的封装。
可以通过window是浏览器特有的对象这一特点,进行环境和逻辑区分。
浏览器端可以通过服务器端注入的全局变量得到初始状态。
function throttle (fn, interval) {
let doing = false;
return () => {
if (doing) return;
fn()
setTimeout(() => { doing = false }, interval || 500)
}
}
- Function as Child Component
class ScrollPos extends Component {
state = { position: null }
componentDidMount = () => {
window.addEventListener ('scroll', this.handleScroll)
}
componentWillUnMount = () => {
window.removeEventListener ('scroll', this.handleScroll)
}
render() {
return (
{this.props.children(this.state.position)}
)
}
}
this.props.children会有多种类型,其中包括undefined, array,object
- setState
setState 存在延迟批处理情况,但有一些更新又是同步,setState除了更改this.state的值之外,还要负责触发重新渲染逻辑,这里面要经过核心的diff算法,最后才决定是否渲染,以及如何渲染。
深入源码,setState中会根据isBatchingUpdates变量判断是直接更新state,还是放到队列中稍后更新。
总结:在react控制的事件处理中,setState不会同步更新状态,而在react控制之外则会同步更新。在交互过程中,使用的都是在React库中封装的事件,例如select,input,button等,这种情况下setState就会以异步的方式执行。
实际上绕过react,直接通过js原生直接添加事件处理函数,就会出现同步更新的状态。
在编写react组件时,我们使用JSX来描述虚拟DOM,事实上,JSX总被编译成createElement,一般babel为我们做了这件事情。
const helloWorld = React.createElement('div', null, 'hello')
ReactDOM.render(helloWorld, document.getElementById('root'))
我们需要使helloWorld创建的节点渲染出来并插入到root节点中
function anElement(ele, child) {
if (typeof ele === 'function') {
return ele()
} else {
const anele = document.createElement(ele)
anele,innerHTML = child.join('')
return anele
}
}
function createElement (el, props, ...child) {
return anElement(el, child)
}
window.React = {
createElement
}
window.ReactDOM = {
render: (el, root) => { root.appendChild(el) }
}
再深入考虑,children也不仅仅是简单的文本节点,它还可能有其他子组件。
加入我们需要创建一个Component父类,在此类中进行props的初始化和赋值。
class Component {
constructor (props) {
this.props = props
}
}
- 优化
- 对已经实例化的class进行缓存
- 当处理一个已经在缓存池中的class时,直接返回实例
- 使用标志位对class进行标识
- 对于新的渲染诉求,生成新的dom树
- 计算两颗dom树的diff
- 将diff更新到真实的dom中
- 使用render方法对v-dom进行操作,而不是直接应用在真实的dom中
- 收集diff,并计算出对真实的dom所做的最小更新
- 将最小更新应用在真实的dom中
关于diff的细节,将两棵树比较时间复杂度降为了o(n),具体表现为
- 前端页面中,跨层级的操作特别少,可以忽略不计
- 拥有相同类型的两个组件,拥有相似的树形结构,拥有不同类型的两个组件,拥有不同的树形结构
- 对于同一层级的一组节点,可以通过唯一id区分
基于第一点,react对树的比较算法,实际上只对树进行分层比较,两棵树只会对同一层的节点比较。这样只需要遍历一次树。基于第二点,react在diff时,同一类型的组件,按照原策略进行比较,如果类型不同,则直接进行替换。基于第三点,列表节点组件通过开发者设置的唯一key,来协助实现添加、删除和排序操作。
当然react允许开发者通过shuoldComponentUpdate方法直接决定组件是否需要diff
redux数据扁平化策略
体积过大的性能优化,基于公共资源的分割,给公共的资源包添加缓存。
基于业务的代码分割:
有了以上认知,我们进一步思考,是否可以将app也进行拆分
合理选择分割维度
- 按照业务逻辑和依赖库划分
- 按照路由划分
- 按照组件划分
import Loadable from 'react-loadable'
const MyLoadingSpinner = () => {
}
- 按需加载实现原理
使用syntax-dynamic-import 这样一个babel插件,通过动态导入实现按需加载,默认情况下,导入的模块是静态的,接下来我们看一看,react如何与动态导入相结合。
对按需加载的关注点在控制加载上,比如如何加载脚本,脚本是否加载。
class Async extends Component {
componentWillMount = () => {
this.cancelUpdate = false
this.props.load.then(c => {
this.C = c
if (!this.cancelUpdate) {
this.forceUpdate()
}
})
}
componentWillUnmount = () => { this.cancelUpdate = true }
render = () => {
const { componentProps } = this.props
return this.C ? this.C.default ?
: : null
}
}
- 正确理解虚拟dom带来的优化
浏览器解析HTML之后,渲染引擎负责展现和渲染页面样式。还需要配合解析css,构建渲染树。
react通过以下几种方式保证虚拟dom diff算法和更新的高效性能。
- 高效的diff算法
- Batch操作
当任何一个组件使用setState方法时,会触发组件本身重新渲染。同时因其维护两套虚拟dom,一套更新后的,一套更新前的。通过对这两套虚拟运用diff,找到需要变化的最小单元集,然后把这个最小单元集运用在真实的DOM中。
当同层组件比较时,如果state或props发生变化,则直接重新渲染组件本身。同一层节点比较时,开发者可以使用key属性来‘声明’同一层级节点的更新方式。
找到最小单元集后,更新不一定同步进行。react会进行setState的Batch操作。
浅浅比较,复杂类型只判断引用是否相同。
在使用purecomponent的时候,在更新props和state的时候,返回一个新的对象和数组。