Open berwin opened 6 years ago
@weimingxinas 不用先推到队列中,先判断下如果之前推进去过,这次就不推就行了。哈哈哈哈
看着头晕啊,再看几遍 😂
由浅到深 也同时清楚了一些概念
从“依赖收集在哪” 那里开始往下就已经不知所云了,好在这些字我都懂。
受教了。
@berwin 为什么这里的vue变化侦测中有的内容跟你写的深入浅出vue那本书中的有些内容不一样
@berwin能不能解释一下函数中的参数的意义
mark
什么情况不能使用 proto 我也不知道,各位大佬谁知道能否给我留个言?跪谢~
应该是考虑到IE对于proto的支持不好,貌似只有IE11支持
把我给讲明白了 写的真好
深入浅出 - vue变化侦测原理
其实在一年前我已经写过一篇关于 vue响应式原理的文章,但是最近我翻开看看发现讲的内容和我现在心里想的有些不太一样,所以我打算重新写一篇更通俗易懂的文章。
我的目标是能让读者读完我写的文章能学到知识,有一部分文章标题都以深入浅出开头,目的是把一个复杂的东西排除掉干扰学习的因素后剩下的核心原理通过很简单的描述来让读者学习到知识。
关于vue的内部原理其实有很多个重要的部分,变化侦测,模板编译,virtualDOM,整体运行流程等。
今天主要把变化侦测这部分单独拿出来讲一讲。
如何侦测变化?
关于变化侦测首先要问一个问题,在 js 中,如何侦测一个对象的变化,其实这个问题还是比较简单的,学过js的都能知道,js中有两种方法可以侦测到变化,
Object.defineProperty
和 ES6 的proxy
。到目前为止vue还是用的
Object.defineProperty
,所以我们拿Object.defineProperty
来举例子说明这个原理。这里我想说的是,不管以后vue是否会用
proxy
重写这部分,我讲的是原理,并不是api,所以不论以后vue会怎样改,这个原理是不会变的,哪怕vue用了其他完全不同的原理实现了变化侦测,但是本篇文章讲的原理一样可以实现变化侦测,原理这个东西是不会过时的。之前我写文章有一个毛病就是喜欢对着源码翻译,结果过了半年一年人家源码改了,我写的文章就一毛钱都不值了,而且对着源码翻译还有一个缺点是对读者的要求有点偏高,读者如果没看过源码或者看的和我不是一个版本,那根本就不知道我在说什么。
好了不说废话了,继续讲刚才的内容。
知道
Object.defineProperty
可以侦测到对象的变化,那么我们瞬间可以写出这样的代码:写一个函数封装一下
Object.defineProperty
,毕竟Object.defineProperty
的用法这么复杂,封装一下我只需要传递一个 data,和 key,val 就行了。现在封装好了之后每当
data
的key
读取数据get
这个函数可以被触发,设置数据的时候set
这个函数可以被触发,但是,,,,,,,,,,,,,,,,,,发现好像并没什么鸟用?怎么观察?
现在我要问第二个问题,“怎么观察?”
思考一下,我们之所以要观察一个数据,目的是为了当数据的属性发生变化时,可以通知那些使用了这个
key
的地方。举个🌰:
模板中有两处使用了
key
,所以当数据发生变化时,要把这两处都通知到。所以上面的问题,我的回答是,先收集依赖,把这些使用到
key
的地方先收集起来,然后等属性发生变化时,把收集好的依赖循环触发一遍就好了~总结起来其实就一句话,getter中,收集依赖,setter中,触发依赖。
依赖收集在哪?
现在我们已经有了很明确的目标,就是要在getter中收集依赖,那么我们的依赖收集到哪里去呢??
思考一下,首先想到的是每个
key
都有一个数组,用来存储当前key
的依赖,假设依赖是一个函数存在window.target
上,先把defineReactive
稍微改造一下:在
defineReactive
中新增了数组 dep,用来存储被收集的依赖。然后在触发 set 触发时,循环dep把收集到的依赖触发。
但是这样写有点耦合,我们把依赖收集这部分代码封装起来,写成下面的样子:
然后在改造一下
defineReactive
:这一次代码看起来清晰多了,顺便回答一下上面问的问题,依赖收集到哪?收集到Dep中,Dep是专门用来存储依赖的。
收集谁?
上面我们假装
window.target
是需要被收集的依赖,细心的同学可能已经看到,上面的代码window.target
已经改成了Dep.target
,那Dep.target
是什么?我们究竟要收集谁呢??收集谁,换句话说是当属性发生变化后,通知谁。
我们要通知那个使用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,有可能是模板,有可能是用户写的一个 watch,所以这个时候我们需要抽象出一个能集中处理这些不同情况的类,然后我们在依赖收集的阶段只收集这个封装好的类的实例进来,通知也只通知它一个,然后它在负责通知其他地方,所以我们要抽象的这个东西需要先起一个好听的名字,嗯,就叫它watcher吧~
所以现在可以回答上面的问题,收集谁??收集 Watcher。
什么是Watcher?
watcher 是一个中介的角色,数据发生变化通知给 watcher,然后watcher在通知给其他地方。
关于watcher我们先看一个经典的使用方式:
这段代码表示当
data.a.b.c
这个属性发生变化时,触发第二个参数这个函数。思考一下怎么实现这个功能呢?
好像只要把这个 watcher 实例添加到
data.a.b.c
这个属性的 Dep 中去就行了,然后data.a.b.c
触发时,会通知到watcher,然后watcher在执行参数中的这个回调函数。好,思考完毕,开工,写出如下代码:
这段代码可以把自己主动
push
到data.a.b.c
的 Dep 中去。因为我在
get
这个方法中,先把 Dep.traget 设置成了this
,也就是当前watcher实例,然后在读一下data.a.b.c
的值。因为读了
data.a.b.c
的值,所以肯定会触发getter
。触发了
getter
上面我们封装的defineReactive
函数中有一段逻辑就会从Dep.target
里读一个依赖push
到Dep
中。所以就导致,我只要先在 Dep.target 赋一个
this
,然后我在读一下值,去触发一下getter
,就可以把this
主动push
到keypath
的依赖中,有没有很神奇~依赖注入到
Dep
中去之后,当这个data.a.b.c
的值发生变化,就把所有的依赖循环触发 update 方法,也就是上面代码中 update 那个方法。update
方法会触发参数中的回调函数,将value 和 oldValue 传到参数中。所以其实不管是用户执行的
vm.$watch('a.b.c', (value, oldValue) => {})
还是模板中用到的data,都是通过 watcher 来通知自己是否需要发生变化的。递归侦测所有key
现在其实已经可以实现变化侦测的功能了,但是我们之前写的代码只能侦测数据中的一个 key,所以我们要加工一下
defineReactive
这个函数:这样我们就可以通过执行
walk(data)
,把data
中的所有key
都加工成可以被侦测的,因为是一个递归的过程,所以key
中的value
如果是一个对象,那这个对象的所有key也会被侦测。Array怎么进行变化侦测?
现在又发现了新的问题,
data
中不是所有的value
都是对象和基本类型,如果是一个数组怎么办??数组是没有办法通过Object.defineProperty
来侦测到行为的。vue 中对这个数组问题的解决方案非常的简单粗暴,我说说vue是如何实现的,大体上分三步:
第一步:先把原生
Array
的原型方法继承下来。第二步:对继承后的对象使用
Object.defineProperty
做一些拦截操作。第三步:把加工后可以被拦截的原型,赋值到需要被拦截的
Array
类型的数据的原型上。vue的实现
第一步:
第二步:
现在可以看到,每当被侦测的
array
执行方法操作数组时,我都可以知道他执行的方法是什么,并且打印到console
中。现在我要对这个数组方法类型进行判断,如果操作数组的方法是 push unshift splice (这种可以新增数组元素的方法),需要把新增的元素用上面封装的
walk
来进行变化检测。并且不论操作数组的是什么方法,我都要触发消息,通知依赖列表中的依赖数据发生了变化。
那现在怎么访问依赖列表呢,可能我们需要把上面封装的
walk
加工一下:我们定义了一个
Observer
类,他的职责是将data
转换成可以被侦测到变化的data
,并且新增了对类型的判断,如果是value
的类型是Array
循环 Array将每一个元素丢到 Observer 中。并且在 value 上做了一个标记
__ob__
,这样我们就可以通过value
的__ob__
拿到Observer实例,然后使用__ob__
上的dep.notify()
就可以发送通知啦。然后我们在改进一下Array原型的拦截器:
可以看到写了一个
switch
对method
进行判断,如果是push
,unshift
,splice
这种可以新增数组元素的方法就使用ob.observeArray(inserted)
把新增的元素也丢到Observer
中去转换成可以被侦测到变化的数据。在最后不论操作数组的方法是什么,都会调用
ob.dep.notify()
去通知watcher
数据发生了改变。arrayMethods 是怎么生效的?
现在我们有一个
arrayMenthods
是被加工后的Array.prototype
,那么怎么让这个对象应用到Array
上面呢?思考一下,我们不能直接修改
Array.prototype
因为这样会污染全局的Array,我们希望arrayMenthods
只对data
中的Array
生效。所以我们只需要把
arrayMenthods
赋值给value
的__proto__
上就好了。我们改造一下
Observer
:如果不能使用
__proto__
,就直接循环arrayMethods
把它身上的这些方法直接装到value
身上好了。什么情况不能使用
__proto__
我也不知道,各位大佬谁知道能否给我留个言?跪谢~所以我们的代码又要改造一下:
关于Array的问题
关于vue对Array的拦截实现上面刚说完,正因为这种实现方式,其实有些数组操作vue是拦截不到的,例如:
修改数组第一个元素的值,无法侦测到数组的变化,所以并不会触发
re-render
或watch
等。在例如:
清空数组操作,无法侦测到数组的变化,所以也不会触发
re-render
或watch
等。因为vue的实现方式就决定了无法对上面举得两个例子做拦截,也就没有办法做到响应,ES6是有能力做到的,在ES6之前是无法做到模拟数组的原生行为的,现在 ES6 的 Proxy 可以模拟数组的原生行为,也可以通过 ES6 的继承来继承数组原生行为,从而进行拦截。
总结
最后掏出vue官网上的一张图,这张图其实非常清晰,就是一个变化侦测的原理图。
getter
到watcher
有一条线,上面写着收集依赖,意思是说getter
里收集watcher
,也就是说当数据发生get
动作时开始收集watcher
。setter
到watcher
有一条线,写着Notify
意思是说在setter
中触发消息,也就是当数据发生set
动作时,通知watcher
。Watcher
到 ComponentRenderFunction 有一条线,写着Trigger re-render
意思很明显了。