• defineProperty和proxy区别
  • 谈谈对vue响应式原理的理解(Vue双向绑定如何实现的)1.数据劫持2.发布订阅
  • vue中的依赖收集是怎么处理的
  • vue如何检测数组变化(重写数组方法)
  • 为何采用异步渲染(Vue中的数据多次变化但只会更新一次)
  • nextTick实现原理
  • v-model实现原理

defineProperty和proxy区别

Vue3.x改用Proxy替代Object.defineProperty。

因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
Proxy只会代理对象的第一层,Vue3是怎样处理这个问题的呢?
判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。

Proxy 与 Object.defineProperty 优劣对比

Proxy 的优势如下:
Proxy 可以直接监听对象而非属性;
Proxy 可以直接监听数组的变化;
Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。

谈谈对vue响应式原理/双向绑定原理的理解

Vue 数据双向绑定主要是指:a数据变化更新视图,b视图变化更新数据。其中,View视图变化更新Data,可以通过事件监听的方式来实现,所以 Vue数据双向绑定的工作主要是如何根据Data变化更新View。

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

监听器 Observer:
完成 Data 中所有数据的代理
对数据对象进行遍历,包括子属性对象的属性,利用Object.defineProperty这个核心API给所有属性都加上 setter 和 getter属性。当某个对象的值发生变化,会触发 setter,那么就能监听到了数据变化,达到视图更新的目的

解析器 Compile:
1.解析 Vue 模板指令(v-for v-if等),将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数(@click/@keydown)
2.跟Watcher建立关系,即添加监听数据的订阅者Watcher,一旦数据有变动,收到通知,调用更新函数进行数据更新。(通过new Watcher()添加回调来接收数据变化的通知)

订阅者(观察者) Watcher:
1.Watcher是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
2.每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把接触过属性记录为依赖(这句话难以理解,其实就是初始化Watcher读取属性的时候,就开始记录为依赖),之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新——这是一个典型的观察者模式

订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。

另外一种全面回答:

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

1.需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
2.compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
3.Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: a.在自身实例化时往属性订阅器(dep)里面添加自己 b.自身必须有一个 update()方法 c.待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。

4.MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。

1.实现一个 Observer

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
52
53
54
55
56
57
function Observer(data) {
this.data = data
this.walk(data)
}
Observer.prototype = {
walk(data) {
//遍历,对这个对象的所有属性都进行监听
Object.keys(data).forEach(() => {
this.defineReactive(data, key, data[key])
})
},
defineReactive(data, key, val) {
let dep = new Dep()
let childObj = observe(val)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter() {
if (Dep.target) {
// 在这里添加一个订阅者
console.log(Dep.target)
dep.addSub(Dep.target)
}
return val
},
set: function setter() {
if (newVal === val) {
return
}
val = newVal
childObj = observe(newVal)
dep.notify()
},
})
},
}
function observe(value, vm) {
if (!value || typeof value !== 'object') {
return
}
return new Observer(value)
}
// 消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数
function Dep() {
this.subs = []
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub)
},
notify() {
this.subs.forEach((sub) => {
sub.update()
})
},
}
Dep.target = null

2.实现一个 Watcher

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
function Watcher(vm, exp, cb) {
this.cb = cb
this.vm = vm
this.exp = exp
this.value = this.get() // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update() {
this.run()
},
run() {
let value = this.vm.data[this.exp]
let oldVal = this.value
if (value !== oldVal) {
this.value = value
this.cb.call(this.vm, value, oldVal)
}
},
get() {
Dep.target = this // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null // 释放自己
return value
},
}

依赖收集

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

最让人疑问的就是组件渲染过程中是如何把接触过的数据property记录为依赖的,简单就是说vue是如何收集依赖的???

(简单点说 在执行渲染函数的途中读取到了data.XX,就触发了defineReactive函数中劫持的get,进行dep.depend)

先过一遍初始化流程

new Vue => initMixin(vue) => initState(vm) => initData => observer(data) => new Observe() => walk(value)
(到这里基本结束了,如果date没有值的话)
=> defineReactive => function reactiveGetter => Dep.target => dep.depend => Watcher.addDep(dep) => dep.addSub(Watcher) => subs.push(Watcher)

先编译成渲染函数,完了会执行$mount,开始挂载

