vue 双向绑定效果
1 | <div id="mvvm-app"> |
效果:
实现双向绑定效果的做法
目前几种主流的 mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare 等)添加了 change(input)事件,来动态修改 model 和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。
实现数据绑定的做法有大致如下几种:
发布者-订阅者模式(backbone.js)
一般通过 sub, pub 的方式实现数据和视图的绑定监听,更新数据方式通常做法是
vm.set('property', value)
,这里有篇文章讲的比较详细,有兴趣可点这里。这种方式现在毕竟太 low 了,我们更希望通过
vm.property = value
这种方式更新数据,同时自动更新视图,于是有了下面两种方式脏值检查(angular.js)
angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过
setInterval()
定时轮询检测数据变动,当然 Google 不会这么 low,angular 只有在指定的事件触发时进入脏值检测,大致如下:- DOM 事件,譬如用户输入文本,点击按钮等( ng-click )
- XHR 响应事件 ( $http )
- 浏览器 Location 变更事件 ( $location )
- Timer 事件( $timeout , $interval )
- 执行 $digest() 或 $apply()
数据劫持(vue.js)
vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过
Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
实现思路
已经了解到 vue 是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()
来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉 defineProperty,猛戳这里
整理了一下,要实现 mvvm 的双向绑定,就必须要实现以下几点:
- 实现一个数据监听器 Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者;
- 实现一个指令解析器 Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数;
- 实现一个 Watcher,作为连接 Observer 和 Compile 的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图;
- mvvm 入口函数,整合以上三者.
上述流程如图所示:
实现 Observer
我们知道可以利用Obeject.defineProperty()
来监听属性变动。
那么将需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter
和getter
。
这样的话,给这个对象的某个值赋值,就会触发setter
,那么就能监听到了数据变化..
1 | var data = { name: 'kindeng' } |
这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器
,很简单,维护一个数组,用来收集订阅者,数据变动触发 notify,再调用订阅者的 update 方法,代码改善之后是这样:
1 | // ... 省略 |
那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上面的思路整理中我们已经明确订阅者应该是 Watcher, 而且var dep = new Dep();
是在 defineReactive
方法内部定义的,所以想通过dep
添加订阅者,就必须要在闭包内操作,所以我们可以在 getter
里面动手脚:
1 | // Observer.js |
这里已经实现了一个 Observer 了,已经具备了监听数据和数据变化通知订阅者的功能。
Observer 完整代码
1 | function Observer(data) { |
实现 Compile
compile 主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图.
因为遍历解析的过程有多次操作 dom 节点,为提高性能和效率,会先将跟节点el
转换成文档碎片fragment
进行解析编译操作,解析完成,再将fragment
添加回原来的真实 dom 节点中
1 | function Compile(el) { |
compileElement 方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定
1 | Compile.prototype = { |
这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如<span v-text="content" other-attr
中v-text
便是指令,而other-attr
不是指令,只是普通的属性。
监听数据、绑定更新函数的处理是在compileUtil.bind()
这个方法中,通过new Watcher()
添加回调来接收数据变化的通知。
Compile 完整代码
1 | function Compile(el, vm) { |
实现 Watcher
Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁,主要做的事情是:
- 在自身实例化时往属性订阅器(dep)里面添加自己
- 自身必须有一个 update()方法
- 待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。
1 | function Watcher(vm, exp, cb) { |
实例化Watcher
的时候,调用get()
方法,通过Dep.target = watcherInstance
标记订阅者是当前 watcher 实例,强行触发属性定义的getter
方法,getter
方法执行的时候,就会在属性的订阅器dep
添加当前 watcher 实例,从而在属性值有变化的时候,watcherInstance 就能收到更新通知。
Watcher 完整代码
1 | function Watcher(vm, exp, cb) { |
基本上 vue 中数据绑定相关比较核心的几个模块也是这几个,猛戳这里 , 在src
目录可找到 vue 源码。
实现 MVVM
MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
一个简单的 MVVM 构造器是这样子:
1 | function MVVM(options) { |
但是这里有个问题,从代码中可看出监听的数据对象是 options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq';
这样的方式来改变数据。
显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';
所以这里需要给 MVVM 实例添加一个属性代理的方法,使访问 vm 的属性代理为访问 vm._data 的属性,改造后的代码如下:
1 | function MVVM(options) { |
这里主要还是利用了Object.defineProperty()
这个方法来劫持了 vm 实例对象的属性的读写权,使读写 vm 实例的属性转成读写了vm._data
的属性值,达到鱼目混珠的效果。
下载 Demo