扯框架
# --- React ---
react 通过 babel 将 jsx 转化为
React.createElement( type, [props], [...children] )
,React.createElement
返回包含元素(element)信息的对象,然后通过调用ReactDOM.render(element, container)
,把元素更新到 DOM 上
# Diff 算法
# diff 策略
- tree diff
- 概念: 将新旧两颗虚拟 DOM 树,按照层级对应的关系,从头到尾的遍历一遍,,就能找到那些元素是需要更新的
- 只会对相同颜色方框内(同级)的DOM节点进行比较,即同一父节点下的所有子节点
- 当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较
- Component diff
- 概念: 在对比每一个层级的时候,会有自己的组件
- 如果类型相同,暂时不更新,
- 如果类型不相同,就需要更新; ( 删除旧的组件,再创建一个新的组件,插入到删除组件的那个位置)
- element diff
- 概念: 在类型相同的组件内, 再继续对比组件内部的元素,查看内部元素是否相同,如果需要修改,找到需要修改的元素,进行针对性的修改!
- 三种节点操作: 1 INSERT_MARKUP(插入) 2 MOVE_EXISTING(移动) 3 REMOVE_NODE(删除)
- 概念: 在类型相同的组件内, 再继续对比组件内部的元素,查看内部元素是否相同,如果需要修改,找到需要修改的元素,进行针对性的修改!
- 只比较同一层级节点
- 通过 key 设置唯一标识,对
element diff
进行算法优化 - 传统diff树时间复杂度O(n^3);react diff 时间复杂度 O(n)
# 生命周期
# 新生命周期在各个阶段的调用情况
- 挂载
- constructor
- getDerivedStateFromProps
- render
- componentDidMount
- 更新
- getDerivedStateFromProps
- shouldComponentUpdate
- render
- getSnapshotBeforeUpdate
- componentDidUpdate
- 卸载
- componentWillUnmount
挂载阶段:
- constructor: 构造函数,最先被执行,我们通常在构造函数里初始化state对象或者给自定义方法绑定this
- getDerivedStateFromProps:
static getDerivedStateFromProps(nextProps, prevState)
,这是个静态方法,当我们接收到新的属性想去修改我们state,可以使用getDerivedStateFromProps - render: render函数是纯函数,只返回需要渲染的东西,不应该包含其它的业务逻辑,可以返回原生的DOM、React组件、Fragment、Portals、字符串和数字、Boolean和null等内容
- componentDidMount: 组件装载之后调用,此时我们可以获取到DOM节点并操作,比如对canvas,svg的操作,服务器请求,订阅都可以写在这个里面,但是记得在componentWillUnmount中取消订阅
更新阶段:
- getDerivedStateFromProps: 此方法在更新个挂载阶段都可能会调用
- shouldComponentUpdate:
shouldComponentUpdate(nextProps, nextState)
,有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回一个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利用此生命周期来优化React程序性能 - render: 更新阶段也会触发此生命周期
- getSnapshotBeforeUpdate:
getSnapshotBeforeUpdate(prevProps, prevState)
,这个方法在render之后,componentDidUpdate之前调用,有两个参数prevProps和prevState,表示之前的属性和之前的state,这个函数有一个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此生命周期必须与componentDidUpdate搭配使用 - componentDidUpdate:
componentDidUpdate(prevProps, prevState, snapshot)
,该方法在getSnapshotBeforeUpdate方法之后被调用,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要用到 DOM 元素的状态,则将对比或计算的过程迁移至 getSnapshotBeforeUpdate,然后在 componentDidUpdate 中统一触发回调或更新状态。
卸载阶段:
- componentWillUnmount: 当我们的组件被卸载或者销毁了就会调用,我们可以在这个函数里去清除一些定时器,取消网络请求,清理无效的DOM元素等垃圾清理工作
# 旧生命周期在各个阶段的调用情况
- 挂载
- constructor
- componentWillMount
- render
- componentDidMount
- 更新
- componentWillReceiveProps
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
- 卸载
- componentWillUnmount
# react 父子组件的生命周期执行顺序
首次渲染
父 constructor 父 getDerivedStateFromProps 父 render 子 constructor 子 getDerivedStateFromProps 子 render 子 componentDidMount 父 componentDidMount
1
2
3
4
5
6
7
8父组件数据修改触发重渲染
父 getDerivedStateFromProps 父 shouldComponentUpdate 父 render 子 getDerivedStateFromProps 子 shouldComponentUpdate 子 render 子 getSnapshotBeforeUpdate 父 getSnapshotBeforeUpdate 子 componentDidUpdate, snapshot: 1 父 componentDidUpdate, snapshot: 1
1
2
3
4
5
6
7
8
9
10子组件数据修改触发重渲染
子 getDerivedStateFromProps 子 shouldComponentUpdate 子 render 子 getSnapshotBeforeUpdate 子 componentDidUpdate, snapshot: 1
1
2
3
4
5父组件调用forceUpdate
父 getDerivedStateFromProps // 父 shouldComponentUpdate 不执行 父 render 子 getDerivedStateFromProps 子 shouldComponentUpdate 子 render 子 getSnapshotBeforeUpdate 父 getSnapshotBeforeUpdate 子 componentDidUpdate, snapshot: 1 父 componentDidUpdate, snapshot: 1
1
2
3
4
5
6
7
8
9
10子组件调用forceUpdate
子 getDerivedStateFromProps // 子 shouldComponentUpdate 不执行 子 render 子 getSnapshotBeforeUpdate 子 componentDidUpdate, snapshot: 1
1
2
3
4
5销毁
父 componentWillUnmount 子 componentWillUnmount
1
2
# setState是异步的还是同步的
先给出答案: 有时表现出异步,有时表现出同步
setState
只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的。setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)
中的callback
拿到更新后的结果。setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新。
# 下面的代码输出什么?
class Example extends React.Component {
constructor() {
super();
this.state = {
val: 0
};
}
componentDidMount() {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 1 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 2 次 log
setTimeout(() => {
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 3 次 log
this.setState({val: this.state.val + 1});
console.log(this.state.val); // 第 4 次 log
}, 0);
}
render() {
return null;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
第一次和第二次都是在 react 自身生命周期内,会调用batchedUpdates,触发时 isBatchingUpdates 为 true,所以并不会直接执行更新 state,而是加入了 dirtyComponents,所以打印时获取的都是更新前的状态 0。
两次 setState 时,获取到 this.state.val 都是 0,所以执行时都是将 0 设置成 1,在 react 内部会被合并掉,只执行一次。设置完成后 state.val 值为 1。
setTimeout 中的代码,因为没有前置的 batchedUpdate 调用,触发时 isBatchingUpdates 为 false,没有走到 dirtyComponents 分支,所以能够直接进行更新,所以连着输出 2,3。
输出: 0 0 2 3
# batchingStrategy (批处理策略)
react的setState更新机制的关键就是 batchingStrategy
,其实它只是一个简单的对象,定义了一个 isBatchingUpdates 的布尔值,和一个 batchedUpdates 方法,下面是简化版的定义
var batchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
// ...
batchingStrategy.isBatchingUpdates = true;
transaction.perform(callback, null, a, b, c, d, e);
}
};
2
3
4
5
6
7
8
9
10
# 总结
关键字:isBatchingUpdates、batchedUpdates、dirtyComponents
在React中,setState是一个异步方法,由队列实现。它有 Batch模式(批量更新模式) 和普通模式。在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的。
原因:在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。
# state 值改变的五种方式
// 方式 1
let {count} = this.state
this.setState({count:2})
// 方式 2:callBack
this.setState(({count})=>({count:count+2}))
// 方式 3:接收 state 和 props 参数
this.setState((state, props) => {
return { count: state.count + props.step };
});
// 方式 4:hooks
const [count, setCount] = useState(0)
setCount(count+2)
// 方式 5:state 值改变后调用
this.setState(
{count:3},()=>{
//得到结果做某种事
}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# react组件的通信
# 方法
- props / 自定义事件,触发回调
- Context : Provider,Consumer
- redux
- event 插件: EventEmitter
- 路由传参
- ref
# 场景
父组件向子组件通讯: 父组件可以向子组件通过传 props 的方式,向子组件进行通讯
子组件向父组件通讯: props+回调的方式,父组件向子组件传递 props 函数,子组件调用该函数,将数据作为参数传递给父组件
兄弟组件通信: 找到这两个兄弟节点共同的父节点,结合上面两种方式由父节点转发信息进行通信
跨层级通信:
Context
设计目的是为了共享那些对于一个组件树而言是“全局”的数据发布订阅模式: 发布者发布事件,订阅者监听事件并做出反应,我们可以通过引入event模块进行通信
全局状态管理工具: 借助Redux或者Mobx等全局状态管理工具进行通信,这种工具会维护一个全局状态中心Store,并根据不同的事件产生新的状态
# React如何进行组件/逻辑复用
- hoc:
- 属性代理
- 操作 props
- 通过 Refs 访问到组件实例
- 提取 state
- 用其他元素包裹 (WrappedComponent)
- 反向继承
- 渲染劫持(Render Highjacking)
- 操作 state
- 属性代理
- renderProps
- react-hooks
# HOC高阶组件
高阶组件就是一个接收一个组件并返回另外一个新组件的函数!
# 高阶组件总共分为两类
属性代理(Props Proxy)
- 操纵 props
- 通过 refs 获取组件实例
- 抽象状态 state
- 把 WrappedComponent 与其它 elements 包装在一起
反向继承(Inheritance Inversion)
渲染劫持(Render Highjacking)
『读取、添加、修改、删除』任何一个将被渲染的 React Element 的 props
在渲染方法中读取或更改 React Elements tree,也就是 WrappedComponent 的 children
根据条件不同,选择性的渲染子树
给子树里的元素变更样式
渲染 指的是 WrappedComponent.render 方法
操作 state
# 属性代理 Props Proxy (PP)
function ppHOC(WrappedComponent) {
return class PP extends React.Component {
render() {
return <WrappedComponent {...this.props}/>
}
}
}
2
3
4
5
6
7
属性代理之 操纵 prop
删除 prop
import React from 'react' function HocRemoveProp(WrappedComponent) { return class WrappingComPonent extends React.Component { render() { const { user, ...otherProps } = this.props; return WrappedComponent {...otherProps} /> } } } export default HocRemoveProp;
1
2
3
4
5
6
7
8
9
10增加 prop(用在shopList这种)
import React from 'react'; const HocAddProp = (WrappedComponent,uid) => class extends React.Component { render() { const newProps = { uid, }; return <WrappedComponent {...this.props} {...newProps} /> } } export default HocAddProp;
1
2
3
4
5
6
7
8
9
10
11
属性代理之 抽取状态
import React from 'react'; const HocContainer = (WrappedComponent) => class extends React.Component { constructor(props) { super(props) this.state = { name: '' } } onNameChange = (event) => { this.setState({ name: event.target.value }) } render() { const newProps = { name: { value: this.state.name, onChange: this.onNameChange } } return <WrappedComponent {...this.props} {...newProps} /> } } export default HocContainer;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25使用
@HocContainer class SampleComponent extends React.Component { render() { return <input name="name" {...this.props.name}/> } }
1
2
3
4
5
6属性代理之 包装组件
const HocStyleComponent = (WrappedComponent, style) => class extends React.Component { render() { return ( <div style={style}> <WrappedComponent {...this.props} {...newProps} /> </div> ) } }
1
2
3
4
5
6
7
8
9
10使用
import HocStyleComponent from './HocStyleComponent'; const colorSytle ={color:'#ff5555'} const newComponent = HocStyleComponent(SampleComponent, colorSytle);
1
2
3
属性代理的生命周期的过程类似于堆栈调用:
didmount 一> HOC didmount 一>(HOCs didmount) 一>(HOCs will unmount) 一>HOC will unmount一>unmount
# 反向继承 Inheritance Inversion(II)
const MyContainer = (WrappedComponent) =>
class extends WrappedComponent {
render() {
return super.render();
}
}
2
3
4
5
6
继承方式的生命周期的过程类似队列调用:
didmount 一> HOC didmount 一>(HOCs didmount) 一>will unmount一>HOC will unmount一> (HOCs will unmount)
# 反向继承之 渲染劫持
// 条件性渲染
const iiHOC = (WrappedComponent) =>
class Enhancer extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render()
} else {
return null
}
}
}
2
3
4
5
6
7
8
9
10
11
12
// 通过 render 来变成 React Elements tree 的结果
const HOCPropsComponent = (WrappedComponent) =>
class extends WrappedComponent {
render() {
const elementsTree = super.render();
let newProps = {
color: (elementsTree && elementsTree.type === 'div') ? '#fff' : '#ff5555'
};
const props = Object.assign({}, elementsTree.props, newProps)
const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children)
return newElementsTree
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 命名
function getDisplayName(component) {
return component.displayName || component.name || 'Component'
}
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
...
}
2
3
4
5
6
7
# 必须将静态方法做拷贝
// 定义静态方法
WrappedComponent.staticMethod = function() {/*...*/}
// 使用高阶组件
const EnhancedComponent = enhance(WrappedComponent);
// 增强型组件没有静态方法
typeof EnhancedComponent.staticMethod === 'undefined' // true
2
3
4
5
6
7
为解决这个问题,在返回之前,将原始组件的方法拷贝给容器:
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必须得知道要拷贝的方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
2
3
4
5
6
或者
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
2
3
4
5
6
# 场景举例
- 页面复用
- 页面鉴权
- 日志及性能打点
# redux的一般流程
# 首先,我们看下几个核心概念:
- Store:保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。
- State:Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。
- Action:State的变化,会导致View的变化。但是,用户接触不到State,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。
- Action Creator:View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。
- Reducer:Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。
- dispatch:是View发出Action的唯一方法。
# 然后我们过下整个工作流程:
- 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法。
- 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State
- State一旦有变化,Store就会调用监听函数,来更新View。
# react-redux是如何工作的:
- Provider: Provider的作用是从最外部封装了整个应用,并向connect模块传递store
- connect: 负责连接React和Redux
- 获取state: connect通过context获取Provider中的store,通过store.getState()获取整个store tree 上所有state
- 包装原组件: 将state和action通过props的方式传入到原组件内部wrapWithConnect返回一个ReactComponent对象Connect,Connect重新render外部传入的原组件WrappedComponent,并把connect中传入的mapStateToProps, mapDispatchToProps与组件上原有的props合并后,通过属性的方式传给WrappedComponent
- 监听store tree变化: connect缓存了store tree中state的状态,通过当前state状态和变更前state状态进行比较,从而确定是否调用
this.setState()
方法触发Connect及其子组件的重新渲染
# redux异步中间件之间的优劣:
redux-thunk优点:
- 体积小: redux-thunk的实现方式很简单,只有不到20行代码
- 使用简单: redux-thunk没有引入像redux-saga或者redux-observable额外的范式,上手简单
redux-thunk缺陷:
- 样板代码过多: 与redux本身一样,通常一个请求需要大量的代码,而且很多都是重复性质的
- 耦合严重: 异步操作与redux的action偶合在一起,不方便管理
- 功能孱弱: 有一些实际开发中常用的功能需要自己进行封装
redux-saga优点:
- 异步解耦: 异步操作被被转移到单独 saga.js 中,不再是掺杂在 action.js 或 component.js 中
- action摆脱thunk function: dispatch 的参数依然是一个纯粹的 action (FSA),而不是充满 “黑魔法” thunk function
- 异常处理: 受益于 generator function 的 saga 实现,代码异常/请求失败 都可以直接通过 try/catch 语法直接捕获处理
- 功能强大: redux-saga提供了大量的Saga 辅助函数和Effect 创建器供开发者使用,开发者无须封装或者简单封装即可使用
- 灵活: redux-saga可以将多个Saga可以串行/并行组合起来,形成一个非常实用的异步flow
- 易测试,提供了各种case的测试方案,包括mock task,分支覆盖等等
redux-saga缺陷:
- 额外的学习成本: redux-saga不仅在使用难以理解的 generator function,而且有数十个API,学习成本远超redux-thunk,最重要的是你的额外学习成本是只服务于这个库的,与redux-observable不同,redux-observable虽然也有额外学习成本但是背后是rxjs和一整套思想
- 体积庞大: 体积略大,代码近2000行,min版25KB左右
- 功能过剩: 实际上并发控制等功能很难用到,但是我们依然需要引入这些代码
- ts支持不友好: yield无法返回TS类型
redux-observable优点:
- 功能最强: 由于背靠rxjs这个强大的响应式编程的库,借助rxjs的操作符,你可以几乎做任何你能想到的异步处理
- 背靠rxjs: 由于有rxjs的加持,如果你已经学习了rxjs,redux-observable的学习成本并不高,而且随着rxjs的升级redux-observable也会变得更强大
redux-observable缺陷:
- 学习成本奇高: 如果你不会rxjs,则需要额外学习两个复杂的库
- 社区一般: redux-observable的下载量只有redux-saga的1/5,社区也不够活跃,在复杂异步流中间件这个层面redux-saga仍处于领导地位
# react组件的优化
# 优化方法
- shouldComponentUpdate
- pureComponent
- React.memo
# 将子节点渲染到存在于父组件以外的 DOM 节点
- ReactDOM.createPortal
# --- Vue ---
# virtual dom
用js来模拟DOM中的节点。
Virtual DOM 其实就是一个简单的 JS 对象,最少包含标签名(tag)、属性(attrs)和子元素对象(children)三个属性来描述节点,实际上它只是一层对真实 DOM 的抽象。
# 优点:
- 具备跨平台的优势
- 操作 DOM 慢,js 运行效率高。我们可以将 DOM 对比操作放在 JS 层,提高效率
- 提升渲染性能
# 详解vue的diff算法
# key 属性的作用和重要性
为了在数据变化时强制更新组件,以避免“原地复用”带来的副作用。
- 在渲染元素列表时,默认采用就地复用策略(无 key 情况),性能更好,因为不需要创建和销毁 vnode,不需要在 dom 中添加移除节点。
- 而 key 的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。
- 但是一般列表组件都有自己的状态。例如:一个新闻列表,可点击列表项来将其标记为"已访问",可通过 tab 切换“娱乐新闻”或是“社会新闻”。无 key 则复用会保留之前状态而出错,带 key 则替换组件拥有正确状态。
- 所以 key 的作用主要是为了高效的更新虚拟 DOM,并且保证组件状态正确。
# 双向绑定的原理是什么
- Vue 的双向数据绑定是由 数据劫持 结合 发布者订阅 实现的
- 数据劫持是通过
Object.defineProperty()
来劫持对象数据的 setter 和 getter 操作 - 发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行
- 原理: 通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化-->视图更新;视图交互变化-->数据变更的双向绑定效果。
- 通过Object.defineProperty的get和set进行数据劫持
- 通过遍历data数据进行数据代理到this上
- 通过{{}}对数据进行编译
- 通过发布订阅模式实现数据与视图同步
# 指令
指令 | 作用 |
---|---|
v-html | 输出真正的 HTML |
v-text | 将数据解析为纯文本 |
v-if / v-else | 条件渲染 |
v-show | 切换 display: none |
v-for | 列表渲染 |
v-bind / : | 属性绑定 |
v-on / @ | 事件绑定 |
# 自定义指令
// 全局定义
Vue.directive("change-color",function(el,binding,vnode){
el.style["color"]= binding.value;
})
// 使用
<template>
<div v-change-color=“color”>{{message}}</div>
</template>
<script>
export default{
data(){
return{
color:'green'
}
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 生命周期
Vue 实例从 开始创建、初始化数据、编译模板、挂载DOM-渲染、更新-渲染、卸载等一系列的过程。
名称 | 解释 |
---|---|
beforeCreate | 未初始化 |
created | 可访问数据和方法 |
beforeMount | 未挂载,仍为虚拟 dom 节点 |
mounted | 挂载到真实 dom 上,可进行 dom 操作 |
beforeUpdate | 更新前 |
updated | 更新 dom 后 |
beforeDestroy | 移除监听,定时器、事件等 |
destroyed | 实例销毁 |
activated | 被 keep-alive 缓存的组件激活时调用,钩子触发的顺序是created->mounted->activated |
deactivated | 被 keep-alive 缓存的组件停用时调用。 |
beforeCreate:创建前,此阶段为实例初始化之后,this指向创建的实例,此时的数据观察事件机制都未形成,不能获得DOM节点。
data,computed,watch,methods 上的方法和数据均不能访问。
可以在这加个loading事件。
created:创建后,此阶段为实例已经创建,完成数据(data、props、computed)的初始化导入依赖项。
可访问 data computed watch methods 上的方法和数据。
初始化完成时的事件写在这里,异步请求也适宜在这里调用(请求不宜过多,避免白屏时间太长)。
可以在这里结束loading事件,还做一些初始化,实现函数自执行。
未挂载DOM,若在此阶段进行DOM操作一定要放在Vue.nextTick()的回调函数中。
beforeMount:挂载前,虽然得不到具体的DOM元素,但vue挂载的根节点已经创建,下面vue对DOM的操作将围绕这个根元素继续进行。
beforeMount这个阶段是过渡性的,一般一个项目只能用到一两次。
mounted:挂载,完成创建vm.$el,和双向绑定
完成挂载DOM和渲染,可在mounted钩子函数中对挂载的DOM进行操作。
可在这发起后端请求,拿回数据,配合路由钩子做一些事情。
beforeUpdate:数据更新前,数据驱动DOM。
在数据更新后虽然没有立即更新数据,但是DOM中的数据会改变,这是vue双向数据绑定的作用。
可在更新前访问现有的DOM,如手动移出添加的事件监听器。
updated:数据更新后,完成虚拟DOM的重新渲染和打补丁。
组件DOM已完成更新,可执行依赖的DOM操作。
注意:不要在此函数中操作数据(修改属性),会陷入死循环。
activated:在使用vue-router时有时需要使用
<keep-alive></keep-alive>
来缓存组件状态,这个时候created钩子就不会被重复调用了。如果我们的子组件需要在每次加载的时候进行某些操作,可以使用activated钩子触发。
deactivated:
<keep-alive></keep-alive>
组件被移除时使用。beforeDestroy:销毁前,
可做一些删除提示,如:您确定删除xx吗?
destroyed:销毁后,当前组件已被删除,销毁监听事件,组件、事件、子实例也被销毁。
这时组件已经没有了,无法操作里面的任何东西了。
# 父子组件的生命周期
执行顺序:
父组件开始执行到beforeMount 然后开始子组件执行,最后是父组件mounted。
如果有兄弟组件,父组件开始执行到beforeMount,然后兄弟组件依次执行到beforeMount,然后按照顺序执行mounted,最后执行父组件的mounted。
// 首次加载 父: beforeCreate 父: created 父: beforeMount --子1: beforeCreate --子1: created --子1: beforeMount ----子2: beforeCreate ----子2: created ----子2: beforeMount --子1: mounted ----子2: mounted 父: mounted
1
2
3
4
5
6
7
8
9
10
11
12
13
父子组件在data变化中是分别监控的,但是更新props中的数据是关联的。
// 父组件更新传到子组件的props 父: beforeUpdate --子1: beforeUpdate ----子2: beforeUpdate ----子2: updated --子1: updated 父: updated
1
2
3
4
5
6
7销毁父组件时,先将子组件销毁后才会销毁父组件。
// 卸载父组件 父: beforeDestroy --子1: beforeDestroy --子1: destroyed ----子2: beforeDestroy ----子2: destroyed 父: destroyed
1
2
3
4
5
6
7兄弟组件的初始化(mounted之前)是分开进行,挂载是从上到下依次进行
当没有数据关联时,兄弟组件之间的更新和销毁是互不关联的
# 声明周期的简单梳理
_init_
initLifecycle/Event
,往vm
上挂载各种属性beforeCreate
: 实例刚创建initInjection/initState
: 初始化注入和 data 响应性created
: 创建完成,属性已经绑定, 但还未生成真实dom
- 进行元素的挂载:
$el / vm.$mount()
- 是否有
template
: 解析成render function
*.vue
文件:vue-loader
会将<template>
编译成render function
beforeMount
: 模板编译/挂载之前- 执行
render function
,生成真实的dom
,并替换到dom tree
中 mounted
: 组件已挂载
update
:- 执行
diff
算法,比对改变是否需要触发UI更新 flushScheduleQueue
watcher.before
: 触发beforeUpdate
钩子 -watcher.run()
: 执行watcher
中的notify
,通知所有依赖项更新UI
- 触发
updated
钩子: 组件已更新
- 执行
actived / deactivated(keep-alive)
: 不销毁,缓存,组件激活与失活destroy
:beforeDestroy
: 销毁开始- 销毁自身且递归销毁子组件以及事件监听
remove()
: 删除节点watcher.teardown()
: 清空依赖vm.$off()
: 解绑监听
destroyed
: 完成后触发钩子
# 代码形式展示vue
的初始化
new Vue({})
// 初始化Vue实例
function _init() {
// 挂载属性
initLifeCycle(vm)
// 初始化事件系统,钩子函数等
initEvent(vm)
// 编译slot、vnode
initRender(vm)
// 触发钩子
callHook(vm, 'beforeCreate')
// 添加inject功能
initInjection(vm)
// 完成数据响应性 props/data/watch/computed/methods
initState(vm)
// 添加 provide 功能
initProvide(vm)
// 触发钩子
callHook(vm, 'created')
// 挂载节点
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
// 挂载节点实现
function mountComponent(vm) {
// 获取 render function
if (!this.options.render) {
// template to render
// Vue.compile = compileToFunctions
let { render } = compileToFunctions()
this.options.render = render
}
// 触发钩子
callHook('beforeMounte')
// 初始化观察者
// render 渲染 vdom,
vdom = vm.render()
// update: 根据 diff 出的 patchs 挂载成真实的 dom
vm._update(vdom)
// 触发钩子
callHook(vm, 'mounted')
}
// 更新节点实现
funtion queueWatcher(watcher) {
nextTick(flushScheduleQueue)
}
// 清空队列
function flushScheduleQueue() {
// 遍历队列中所有修改
for(){
// beforeUpdate
watcher.before()
// 依赖局部更新节点
watcher.update()
callHook('updated')
}
}
// 销毁实例实现
Vue.prototype.$destory = function() {
// 触发钩子
callHook(vm, 'beforeDestory')
// 自身及子节点
remove()
// 删除依赖
watcher.teardown()
// 删除监听
vm.$off()
// 触发钩子
callHook(vm, 'destoryed')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 组件通信
- props / $emit
- $children / $parent
- provide/ inject
- ref / refs
- eventBus
- Vuex
- localStorage / sessionStorage
- $attrs与 $listeners
# 总结
父子组件通信:
props
;$parent
/$children
;provide
/inject
;ref
;$attrs
/$listeners
兄弟组件通信:
eventBus
; Vuex跨级通信:
eventBus
;Vuex;provide
/inject
、$attrs
/$listeners
# vuex 流程及核心概念
# 流程
- 页面通过 mapAction 异步提交事件到 action
- action 通过 commit 把对应参数同步提交到 mutation
- mutation 会修改 state 中对应的值
- 最后通过 getter 把对应值抛出去,在页面的计算属性中通过 mapGetter 来动态获取 state 中的值
# 核心概念
- state: 保存着共有数据,数据是响应式的
- getter: 可以对 state 进行计算操作,主要用来过滤一些数据,可以在多组件之间复用
- mutations: 动态修改 state 中的数据,通过 commit 提交方法,方法必须是同步的
- actions: 通过 commit 提交 mutations,进行修改数据,可以包含任意异步操作;在组件 methods 中 mapActions 分发 action
- modules: 模块化 vuex
# nextTick
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
在vue中使用了三种情况来延迟调用该函数,
- 首先我们会判断我们的设备是否支持Promise对象,如果支持的话,会使用 Promise.then 来做延迟调用函数。
- 如果设备不支持Promise对象,再判断是否支持 MutationObserver 对象,如果支持该对象,就使用MutationObserver来做延迟,
- 最后如果上面两种都不支持的话,会使用
setTimeout(() => {}, 0);
setTimeout 来做延迟操作。
# 使用场景
- 在created生命周期中进行DOM操作
- 更改数据后,进行节点DOM操作
# 响应式的数据for循环改变了1000次为什么视图只更新了一次
Vue 异步执行 DOM 更新
只要观察到数据变化,Vue 将开启一个异步队列,并缓冲在同一事件循环中发生的所有数据改变
如果同一个 watcher 被多次触发,只会被推入到队列中一次
然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际工作
Vue 在内部尝试对异步队列使用原生的
Promise.then
和MessageChannel
,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替
# watch
如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化
- 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察
props
$emit
或者本组件的值,当数据变化时来执行回调进行后续操作 - 无缓存性,页面重新渲染时值不变化也会执行
# watch 何时初始化
function Vue() {
// ... 其他处理
initState(this);
// ...解析模板,生成DOM 插入页面
}
function initState(vm) {
// ...处理 data,props,computed 等数据
if (opts.watch) {
initWatch(this, vm.$options.watch);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# initWatch
function initWatch(vm, watch) {
for (var key in watch) {
var handler = watch[key];
createWatcher(vm, key, handler);
}
}
function createWatcher(vm, expOrFn, handler, options) {
// expOrFn 是 key,handler 可能是对象
// 监听属性的值是一个对象,包含 handler,deep,immediate
if (typeof handler === "object") {
options = handler;
handler = handler.handler;
}
// 回调函数是一个字符串,从 vm 获取
if (typeof handler === "string") {
handler = vm[handler];
}
// expOrFn 是 key,options 是 watch 的全部选项
vm.$watch(expOrFn, handler, options);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
initWatch 做了什麼?
遍历 watch
获取到监听回调
// 传入的 watch 配置可能是这三种 watch: { name: { handler() {} }, name() {}, name: "getname" };
1
2
3
4
5
6
7
8- 如果配置是个对象,就取handler 字段
- 如果配置是函数,那么直接就是 监听回调
- 如果配置是字符串,从实例上获取函数
调用 vm.$watch
$watch()本质还是创建一个Watcher实例对象
Vue.prototype.$watch = function(expOrFn, cb, opts) { // expOrFn 是 监听的 key,cb 是监听的回调,opts 是 监听的所有选项 var watcher = new Watcher(this, expOrFn, cb, opts); // 设定了立即执行,所以马上执行回调 if (opts.immediate) { cb.call(this, watcher.value); }
1
2
3
4
5
6
7
8
9
};
- 判断是否立即执行监听回
- 如果你设置了 immediate 的话,表示不用等我数据变化,初始化时马上执行一遍,执行的代码就是直接调用 回调,绑定上下文,传入监听值
- 每个 watch 配发 watcher
```js
var Watcher = function(vm, key, cb, opt) {
this.vm = vm;
this.deep = opt.deep;
this.cb = cb;
// 这里省略处理 xx.xx.xx 这种较复杂的key
this.getter = function(obj) {
return obj[key];
};
// this.get 作用就是执行 this.getter函数
this.value = this.get();
};
```
1. **怎么对设置的 key 进行监听?**
我们要先对 Watch 中的 this.getter 的函数进行理解,他的本质是为了获取对象的key值
然后 getter 是在 watcher.get 中执行的,看下 get 源码
```js
Watcher.prototype.get = function() {
// 精简
var value = this.getter(this.vm);
return value;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
你能看到,Watch 在结尾会立即执行一次 watcher.get,其中便会执行 getter,便会根据你监听的 key,去实例上读取并返回,存放在 watcher.value 上。看到了吗,从实例上读取属性,这句话。
首先,watch 初始化之前,data 应该初始化完毕了,每个 data 数据都已经是响应式的
使用例子来说明一下
data() {
return {
name: 111
};
},
watch: {
name() {}
}
// 当 watch.getter 执行,而读取了 vm.name 的时候,name 的依赖收集器就会收集到 watch-watcher
// 于是 name 变化的时候,会可以通知到 watch,监听就成功了
2
3
4
5
6
7
8
9
10
11
如何进行深度监听?
watch: { name: { deep: true, handler() {} } }
1
2
3
4
5
6首先,深度监听,是你设置了 deep 的时候,deep 会保存在watcher 中,以便后用。
上一问题说过,在 新建 watcher 的时候,会马上执行一个 get,上个问题的 get 源码简化很多,把 处理深度监听的部分去掉了,这里露出来了
Watcher.prototype.get = function() { Dep.target = this; var value = this.getter(this.vm); // 没错,处理深度监听只有一条语句! if (this.deep) traverse(value); Dep.target = null; return value; };
1
2
3
4
5
6
7
8
9
10
11traverse 英文 遍历 的意思
function traverse(val) { var i, keys; // 数组逐个遍历 if (Array.isArray(val)) { i = val.length; // val[i] 就是读取值了,然后值的对象就能收集到 watch-watcher while (i--) { traverse(val[i]); } } else { keys = Object.keys(val); i = keys.length; // val[keys[i]] 就是读取值了,然后值的对象就能收集到 watch-watcher while (i--) { traverse(val[keys[i]]); } } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21他的想法是这样的:不断递归深入读取对象
因为读取,就可以让这个属性收集到 watch-watcher 的原则。就算是深层级的对象,其中的每个属性也都是响应式的,每个属性都有自己的依赖收集器。通过不断深入的读取每个属性,这样每个属性就都可以收集到 watch-watcher 了。这样不管对象内多深的属性变化,都会通知到 watch-watcher。于是这样就完成了深度监听
监听值变化,如何触发监听函数?
监听的数据变化的时候,就能通知 watch-watcher 更新,所谓通知更新,就是手动调用 watch.update。就是读取一遍值,然后保存新值,接着 调用 监听回调,并传入新值和旧值
Watcher.prototype.update = function() { var value = this.get(); if (this.deep) { var oldValue = this.value; this.value = value; // cb 是监听回调 this.cb.call(this.vm, value, oldValue); } };
1
2
3
4
5
6
7
8
9
10
watch实现过程:
- watch的初始化在data初始化之后(此时的data已经通过
Object.defineProperty
的设置成响应式) - watch的key会在Watcher里进行值的读取,也就是立马执行get获取value(从而实现data对应的key执行getter实现对于watch的依赖收集),此时如果有
immediate
属性那么立马执行watch对应的回调函数 - 当data对应的key发生变化时,触发user watch实现watch回调函数的执行
# computed
当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
computed
是计算属性,也就是计算值,它更多用于计算值的场景computed
具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计computed
适用于计算比较消耗性能的计算场景
# computed运行原理
- computed的属性是动态挂载到vm实例上的,和普通的响应式数据在data里声明不同
- 设置computed的getter,如果执行了computed对应的函数,由于函数会读取data属性值,因此又会触发data属性值的getter函数,在这个执行过程中就可以处理computed相对于data的依赖收集关系了
- 首次计算computed的值时,会执行vm.computed属性对应的getter函数(用户指定的computed函数,如果没有设置getter,那么将当前指定的函数赋值computed属性的getter),进行上述的依赖收集
- 如果computed的属性值又依赖了其他computed计算属性值,那么会将当前target暂存到栈中,先进行其他computed计算属性值的依赖收集,等其他计算属性依赖收集完成后,在从栈中pop出来,继续进行当前computed的依赖收集
var vm = new Vue({
el: '#demo',
data: {
firstName: 'Foo',
lastName: 'Bar'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
2
3
4
5
6
7
8
9
10
11
12
由于 this.firstName
和 this.lastName
(上面是Vue官方示例)都是响应式变量,因此会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候Dep.target
就是这个 computed watcher,具体步骤如下:
- data 属性初始化 getter setter
- computed 计算属性初始化,提供的函数将用作属性
vm.fullName
的 getter - 当首次获取
fullName
计算属性的值时,Dep 开始依赖收集 - 在执行 message getter 方法时,如果 Dep 处于依赖收集状态,则判定
firstName
和lastName
为fullName
的依赖,并建立依赖关系 - 当
firstName
或lastName
发生变化时,根据依赖关系,触发fullName
的重新计算 - 如果计算值没有发生变化,不会触发视图更新
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。
# computed实现
从两个问题出发:
- 建立与其他属性(如:data、 Store)的联系;
- 属性改变后,通知计算属性重新计算。
实现时,主要如下
- 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter。
- 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。
- Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。
- 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集
# keep-alive
keep-alive是个抽象组件(功能型组件),不会被渲染在DOM树中。它的作用是在内存中缓存组件(不让组件销毁),等到下次再渲染的时候,还会保持其中的所有状态,并且会触发activated钩子函数。
它提供了include与exclude两个属性,允许组件有条件地进行缓存;max 缓存的组件实例数量上限。
# 监听子组件的生命周期
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,常规的写法可能如下:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
2
3
4
5
6
7
此外,还有一种特别简单的方式,子组件不需要任何处理,只需要在父组件引用的时候通过@hook 来监听即可,代码如下:
<Child @hook:mounted="doSomething" />
<Child @hook:updated="doSomething" />
2
当然这里不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以。
# 路由参数变化组件不更新
同一path的页面跳转时路由参数变化,但是组件没有对应的更新。
原因:主要是因为获取参数写在了created或者mounted路由钩子函数中,路由参数变化的时候,这个生命周期不会重新执行。
解决方案1:watch监听路由
watch: {
// 方法1 //监听路由是否变化
'$route' (to, from) {
if(to.query.id !== from.query.id){
this.id = to.query.id;
this.init();//重新加载数据
}
}
}
//方法 2 设置路径变化时的处理函数
watch: {
'$route': {
handler: 'init',
immediate: true
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
解决方案2 :为了实现这样的效果可以给router-view添加一个不同的key,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。
<router-view :key="$route.fullpath"></router-view>
# mixin
Mixins 使我们能够为 Vue 组件编写可插拔和可重用的功能。
如果你希望在多个组件之间重用一组组件选项,例如生命周期 hook、方法等,则可以将其编写为 mixin,并在组件中简单地引用它。然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优先于组件自己的 hook 。
缺点:命名冲突、隐式依赖
# Proxy 相比于 defineProperty 的优势
Proxy的优势如下:
- Proxy可以直接监听对象而非属性, 不需要深度遍历监听
- Proxy可以直接监听数组的变化
- Proxy有多达13种拦截方法, 不限于apply、ownKeys、deleteProperty、has等
- Proxy返回的是一个新对象, 我们可以只操作新的对象达到目的, 而
Object.defineProperty
只能遍历对象属性直接修改 - Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
Object.defineProperty的优势如下:
- 兼容性好,支持IE9