vue2响应式

  • init数据初始化的时候,对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(这时候只会劫持已经存在的属性)。如果数据是数组类型, Vue2中是通过重写数组方法来实现。多层对象是通过递归来实现劫持的。
  • 在初始化流程中的编译阶段,当render function 被渲染的时候,会读取Vue实例中和视图相关的响应式数据,此时会触发 getter 函数进行 依赖收集(将观察者Watcher对象存放到当前闭包的订阅者Depsubs中),此时的数据劫持功能和观察者模式就实现了一个MVVM模式中的Binder,之后就是正常的渲染和更新流程。
  • 当数据发生变化或者视图导致的数据发生变化时,会触发数据劫持的setter函数,setter会通知初始化依赖收集中的Dep中和视图相应的 Watcher ,告知需要重新渲染视图,Watcher 就会再次通过 update 方法来更新视图。

vue的组件通信

  1. props/$emit
  2. $emit/$on(eventbus)
  3. vuex (单向数据流、state存放数据,mutation修改更新数据,action提供异步)
  4. $attrs/$listeners$attrs 里存放的是父组件中绑定的非 Props 属性(传递下来没被props接收的参数),$listeners里存放的是父组件中绑定的非原生事件。)
  5. provide/inject(祖先组件向其后所有子孙注入一个依赖都可调用)

有六种

遇到父子传数组或者对象时,通过加default实现。

1
2
3
4
5
6
7
props: {
content: {
type: Array,
// default: function () { return [] }
default: () => []
},
}

Vue.$set()

如果在实例创建之后添加新的属性到实例上,它不会触发视图更新

  1. 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter
  2. 由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。
  • Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (就是说就算是空也要提前在data中写好了)。
  • 它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上
  • this.$set(this.obj,'e',02)

get vs. defineProperty

当使用 get 关键字时,它和Object.defineProperty(对象,属性名,对应的方法或属性) 有类似的效果,在classes中使用时,二者有细微的差别。

当使用 get 关键字时,属性将被定义在实例的原型上,当使用Object.defineProperty()时,属性将被定义在实例自身上。

defineProperty其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过属性里的get和set实现了响应式

Vue.set()和this.$set()

vm.$set( target, propertyName/index, value )

this.$set(this.items, 1, 'two')将数组第二位更新为’two’

区别在于 Vue.set( ) 是将 set 函数绑定在 Vue 构造函数上,this.$set() 是将 set 函数绑定在 Vue原型上。

1.判断目标值是否为有效值,不是有效值直接停止

2.判断是否为数组,并且key值是否为有效的key值

  • 如果是数组,就选择数组的长度和key值取较大值作为数组的新的length值,并且替换目标值
  • splice方法,重写了,所以执行splice,会双向数据绑定

3.判断目标值是否为响应式的

  • 如果是vue实例,直接不行
  • 如果不是响应式的数据,就是普通的修改对象操作
  • 如果是响应式数据,那就通过Object.defineProperty进行数据劫持

4.通知dom更新

vue.$set()实现原理

Vue的mvvm原理

不只是object.defineProperty();从数据改变到视图改变,通过observe、dep、watcher、compile四个类和CpompileUtil辅助类实现的。

observe

会遍历检测每一个属性,如果不是对象则返回

1
2
3
4
5
6
7
8
9
10
11
function observe(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target;
}

// 重新定义各个属性,遍历
for(let key in target) {
defineReactive(target, key, target[key])
}
}

Watcher

观察者的目的就是给需要变化的那个元素增加一个观察者,用新值和老值进行比对,如果数据变化就执行对应的方法

这个类主要是用来观察方法/表达式中引用到的数据(数据需要是 reative 的,即 data 或者 props)变更,当变更后做出相应处理。

在compile的时候,给需要重新编译的DOM增加watcher

Dep

依赖收集器,如何将视图和数据关联起来呢?就是将每个数据和对应的watcher关联起来。当数据变化时让对应的watcher执行update方法即可!再想想在哪做操作呢?就是我们的set和get!

1
2
3
4
5
6
7
8
9
10
11
12
class Dep{
constructor(){
// 订阅的数组
this.subs = []
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}

Compile

交给virtualDOM完成。

数据发生改变,发布订阅

当data中的某一项数据发生改变时,将调用这个属性的Dep的notify方法,通知订阅了该属性的Watcher调用自身的update方法改变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
// watcher中的get方法
get(){
// 在取值前先将watcher保存到Dep上
Dep.target = this;
let value = this.getVal(this.vm,this.expr); // 会调用属性对应的get方法
Dep.target = null;
return value;
}

// 更新Observer中的defineReactive
defineReactive(obj,key,value){
let that = this;
let dep = new Dep(); // 每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
Object.defineProperty(obj,key,{
enumerable:true,
configurable:true,
get(){ // 当取值时调用的方法
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue){
if(newValue!=value){
that.observe(newValue);
value = newValue;
+ dep.notify(); // 通知所有人 数据更新了
}
}
});
}

总流程

首先会使用的类:

