Vue
内部流程图
虚拟DOM和Diff算法
- Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。
- vnode 有几个重要的属性
tag
属性即这个vnode
的标签属性data
属性包含了最后渲染成真实dom
节点后,节点上的class
,attribute
,style
以及绑定的事件children
属性是vnode
的子节点text
属性是文本属性elm
属性为这个vnode
对应的真实dom
节点key
属性是vnode
的标记,在diff
过程中可以提高diff
的效率,就地复用
- Diff算法:主要体现在 patchVnode函数 。将新产生的 VNode 节点与老 VNode 进行一个 patch 的过程,比对得出「差异」,最终将这些「差异」更新到视图上。
- 由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。(依赖一层适配层,将不同平台的 API 封装在内,以同样的接口对外提供。)
- diff 算法(虚拟DOM:h函数,patch函数)
- 逐层、同层比对,一层不匹配,就不再比对,直接替换
- key值可以提升虚拟DOM比对的性能(就地复用 —— 复用的是没有发生改变的元素,其他的还要依次重排 )
- initial render 和 updates(初始渲染和更新)
1 | Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { |
- sameVnode( patchVnode在符合 sameVnode 的条件下触发的,所以会进行「比对」)
1 | /* sameVnode会对传入的2个vnode进行基本属性的比较,只有当基本属性相同的情况下才认为这个2个vnode只是局部发生了更新,然后才会对这2个vnode进行diff,如果2个vnode的基本属性存在不一致的情况,那么就会直接跳过diff的过程,进而依据vnode新建一个真实的dom,同时删除老的dom节点。*/ |
patchVnode ( diff 算法的体现 )
- 接收两个参数 oldVnode和 vnode
- 实现
- 第一种情况是新老 VNode 节点相同的情况下,就不需要做任何改变了,直接 return
- 第一种情况是新老 VNode 节点都是 isStatic (静态的),并且 key 相同时,只要将老节点的 componentInstance 与 elm 赋值给新节点即可(模板编译的optimize阶段会标记静态结点,优化了patch的性能)
- 第三种情况是当新 VNode 节点是非文本节点的时候,oldCh 与 ch 都存在且不相同时,使用 updateChildren 函数来更新子节点 ( 比对vnode下面的子节点 )
updateChildren
- 头头类型相同、尾尾类型相同的节点
- 头尾类型相同的节点
- 新增的节点
- 删除的节点
- 更新节点(位置移动)
实现虚拟DOM
DOM-diff过程
- 用JS对象模拟DOM(虚拟DOM)
- 把此虚拟DOM转成真实DOM并插入页面中(render)
- 如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff)
- 把差异对象应用到真正的DOM树上(patch)
创建虚拟DOM
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//elment.js
//vnode类
class ELement{
constructor(type,props,children){//type--->tag,props--->data,children--->children
this.type=type;
this.props=props;
this.children=children;
}
}
//创建虚拟DOM,返回vnode
function createElement(type,props,children){
return new Element(type,props,children);
}
export {
Element,
createElement
}渲染虚拟DOM
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
26
27
28
29
30
31//element.js
class Element{ ... }
function createElement(){ ... }
//render方法可以将虚拟DOM转化成真实DOM
function render(domObj) {
// 根据type类型来创建对应的元素
let el = document.createElement(domObj.type);
// 再去遍历props属性对象,然后给创建的元素el设置属性
for (let key in domObj.props) {
// 设置属性的方法
setAttr(el, key, domObj.props[key]);
}
// 遍历子节点
// 如果是虚拟DOM,就继续递归渲染
// 不是就代表是文本节点,直接创建
domObj.children.forEach(child => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child);
// 添加到对应元素内
el.appendChild(child);
});
return el;
}
//设置属性
function setAttr(node,key,value){ ... };
//将元素插入页面
function renderDom(el,target){
target.appendChild(el);
}DOM-diff
- 比较规则
- 新的DOM节点不存在{type: ‘REMOVE’, index}
- 文本的变化{type: ‘TEXT’, text: 1}
- 当节点类型相同时,去看一下属性是否相同,产生一个属性的补丁包{type: ‘ATTR’, attr: {class: ‘list-group’}}
- 节点类型不相同,直接采用替换模式{type: ‘REPLACE’, newNode}
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48//diff.js
function diff(oldTree,newTree){
//声明变量patches用来存放补丁对象
let patches={};
let index=0;
//递归树,比较后的结果放到补丁里
walk(oldTree,newTree,index,patches);
return patches;
}
function walk(oldNode, newNode, index, patches) {
// 每个元素都有一个补丁
let current = [];
if (!newNode) { // rule1
current.push({ type: 'REMOVE', index });
} else if (isString(oldNode) && isString(newNode)) {
// 判断文本是否一致
if (oldNode !== newNode) {
current.push({ type: 'TEXT', text: newNode });
}
} else if (oldNode.type === newNode.type) {
// 比较属性是否有更改
let attr = diffAttr(oldNode.props, newNode.props);
if (Object.keys(attr).length > 0) {
current.push({ type: 'ATTR', attr });
}
// 如果有子节点,遍历子节点
diffChildren(oldNode.children, newNode.children, patches);
} else { // 说明节点被替换了
current.push({ type: 'REPLACE', newNode});
}
// 当前元素确实有补丁存在
if (current.length) {
// 将元素和补丁对应起来,放到大补丁包中
patches[index] = current;
}
}
function isString(obj) {
return typeof obj === 'string';
}
function diffAttr(oldAttrs, newAttrs) { ... }
// 所有都基于一个序号来实现
let num = 0;
function diffChildren(oldChildren, newChildren, patches) { ... }- 比较规则
patch补丁更新:打补丁需要传入两个参数,一个是要打补丁的元素(真实dom),另一个就是所要打的补丁
步骤
- 用一个变量来得到传递过来的所有补丁allPatches
patch方法接收两个参数(node, patches) (在方法内部调用walk方法,给某个元素打上补丁)
walk方法里获取所有的子节点
- 给子节点也进行先序深度优先遍历,递归walk
- 如果当前的补丁是存在的,那么就对其打补丁(doPatch)
doPatch打补丁方法会根据传递的patches进行遍历
判断补丁的类型来进行不同的操作
属性ATTR for in去遍历attrs对象,当前的key值如果存在,就直接设置属性setAttr; 如果不存在对应的key值那就直接删除这个key键的属性
文字TEXT 直接将补丁的text赋值给node节点的textContent即可
替换REPLACE 新节点替换老节点,需要先判断新节点是不是Element的实例,是的话调用render方法渲染新节点;
不是的话就表明新节点是个文本节点,直接创建一个文本节点就OK了。
之后再通过调用父级parentNode的replaceChild方法替换为新的节点
删除REMOVE 直接调用父级的removeChild方法删除该节点
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
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//patch.js
import { Element, render, setAttr } from './element';
let allPatches;
let index = 0; // 默认哪个需要打补丁
function patch(node, patches) {
allPatches = patches;
// 给某个元素打补丁
walk(node);
}
function walk(node) {
let current = allPatches[index++];
let childNodes = node.childNodes;
// 先序深度,继续遍历递归子节点
childNodes.forEach(child => walk(child));
if (current) {
doPatch(node, current); // 打上补丁
}
}
function doPatch(node, patches) {
// 遍历所有打过的补丁
patches.forEach(patch => {
switch (patch.type) {
case 'ATTR':
for (let key in patch.attr) {
let value = patch.attr[key];
if (value) {
setAttr(node, key, value);
} else {
node.removeAttribute(key);
}
}
break;
case 'TEXT':
node.textContent = patch.text;
break;
case 'REPLACE':
let newNode = patch.newNode;
newNode = (newNode instanceof Element) ? render(newNode) : document.createTextNode(newNode);
node.parentNode.replaceChild(newNode, node);
break;
case 'REMOVE':
node.parentNode.removeChild(node);
break;
default:
break;
}
});
}
export default patch;
编译(模板如何被解析的)
三个阶段
parse(分析)
- parse 会用正则等方式解析 template 模板中的指令、class、style 等数据,形成 AST
optimize(优化)
- 主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。
generate(生成)
- generate 是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串
1 | function isStatic (node) { |
响应式
- 「 依赖收集」的过程就是把 Subscriber 实例存放到对应的 Publisher 对象中去。 get 方法可以让当前的 Subscriber 对象( Publisher.target )存放到它的 subs 中( addSub )方法,在数据变化时, set 会调用 Publisher对象的 notify 方法通知它内部所有的 Subscriber 对象进行视图更新。
( get 进行「依赖收集」。 set 通过发布者通知订阅者进行更新。更新操作会触发patch函数,接收 vnode做参数。patch函数用到diff算法 )
- 「 依赖收集」的前提条件还有两个:
- 1.触发 get 方法;( render function 进行渲染,那么其中的依赖的对象都会被「读取」)
- 2.新建一个 Subscriber 对象。( 在 Vue 的构造类中处理 ,data字段)
- Object.defineProperty 和发布订阅模式
- Proxy和Reflect
- 实现
- 数据劫持结合发布—订阅模式(publisher 发布者、event center 事件中心、subscriber 订阅者)
- Object.defineProperty (vue 3.0 proxy)
- 将data的属性代理到vm上
1 | //Publisher 依赖收集,收集事件有哪些订阅者,同时事件更新时通知更新 |
Vuex
使用 State 和 Getter 对状态进行定义;使用 Mutation 和 Action 对状态进行变更;引入 Module 对状态进行模块化分割
- state 用来数据共享数据存储
- getters 用来对共享数据进行过滤操作
- mutation 用来注册改变数据状态
- action 解决异步改变共享数据
vuex 使用过程
1 | 1.Vue的插件机制,安装Vuex。 Vue.use(Vuex) |
Vuex的store是如何注入到组件中的?
- 1.利用Vue插件机制装载vuex
- 2.install Vuex.store()
- 3.利用Vue.mixin() 在Vue生命周期beforeCreate之前进行VuexInit (得益于 mixin 机制,this 将指向 Vue 组件实例。我们可以在 Vue 组件实例上获得 Vuex 的 store 对象的引用 $store)
总结:Vuex 利用了 Vue 的 mixin 机制,混合 beforeCreate 钩子,将 store 注入至 Vue 组件实例上,并注册了 Vuex store 的引用属性 $store。
- Vuex 的state 和 getter 是如何映射到各个组件实例中自动更新的呢?
- 1.state 是借助 Vue 的响应式 data 实现的。
- 2.getter 是借助 Vue 的计算属性 computed 特性实现的。
- Vuex 核心代码
1 | // src/store.js |
将我们传入的state作为一个隐藏的vue组件的data,也就是说,我们的commit操作,本质上其实是修改这个组件的data值。vuex中的store本质就是没有
template
的隐藏着的vue组件
使用场景
- 多个视图依赖于同一状态。
- 有A,B,C,D,E,F组件…都需要同一个数据,如果每次都去请求一下接口获取数据,那么每多一次请求就会对服务器多一份负担。这个时候就可以利用vuex,只访问接口一次,获取到这个数据,然后存到这个”全局变量“里,用的时候直接取,不管有多少组件需要,都可以直接拿来用
- 来自不同视图的行为需要变更同一状态。
- 1、在首页、分类、商品详情页添加商品需要触发一次
- 2、在购物车进入编辑状态,删除购物车项,需要触发一次
- 多个视图依赖于同一状态。
缺点:刷新页面后数据消失
原因
- js代码是运行在内存(栈内存和堆内存)中的,代码运行时的所有变量、函数也都是保存在内存中的。刷新页面,以前申请的内存被释放,重新加载脚本代码,变量重新赋值。
问题场景
- 用户已经登录,登录状态放到state中,一刷新页面,还要重新登录。
- 购物车里的添加的数据,一刷新要重新添加。
解决方案
- 利用webStorage
跨层级组件通信(provide + inject)
- 应用场景:删除一条数据或者新增数据之后需要重新刷新当前页面
为什么选择这个方案去实现
1.用vue-router重新路由到当前页面,页面是不进行刷新的
1
2
3
4this.$router.replace({
path:...,
name:...
});2.采用window.reload(),或者router.go(0)刷新时,整个浏览器进行了重新加载,闪烁,体验不好
具体实现
provide + inject 组合
- 作用:允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
App.vue
声明reload方法,控制router-view的显示或隐藏,从而控制页面的再次加载
1
2
3
4
5
6
7
8
9
10
11
12
13
141. <router-view v-if="isRouterAlive"/>
2. provide(){
return {
reload:this.reload
}
}
3. methods:{
reload(){
this.isRouterAlive=false;
this.$nextTick(()=>{
this.isRouterAlive=true;
})
}
}- 在页面注入App.vue组件提供(provide)的 reload 依赖,在逻辑完成之后(删除或添加…),直接this.reload()调用,即可刷新当前页面。
1
2
3
4
5
6
7
8//刷新项目信息
freshPro(){
if(this.curProPage){
this.getPageData(this.curProPage);
}else{
this.reload();
}
}
React
Redux
- 基本做法:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。
- Store
- 保存数据的地方,可以看成是一个容器,整个应用只能有一个 Store。
- Redux 提供
createStore
这个函数,用来生成 Store
1 | import { createStore } from 'redux'; |
- State
Store
对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。- Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
- Action
- State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 是 View 发出的通知,表示 State 要发生变化了。
- Action 是一个对象。其中的
type
属性是必须的,表示 Action 的名称,其他属性可以自由设置 - Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store。
- Action Creator
- View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
- store.dispatch()
- View 发出 Action 的唯一方法,接受一个 Action 对象作为参数,将它发送出去。
- Reducer
- Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
store.dispatch
方法会触发 Reducer 的自动执行。- Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
- 为什么这个函数叫做 Reducer 呢?因为它可以作为数组的
reduce
方法的参数。
1 | const actions = [ |
Reducer 是一个纯函数,只要是同样的输入,必定得到同样的输出。
纯函数
定义(要求)
- 不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入参数
- 不会产生任何可观察的副作用,例如网络请求,输入和输出设备或数据突变(mutation)。
可观察的副作用
在函数内部与其外部的任意交互,可能是在函数内修改外部的变量,或者在函数里调用另外一个函数等。
副作用来自,但不限于:
进行一个 HTTP 请求
Mutating data
输出数据到屏幕或者控制台
DOM 查询/操作
Math.random()
获取的当前时间
Redux-thunk
中间件就是一个函数,对
store.dispatch
方法进行了改造,可以接受函数作为参数,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。
异步操作的思路
- 操作开始时,送出一个 Action,触发 State 更新为”正在操作”状态,View 重新渲染
- 操作结束后,再送出一个 Action,触发 State 更新为”操作结束”状态,View 再一次重新渲染
Vue VS React
生命周期
Vue 创建 = > 挂载 = > 更新 = > 销毁 (vue 中操作DOM是异步的)
- beforeCreate/created 创建阶段没有el选项
- beforeCreate el,data都未被初始化
- created 已经和data属性进行绑定,el属性还不存在
- beforeMount/mounted
- beforeMount el,data都被初始化,但还未渲染到视图中,还是虚拟dom的形式
- mounted el,data都被初始化,也被渲染到视图中,是真实dom的形式
- beforeUpdate/updated
- beforeUpdate data已经改变,但并未更新到视图中
- updated 视图已经更新
- beforeDestory/destroyed
- beforeDestroy 实例仍然完全可用
- destroyed Vue 实例指示的所有东西都会被解绑,所有的事件监听器会被移除,所有的子实例也会被销毁。
- beforeCreate/created 创建阶段没有el选项
React 生成期 => 存在期 => 销毁期
- 生成期 : constructor componentWillMount render componentDidMount
- 存在期: componentWillReceiveProps shouldComponentUpdate(nextProps, nextState) componentWillUpdate render ComponentDidUpdate
- 销毁期 componentWillUnmount
区别
生命周期
- Vue 四个时期
- React 三个时期 render 函数生成虚拟dom,使用diff算法 shouldComponentUpdate 性能优化
设计思想
- Vue 是响应式的设计思想,推崇数据可变,支持双向数据流。追求开发简单。
- React 是函数式的设计思想,推崇数据不可变,单向数据流。追求方式是否正确。
模板
Vue 采用基于HTML的模板语法
React采用JSX
Vuex VS Redux
Redux
- 三大原则:唯一数据源、状态只读、数据的改变只能通过纯函数(reducer)完成
- 核心三部分:store、reducer、action
- Redux的核心是store,它由Redux提供的 createStore(reducer, defaultState) 这个方法生成,生成三个方法,getState(),dispatch(),subscribe()。
- reducer是一个纯函数,它根据previousState和action计算出新的state。reducer(previousState,action)
- action本质上是一个JavaScript对象,其中必须包含一个type字段来表示将要执行的动作,其他的字段都可以根据需求来自定义。
React
:负责组件的UI界面渲染;Redux
:数据处理中心;React-Redux
:连接组件和数据中心,也就是把React和Redux联系起来。
React-Redux
- Redux 本身和 React 没有关系,只是数据处理中心,是React-Redux让他们联系在一起
- React-rRedux提供两个方法:connect和Provider。
- connect连接React组件和Redux store。connect实际上是一个高阶函数,返回一个新的已与 Redux store 连接的组件类。
1 | const VisibleTodoList = connect( |
- Provider实现store的全局访问,将store传给每个组件。
- 两者区别
- vuex的流向:view—>commit—>mutations—>state变化—>view变化(同步操作)
view—>dispatch—>actions—>commit—>mutations—>state变化—>view变化(异步操作) - redux的流向:view —>dispatch->actions —>reducer —>state变化 —>view变化(同步异步一样)
- vuex的流向:view—>commit—>mutations—>state变化—>view变化(同步操作)