$mount -> mountComponent -> new Watcher(vm, updateComponent) => Watcher.get() => updateComponent => this.getter(Dep.target: true 开始收集依赖啦!!!,这里的getter就是vm._render) => vm._update => vm._render() 生成Vnode(虚拟dom) => patch方法把虚拟DOM转换成真正的DOM节点

核心还是 Watcher观察者中的getter方法,触发收集依赖。

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
class Watcher {
constructor(
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: '';
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
}
// 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
this.value = this.lazy
? undefined
: this.get();
},
get () {
// 该函数用于缓存 Watcher
// 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 触发getter,添加自己到属性订阅器中
// 调用回调函数,也就是 updateComponent 函数 vm._update(vm._render(), hydrating);
// this.getter ƒ () {
// vm._update(vm._render(), hydrating);
// }
// 实例化Watcher 会读取双向绑定的值,从而触发依赖收集 最最最重要的一步 关键环节
// 如果这步注释掉 vue的双向绑定就会失效 game over
value = this.getter.call(vm, vm)
}
finally {
// 恢复 Watcher
popTarget()
// 清理依赖,判断是否还需要某些依赖,不需要的清除
// 这是为了性能优化
this.cleanupDeps()
}
return value
}
}

我们再拉通串一下整个流程:Vue 通过 defineProperty 完成了 Data 中所有数据的代理,当数据触发 get 查询时,会将当前的 Watcher 对象加入到依赖收集池 Dep 中,当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去 update 视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function setter() {}
})

代码演示

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
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
79
80
81
class Observer {
constructor(value) {
this.value = value
if (!value || (typeof value !== 'object')) {
return
} else {
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
// 订阅者Dep,存放观察者对象
class Dep {
constructor() {
this.subs = []
}
/*添加一个观察者对象*/
addSub (sub) {
this.subs.push(sub)
}
/*依赖收集,当存在Dep.target的时候添加观察者对象*/
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知所有watcher对象更新视图
notify () {
this.subs.forEach((sub) => {
sub.update()
})
}
}
class Watcher {
constructor() {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
}
update () {
console.log('视图更新啦')
}
/*添加一个依赖关系到Deps集合中*/
addDep (dep) {
dep.addSub(this)
}
}
function defineReactive (obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
dep.depend() /*进行依赖收集*/
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val) return
dep.notify()
}
})
}
class Vue {
constructor (options) {
this._data = options.data
new Observer(this._data) // 所有data变成可观察的
new Watcher() // 创建一个观察者实例
console.log('render~', this._data.test)
}
}
let o = new Vue({
data: {
test: 'hello vue.'
}
})
o._data.test = 'hello mvvm!'

Dep.target = null

总结

1.首先 Vue 通过 defineProperty 完成了 Data 中所有数据的代理,
2.在Vue中模版编译过程中的 指令或者数据绑定 都会实例化一个Watcher实例,实例化过程中触发get()将自身指向Dep.target,会将当前的 Watcher 对象加入到依赖收集池 Dep 中,即触发dep.depend()进行依赖收集
3.当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去 update 视图,最后实际上调用的是当前Watcher中的回调函数cb,进而更新视图

vue如何检测数组变化

如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作,更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。

  • 使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法,当调用数组api时,可以通知依赖更新。
  • 如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
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
// 防止全局污染 重新定义数组原型
const arrayProto = Array.prototype
// 创建新对象,原型指向Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重写以下函数
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 缓存原生函数
const original = arrayProto[method]
// 重写函数
def(arrayMethods, method, function mutator (...args) {
// 先调用原生函数获得结果
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
// 调用以下几个函数时,监听新数据
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 手动派发更新
ob.dep.notify()
return result
})
})

/**
* Define a property. 数据描述符
*/
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}

vue为何采用异步渲染,跟$nextTick有联系

为啥要异步渲染

  • 提升用户体验
  • 提升性能 去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的

例如,当你设置 vm.someData = ‘new value’,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。

Vue如何异步渲染

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
即优先考虑 Promise.then => MutationObserver => setImmediate => setTimeout

接下来在源码层面梳理一下的Vue的异步渲染过程。(最终还是会异步执行run)