  • Observer类(数据劫持将对象所有属性改成get、set方法)
    • 深度递归劫持对象中的其他对象
    • defineReactive
      • 定义响应式,其中使用的基础是defineProperty
      • 每个变化的数据都会对应一个数组在dep中
      • 就是把对象obj里的属性key变成一个getter/setter形式的响应式的属性。同时在getter的时候收集依赖,并在setter的时候触发依赖。
  • Watcher类
    • 获取值的方法get,getVal,然后保存在dep中
    • 更新值的方法,如果两值不一致就更新
    • 在compile时,当数据变化的时候调用Watcher判断更新

关于发布订阅流程

  • dep(一个依赖收集通知器)

参考

更加详细

vue的双向绑定

3995692-a100ac5db2a19dd0.png

vue2.x

object.defindProperty()

定义对象的属性,get和set才是实现响应式的

defineReactive 对依赖收集的总结

在初次渲染时,会触发一次get函数,为了提高效率,节省资源,采用依赖收集,这里以之前手写的为例,get部分,我们就会对this.$data里的每一个属性(即key值)进行收集,看在哪些组件里进行了调用,以此提高效率。

而在set部分,就会更新我们收集到的依赖

vue3

相对于vue2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var obj={
a:1,
b:2
}
//无需借助外部变量
//对于vue 2,提高效率,无需for in 遍历找属性
//不会污染原对象,会返回一个新的代理对象,原对象依旧是原对象
//也是软件工程里的重要知识,尽量不要"污染"原对象,不用给原对象做任何操作
//只需对代理对象进行操作
var objChildren=new Proxy(obj,{
get(target,key,receiver){
console.log(target,key,receiver);
return target[key];
},
set(target,key,value,receiver){
return Reflect.set(target,key,value);
//return target[key]=value;
/*上面注释的代码和上一行意思相同*/
}
})

diff

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
//diff算法匹配机制
patchVnode(oldVnode,vnode){
//先拿到真实的dom
const el=vnode.el=oldVnode.el;
//分别拿出旧节点和新节点的子元素
let i,oldCh=oldVnode.children,ch=vnode.children;
//如果新旧节点相同,直接return
if(oldVnode==vnode) return;
/*分四种情况讨论*/
//1.只有文字节点不同的情况
if(oldVnode.text!==null&&vnode.text!==null&&oldVnode.text!==vnode.text){
api.setTextContent(el,vnode.text);
}else{
updateEle();
//2.如果新旧节点的子元素都存在,那么发生的是子元素变动
if(oldCh&&ch&&oldCh!==ch){
updateChildren();
//3.如果只有新节点有子元素,那么发生的是新增子元素
}else if(ch){
createEl(vnode);
//4.如果只有旧节点有子元素,那么发生的是新节点删除了子元素
}else if(oldCh){
api.removeChildren(el);
}
}
}

关于vue的diff优化

  1. 在update时旧node和新node头尾都有两个头尾变量,会先对他们的两个变量相互比较,都没匹配成功就会去比较key了
  2. key不要用index。

Vue3的diff优化

在模板编译的时候会在动态标签末尾加上/Text/PatchFlag,(也就是在生成VNode的时候,同时打上标记,在这个基础上再进行核心的diff算法),

  • 当 patchFlag 的值「大于」 0 时,代表所对应的元素在 patchVNode 时或 render 时是可以被优化生成或更新的。
  • 当 patchFlag 的值「小于」 0 时,代表所对应的元素在 patchVNode 时,是需要被 full diff,即进行递归遍历 VNode tree 的比较更新过程。

「Vue3.0对于不参与更新的元素,做静态标记并提示,只会被创建一次,在渲染时直接复用。」

VUEX和eventbus

  • 不是复杂的组件交互绑定的时候可以用eventbus
  • 有比较深层结构复杂,容易经常交互的组件就要用vuex更好一些,eventbus的性能开销更大而且会有很多绑定操作。
  • 全局状态管理可以适用于大的项目和交互复杂的项目

vuex本质

vuex的更新状态

  1. vuex内部实例化了一个vue实例, 并且将store的state,挂载在vue实例data中,$$state属性。在vuex源码可以看看相关的代码。
  2. 因为挂载在data中, 因此对vue对state也进行了setter和getter的劫持。更改store中的state实质上更改的vue实例data中的数据。

vuex本质就是一个没有template的vue组件

v-text 和 v-html 区别

v-text就是{{ }},其中的东西都不会进行html解析

v-html会对其中的东西进行html解析

NextTick原理

  1. 把回调函数放入callbacks等待执行
  2. 将执行函数放到微任务或者宏任务中
  3. 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调

Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

根据环境放入不同任务中:Promise.thenMutationObserversetImmediatesetTimeout