双向数据绑定方式
发布订阅方式
一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set(‘property’, value)
脏值检查
angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:
- DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
- XHR响应事件 ( $http )
- 浏览器Location变更事件 ( $location )
- Timer事件( timeout ,interval )
- 执行 digest() 或apply()
数据劫持
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Vue的双向绑定实现方式
首先实现一个observer类,用来监听数据并通知订阅者执行相应的回调函数
实现compile类,作用是解析指令,为每个指令对应节点绑定更新函数,添加监听数据的订阅者
watch类为observer和compile的桥梁,在自身实例化的时候向订阅器里添加自身

1 |
|
这里跟Vue的源码写的还是有很多出入,像complie解析dom节点的指令这一块是很复杂而且是单封装成一个函数的,Vue还可以实现自定义指令等等这里都没有实现。observe劫持监听函数也是具体逻辑没这么简单,订阅器这里只是简单的写了一个_bindding = [], 实际也要复杂很多,
源码里的双向数据绑定
首先,Vue会先初始化,调用_init()方法
_init()
1 | function Vue(options) { |
其中调用了initState(vm)方法
initState(vm)
1 | function initState(vm) { |
initData(vm);
1 | //初始化数据 获取options.data 的数据 将他们添加到 监听者中 |
这里面我们主要看observe(data, true);方法
observe(vm._data = {}, true)
1 | /** |
这里会为value添加创建一个观察者也就是new Observer(value);,下面我们来看Observer函数
Observer构造函数
1 | /** |
这里Observer会判断value类型是否为数组,如果是数组的话会调用observeArray方法,拆分数组一个个重新调用observe方法。
否则的话就调用walk方法,会遍历对象每一个属性然后调用defineReactive方法
defineReactive()
1 | /** |
这是双向绑定的核心–通过Object.defineProperty重写value的get,set,也就是读取和设置新值。
其中Dep可以理解为一个订阅器,
- 在get是会判断是否有Dep.target从而调用dep.depend()。Dep.target是Watcher的实例
- 在set时会判断调用dep.notify(),为订阅器里的所有订阅者更新视图。
下面是Dep的源码:
Dep
1 | /** |
这里面有上面defineReactive调用比较重要的depend、notify方法。
依据Dep.target会调用depend方法。depend方法会调用Dep.target.addDep(this);方法,此方法调用dep.addSub(this);向订阅器添加了订阅者。
1 | /** |
也就是说,defineReactive内部的get会依据是否有Dep.target来向订阅器添加订阅者
那Dep.target到底是什么以及它的作用:
Dep.target是Watcher的实例,
了解作用之前我们看一下都在哪些地方调用了Dep.target?
1 | Dep.target = null; |
Dep.target虽然是全局属性,但是被封装在上面这两个函数中
那又有什么地方调用了pushTarget呢?
1 | // 第一处 |
以上虽然有三处调用pushTarget,但实际只有一处有实际作用,第一二处Dep.target反而设置为空,没有发挥实际作用。而第三处是在Watcher类原型方法上调用的,我们来看看Watcher类
Watcher
1 | /** |
从Watcher类可以看到,每次实例化Watcher的时候都会调用this.get函数,
这里this.get()主要有两步骤
- 调用pushTarget
- 调用getter方法触发Observer的get方法将自己加入订阅者
那又是什么时候会实例化Watcher呢
- 安装组件
mountComponent - 初始化计算属性 并且判断属性的key是否在data中 ,将计算属性的key添加入监听者中
initComputed - Vue.prototype.$watch中
- keep-alive组件的mounted
- 封装在createWatcher函数中
- 初始化watch监听
initWatch
- 初始化watch监听
watch 和 computed的区别
computed计算属性
计算属性的初始化是发生在Vue实例初始化的initState里面然后调用initComputed
initComputed
1 | const computedWatcherOptions = { lazy: true } |
initComputed这段代码做了几件事
- 获取这个 userDef 对应的 getter 函数(存放到
watcher.getter中,用于后面的计算) - 每个computed配发watcher;这个watcher属性很重要
{ lazy: true } - defineComputed处理;
- 收集所有computed的watcher;
在Watcher源码里lazy作用
- computed新建watcher的时候,传入lazy,作用是把计算结果缓存起来,而不是每次使用都要重新计算;
- 保存设置的 getter,把用户设置的 computed-getter,存放到 watcher.getter 中,用于后面的计算;
- watcher.value 存放计算结果,但是这里有个条件,因为 lazy 的原因,不会新建实例并马上读取值;这里可以算是 Vue 的一个优化,只有你再读取 computed,再开始计算,而不是初始化就开始计算值了,虽然没有一开始计算,但是计算 value 还是这个 watcher.get 这个方法
defineComputed
1 | function defineComputed (target,key,userDef){ |
defineComputed 方法实质也是利用 Object.defineProperty 给计算属性添加 set get。
set函数默认是空函数,如果用户设置,则使用用户设置
createComputedGetter
1 | function createComputedGetter (key) { |
缓存控制是通过watcher.dirty控制的,watcher.evaluate用来重新计算,更新缓存值,并重置dirty为false,表示缓存已更新
watch
1 | Vue.prototype.$watch = function ( |
也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher。
所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。其实 Watcher 支持了不同的类型。
Vue双向绑定的问题
监听对象问题
- 无法检测对象属性的增、删、改
对象和数组都是js里的引用类型,在实际存储中,数据是存储在堆中的,利用存储在栈里的对象名或者数组名的指针进行索引,因此也存在在浅拷贝和深拷贝以及等号赋值时,到底是仅仅新建了一个指针指向了同一份数据,还是两个指针分别指向了两份完全一样的数据的问题
监听数组/对象的问题
- 无法检测数组的新增
- 无法检测通过索引改变数组的操作。
Vue2.0中响应式数据是通过Object.defineProperty实现,因此无法检测数组/对象的新增。
为什么不能检测通过索引改变数组的操作呢??
实际上Object.defineProperty 可以监听到数组改变,但是Vue没有这么做,尤雨溪给出理由是性能问题。
解决方法
1 | this.$set(obj, key ,value) - 可实现增、改 |
this.$set源码: Vue.$set = set
1 | /** |
参考文章
https://segmentfault.com/a/1190000014274840
https://juejin.im/post/6844903854740340749#heading-3
https://www.cnblogs.com/youhong/p/12173354.html
Vue源码分析