start => this.val = xxx => Object.defineProperty.setter => dep.notify() => subs[i].update() => 是否需要异步渲染sync,需要 => queueWatcher(this),新增全局变量queue = []存储更新操作函数 => nextTick(flushSchedulerQueue)批量将更新函数flushSchedulerQueue作为参数传入到nextTick => timerFunc => 异步api(Promise.then/MutationObserver/setImmediate/setTimeout) 开始执行 => flushCallbacks => 遍历执行订阅函数flushSchedulerQueue => queue.sort,按照id排序进行区分新旧值 =>
Watcher.run() => Watcher.get() => this.getter() => uppdateComponent => _update => vm.patch比较新旧节点 => updateChildern => end

nextTick 实现原理

首先nextTick并不是浏览器本身提供的一个异步API,而是Vue中,用由浏览器本身提供的原生异步API封装而成的一个异步封装方法

nextTick函数的执行后,把传入的flushSchedulerQueue函数push到callbacks全局数组里,pending在初始情况下是false,这时候将触发timerFunc。
timerFunc函数是由浏览器的Promise、MutationObserver、setImmediate、setTimeout这些异步API实现的,异步API的回调函数是flushCallbacks函数。

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
52
53
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}

var timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
};
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true
});
timerFunc = function () {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}

它对于浏览器异步API的选用规则如下,Promise存在取由Promise.then,不存在Promise则取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在最后取setTimeout来实现。从上面的取用规则也可以看出来,nextTick即有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。

Vue能不能同步渲染?

  • Vue.config.async = false
1
2
3
4
5
6
7
8
9
10
11
12
13
function queueWatcher (watcher) {
...
// 在全局队列里存储将要响应的变化update函数
queue.push(watcher);
...
// 当async配置是false的时候,页面更新是同步的
if (!config.async) {
flushSchedulerQueue();
return
}
// 将页面更新函数放进异步API里执行,同步代码执行完开始执行更新页面函数
nextTick(flushSchedulerQueue);
}

在我们的开发代码里,只需要加入下一句即可让你的页面渲染同步进行。

1
2
import Vue from 'Vue'
Vue.config.async = false
  • this._watcher.sync = true
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Watcher.prototype.update = function update () {
    if (this.lazy) {
    this.dirty = true;
    } else if (this.sync) {
    this.run();
    } else {
    queueWatcher(this);
    }
    };
    vue中这么用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    data () {
    return { val: 0 }
    },
    mounted () {
    this._watcher.sync = true
    this.val = 1
    this._watcher.sync = false
    this.val = 2
    this.val = 3
    }

v-model实现原理

v-model是如何实现双向绑定的?
v-model是用来在表单控件或者组件上创建双向绑定的
他的本质是v-bind和v-on的语法糖
在一个组件上使用v-model,默认会为组件绑定名为value的prop和名为input的事件
使用场景: 子组件需要改变父组件 通过props传入的值,即子组件改变父组件的某一个值

使用v-model模拟

父组件

父组件通过v-model绑定值
如需根据v-model传入的值改变,而触发其他更新请通过watch传入的值

子组件

声明model对象,设置组件的event事件和prop值
通过props接受父组件传过来的值
修改通过this.$emit广播事件

父组件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<children v-model="message"></children>
</template>
<script>
import children from "./children.vue";
export default {
components: {
children
},
data() {
return {
message: "parent"
};
},
watch: {
// 监听message变化
message(newV, oldV) {
console.log(newV, oldV);
}
}
};

子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<input
type="text"
v-bind:value='value'
v-on:input='onMessage'
/>
</template>
<script>
export default {
model: {
prop: "value", //这个字段,是指父组件设置 v-model 时,将变量值传给子组件的 msg
event: "input" //这个字段,是指父组件监听 parent-event 事件
},
props: {
value: String //此处必须定义和model的prop相同的props,因为v-model会传值给子组件
},
methods: {
onMessage(e) {
this.$emit('input', e.target.value)
}
}
};
</script>

不使用v-model模拟

父组件代码修改

1
2
3
4
5
6
<template>
<Children v-bind:value="message" v-on:input="(event) => { message = event }"/>
</template>
<script>
// 不变
</script>

子组件修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<input
type="text"
v-bind:value='value'
v-on:input='onMessage'
/>
</div>
</template>
export default {
props: {
value: String
},
methods: {
onMessage(e) {
this.$emit('input', e.target.value)
}
}
};
</script>

参考