- 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 | function Observer(data) { |
2.实现一个 Watcher
1 | function Watcher(vm, exp, cb) { |
依赖收集
当你把一个普通的 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 | class Watcher { |
我们再拉通串一下整个流程:Vue 通过 defineProperty 完成了 Data 中所有数据的代理,当数据触发 get 查询时,会将当前的 Watcher 对象加入到依赖收集池 Dep 中,当数据 Data 变化时,会触发 set 通知所有使用到这个 Data 的 Watcher 对象去 update 视图。
1 | Object.defineProperty(obj, key, { |
代码演示
1 | class Observer { |
总结
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 | // 防止全局污染 重新定义数组原型 |
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 | function nextTick (cb, ctx) { |
它对于浏览器异步API的选用规则如下,Promise存在取由Promise.then,不存在Promise则取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在最后取setTimeout来实现。从上面的取用规则也可以看出来,nextTick即有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。
Vue能不能同步渲染?
- Vue.config.async = false
1 | function queueWatcher (watcher) { |
在我们的开发代码里,只需要加入下一句即可让你的页面渲染同步进行。
1 | import Vue from 'Vue' |
- this._watcher.sync = truevue中这么用
1
2
3
4
5
6
7
8
9Watcher.prototype.update = function update () {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};1
2
3
4
5
6
7
8
9
10data () {
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 | <template> |
子组件
1 | <template> |
不使用v-model模拟
父组件代码修改
1 | <template> |
子组件修改
1 | <template> |