详解Vue源码学习之双向绑定
- 作者: 社会你城哥
- 来源: 51数据库
- 2021-08-14
原理
当你把一个普通的 javascript 对象传给 vue 实例的 data 选项,vue 将遍历此对象所有的属性,并使用 object.defineproperty 把这些属性全部转为 getter/setter。object.defineproperty 是 es5 中一个无法 shim 的特性,这也就是为什么 vue 不支持 ie8 以及更低版本浏览器。
上面那段话是vue官方文档中截取的,可以看到是使用object.defineproperty实现对数据改变的监听。vue主要使用了观察者模式来实现数据与视图的双向绑定。
function initdata(vm) { //将data上数据复制到_data并遍历所有属性添加代理
vm._data = vm.$options.data;
const keys = object.keys(vm._data);
let i = keys.length;
while(i--) {
const key = keys[i];
proxy(vm, `_data`, key);
}
observe(data, true /* asrootdata */) //对data进行监听
}
在第一篇数据初始化中,执行new vue()操作后会执行initdata()去初始化用户传入的data,最后一步操作就是为data添加响应式。
实现
在vue内部存在三个对象:observer、dep、watcher,这也是实现响应式的核心。
observer
observer对象将data中所有的属性转为getter/setter形式,以下是简化版代码,详细代码请看这里。
export function observe (value) {
//递归子属性时的判断
if (!isobject(value) || value instanceof vnode) {
return
}
...
ob = new observer(value)
}
export class observer {
constructor (value) {
... //此处省略对数组的处理
this.walk(value)
}
walk (obj: object) {
const keys = object.keys(obj)
for (let i = 0; i < keys.length; i++) {
definereactive(obj, keys[i]) //为每个属性创建setter/getter
}
}
...
}
//设置set/get
export function definereactive (
obj: object,
key: string,
val: any
) {
//利用闭包存储每个属性关联的watcher队列,当setter触发时依然能访问到
const dep = new dep()
...
//如果属性为对象也创建相应observer
let childob = observe(val)
object.defineproperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactivegetter () {
if (dep.target) {
dep.depend() //将当前dep传到对应watcher中再执行watcher.adddep将watcher添加到当前dep.subs中
if (childob) { //如果属性是对象则继续收集依赖
childob.dep.depend()
...
}
}
return value
},
set: function reactivesetter (newval) {
...
childob = observe(newval) //如果设置的新值是对象,则为其创建observe
dep.notify() //通知队列中的watcher进行更新
}
})
}
创建observer对象时,为data的每个属性都执行了一遍definereactive方法,如果当前属性为对象,则通过递归进行深度遍历。该方法中创建了一个dep实例,每一个属性都有一个与之对应的dep,存储所有的依赖。然后为属性设置setter/getter,在getter时收集依赖,setter时派发更新。这里收集依赖不直接使用addsub是为了能让watcher创建时自动将自己添加到dep.subs中,这样只有当数据被访问时才会进行依赖收集,可以避免一些不必要的依赖收集。
dep
dep就是一个发布者,负责收集依赖,当数据更新是去通知订阅者(watcher)。
export default class dep {
static target: ?watcher; //指向当前watcher
constructor () {
this.subs = []
}
//添加watcher
addsub (sub: watcher) {
this.subs.push(sub)
}
//移除watcher
removesub (sub: watcher) {
remove(this.subs, sub)
}
//通过watcher将自身添加到dep中
depend () {
if (dep.target) {
dep.target.adddep(this)
}
}
//派发更新信息
notify () {
...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
watcher
//解析表达式(a.b),返回一个函数
export function parsepath (path: string): any {
if (bailre.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]] //遍历得到表达式所代表的属性
}
return obj
}
}
export default class watcher {
constructor (
vm: component,
exporfn: string | function,
cb: function,
options?: ?object,
isrenderwatcher?: boolean
) {
this.vm = vm
if (isrenderwatcher) {
vm._watcher = this
}
//对创建的watcher进行收集,destroy时对这些watcher进行销毁
vm._watchers.push(this)
// options
if (options) {
...
this.before = options.before
}
...
//上一轮收集的依赖集合dep以及对应的id
this.deps = []
this.depids = new set()
//新收集的依赖集合dep以及对应的id
this.newdeps = []
this.newdepids = new set()
this.expression = process.env.node_env !== 'production'
? exporfn.tostring()
: ''
// parse expression for getter
if (typeof exporfn === 'function') {
this.getter = exporfn
} else {
this.getter = parsepath(exporfn)
...
}
...
this.value = this.get()
}
/** * evaluate the getter, and re-collect dependencies. */
get () {
pushtarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleerror(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
poptarget()
this.cleanupdeps() //清空上一轮的依赖
}
return value
}
/** * add a dependency to this directive. */
adddep (dep: dep) {
const id = dep.id
if (!this.newdepids.has(id)) { //同一个数据只收集一次
this.newdepids.add(id)
this.newdeps.push(dep)
if (!this.depids.has(id)) {
dep.addsub(this)
}
}
}
//每轮收集结束后去除掉上轮收集中不需要跟踪的依赖
cleanupdeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newdepids.has(dep.id)) {
dep.removesub(this)
}
}
let tmp = this.depids
this.depids = this.newdepids
this.newdepids = tmp
this.newdepids.clear()
tmp = this.deps
this.deps = this.newdeps
this.newdeps = tmp
this.newdeps.length = 0
},
update () {
...
//经过一些优化处理后,最终执行this.get
this.get();
}
// ...
}
依赖收集的触发是在执行render之前,会创建一个渲染watcher:
updatecomponent = () => {
vm._update(vm._render(), hydrating) //执行render生成vnode并更新dom
}
new watcher(vm, updatecomponent, noop, {
before () {
if (vm._ismounted) {
callhook(vm, 'beforeupdate')
}
}
}, true /* isrenderwatcher */)
在渲染watcher创建时会将dep.target指向自身并触发updatecomponent也就是执行_render生成vnode并执行_update将vnode渲染成真实dom,在render过程中会对模板进行编译,此时就会对data进行访问从而触发getter,由于此时dep.target已经指向了渲染watcher,接着渲染watcher会执行自身的adddep,做一些去重判断然后执行dep.addsub(this)将自身push到属性对应的dep.subs中,同一个属性只会被添加一次,表示数据在当前watcher中被引用。
当_render结束后,会执行poptarget(),将当前dep.target回退到上一轮的指,最终又回到了null,也就是所有收集已完毕。之后执行cleanupdeps()将上一轮不需要的依赖清除。当数据变化是,触发setter,执行对应watcher的update属性,去执行get方法又重新将dep.target指向当前执行的watcher触发该watcher的更新。
这里可以看到有deps,newdeps两个依赖表,也就是上一轮的依赖和最新的依赖,这两个依赖表主要是用来做依赖清除的。但在adddep中可以看到if (!this.newdepids.has(id))已经对收集的依赖进行了唯一性判断,不收集重复的数据依赖。为何又要在cleanupdeps中再作一次判断呢?
while (i--) {
const dep = this.deps[i]
if (!this.newdepids.has(dep.id)) {
dep.removesub(this)
}
}
let tmp = this.depids
this.depids = this.newdepids
this.newdepids = tmp
this.newdepids.clear()
tmp = this.deps
this.deps = this.newdeps
this.newdeps = tmp
this.newdeps.length = 0
在cleanupdeps中主要清除上一轮中的依赖在新一轮中没有重新收集的,也就是数据刷新后某些数据不再被渲染出来了,例如:
<body>
<div id="app">
<div v-if='flag'> </div>
<div v-else> </div>
<button @click="msg1 += '1'">change</button>
<button @click="flag = !flag">toggle</button>
</div>
<script type="text/javascript">
var vm = new vue({
el: '#app',
data: {
flag: true,
msg1: 'msg1',
msg2: 'msg2'
}
})
</script>
</body>
每次点击change,msg1都会拼接一个1,此时就会触发重新渲染。当我们点击toggle时,由于flag改变,msg1不再被渲染,但当我们点击change时,msg1发生了变化,但却没有触发重新渲染,这就是cleanupdeps起的作用。如果去除掉cleanupdeps这个步骤,只是能防止添加相同的依赖,但是数据每次更新都会触发重新渲染,又去重新收集依赖。这个例子中,toggle后,重新收集的依赖中并没有msg1,因为它不需要被显示,但是由于设置了setter,此时去改变msg1依然会触发setter,如果没有执行cleanupdeps,那么msg1的依赖依然存在依赖表里,又会去触发重新渲染,这是不合理的,所以需要每次依赖收集完毕后清除掉一些不需要的依赖。
总结
依赖收集其实就是收集每个数据被哪些watcher(渲染watcher、computedwatcher等)所引用,当这些数据更新时,就去通知依赖它的watcher去更新。
以上所述是小编给大家介绍的vue源码学习之双向绑定详解整合,希望对大家有所帮助
