回顾
上一节对数据进行了劫持, 和了解 Dep 和 watcher 是什么, 以及他们之间关系
目前已知在 Dep 对象中有通知功能和存储 watcher 功能, 在 Watcher 对象中有存储 dep 和视图更新功能, 接下来开始写代码
Dep
在 observer.JS 文件中声明 Dep 对象
- // observer.JS
- // 对 Dep 设置 id
- var countId = 0
- function Dep() {
- // 每次实例化 Dep 的时候就使 id+1
- this.id = countId++
- // 用于存储 watch 对象的数组
- this.watchs = []
- }
- Dep.prototype = {
- // 添加 watcher 存储到 dep 上
- addWatchs(watch) {
- this.watchs.push(watch)
- },
- // 发布消息, 通知 watch 更新
- notify(){
- // 通知更新
- ....
- }
- }
Dep 实例化的时机
之前已经写过, 每一个数据对应一个 Dep 对象, 那么 Dep 实例化的时机应该是在对数据进行 defineProperty 的时候, 我们需要 Dep 对象, 在 setter 的时候通知 watcher 进行更新
在 defineReactive 函数中创建属于当前数据的 Dep, 然后在 setter 的时候通过 notify 函数发布通知
- // observer.JS
- ...
- defineReactive(data, key,val){
- // 创建该对象的 dep 对象
- var dep = new Dep()
- // 间接性的递归, 监测更深层次的数据
- var childObj = observe(val);
- // 给 data 重新定义属性(添加 set/get)
- Object.defineProperty(data,key,{
- enumerable: true, // 可枚举
- configurable: false, // 不能再 define
- get(){
- return val
- },
- set(newVal){
- if(newVal === val){
- return
- }
- val = newVal
- // 如果新值是一个对象的话就再进行监测
- childObj = observe(newVal)
- // 通知更新
- dep.notify()
- }
- })
- }
- ...
- Watcher
上一章中有写到 watcher 对象的创建时机应该是在模板编译阶段
那么在编译阶段什么地方创建 watcher 对象呢?
由于 watcher 对象需要对模板更新数据, 那么更新模板的函数自然由指令处理函数也就是解析模板数据的函数来解决
在 compile.JS 内的 vUtil 对象中, 我们对指令进行了统一的处理, 并且模板更新的函数也是由他来提供的, 所以 watcher 的更新回调写在这里是再好不过的
- // compile.JS
- Compile.prototype.vUtil = {
- ...
- deal: function (vm, node, value, type) {
- var _self = this;
- // 这里需要处理不同的指令
- this.dealTypeFn[type+'Updata'] && this.dealTypeFn[type+'Updata'](node, this.getDataValue(vm,value))
- // 进行数据监听
- // 接收三个值: 表达式, 实例对象, 回调函数
- // 表达式是用来辅助获取_data 中的数据
- // vm 是为了方便拿取其绑定的_data 中的数据
- // 回调函数使用 dealTypeFn 函数处理页面更新
- new Watcher(value, vm, function (val) {
- _self.dealTypeFn[type+'Updata'] && _self.dealTypeFn[type+'Updata'](node, val)
- })
- }
- ...
- }
新建一个 watcher.JS 文件, 创建 watcher 对象
- // watcher.JS
- function Watcher(exp, vm, cb) {
- this.exp = exp // 记录传过来的表达式
- this.vm = vm // 实例对象
- this.cb = cb // 函数回调
- this.depIds = {} // 用来存储 dep 对象, key 是 dep 的 id
- }
每一个 dep 对象中有存储当前 watcher 对象, 在 watcher 对象中有存储当前 dep 对象, 但是如何把他们串联在一起是一个问题
首先知道的是每一个数据都有 getter 函数, 在数据被获取的时候触发这个函数, 那么就从这个函数去着手解决以上问题
在 dep 类中创建一个 target 属性用来获取当前 watcher 对象, 注意, 这里 target 应该是作为 Dep 类的一个属性, 而不是实例的属性
- // observer.JS
- ...
- Dep.target = null
- ...
watcher 类新增一个 addDep 函数
- // watcher.JS
- Watcher.prototype = {
- // 添加 dep, 存储在 Watcher 对象里
- // 参数是当前数据对应的 dep 对象
- addDep(dep){
- // 如果存在就不用再添加
- if(!this.depIds.hasOwnProperty(dep.id)){
- // 调用 dep 的 addWatchs 函数添加当前 watcher 对象
- dep.addWatchs(this)
- // 存储 dep 对象到 depIds
- this.depIds[dep.id] = dep
- }
- }
- }
以上做了两件很重要的事添加 dep 和 watcher.
由于在获取某个数据的时候会去调用该数据的 getter 函数, 且应该在创建 watcher 实例的时候 (因为这个时候会对模板进行编译和解析) 就应该对 dep 和 watcher 对象都进行各自的存储, 所以 addDep 函数调用时机最好是放在 getter 函数触发的时机
- // watcher.JS
- function Watcher(exp, vm, cb) {
- ...
- // get 函数返回当前数据值
- // this.value 之后在数据更新时用来判断是否是旧值
- this.value = this.get()
- }
- Watcher.prototype = {
- get(){
- // 将 target 指向该 Watcher 对象
- Dep.target = this
- // 这里在获取 value 值时会去调用对应数据的 getter 函数
- var value = this.getDataValue()
- // 置空
- Dep.target = null
- return value
- },
- // 同 compile.JS 中的 getDataValue 一样
- getDataValue(){
- var vals = this.exp.split(".")
- var val = this.vm._data
- vals.forEach(function (key) {
- val = val[key]
- })
- return val
- }
- }
- // observer.JS
- defineReactive(data, key,val){
- ...
- Object.defineProperty(data,key,{
- enumerable: true, // 可枚举
- configurable: false, // 不能再 define
- get(){
- // 在 get 方法中添加 watche 对象到该 dep 上
- // 如果是通过 watch 触发的 get 方法就进行 watch 存储到该 dep 上
- if(Dep.target){
- // 在 watcher 那边添加 dep 对象, 以及 dep 存储当前 watcher
- // 将当前数据的 dep 对象做参数传过去
- Dep.target.addDep(dep)
- }
- return val
- },
- ...
- })
- }
以上调用顺序应该是:
创建 watcher 对象 -> 获取每一个数据 -> getter 函数中调用 addDep 函数 -> addDep 函数进行各自的对象存储
数据更新
接下来就是数据更新了, cb 回调函数还没有用, watcher 类中新增 update 函数, 然后在 Dep 类中的 notify 函数添加函数体
- // watcher.JS
- Watcher.prototype = {
- ...
- update(){
- var oldValue = this.value
- // 在_data 中获取修改后的值
- var newValue = this.get()
- // 判断是否和原值一样
- if(newValue === oldValue){
- return
- }
- // 调用更新视图, 这里需要把 this 指向改为 mvvm 对象实例
- this.cb.call(this.vm ,newValue)
- this.value = newValue
- }
- ...
- }
- // observer.JS
- Dep.prototype = {
- ...
- // 发布消息, 通知 watch 更新
- notify(){
- this.watchs.forEach(function (watch) {
- watch.update()
- })
- }
- }
这样在数据发生改变时, 就会调用该数据的 setter 函数, 然后在函数内调用 notify 函数通知更新.
v-model
很简单只需要在工具函数中加一个 model 指令解析, 然后监听 input 事件, 通过 input 事件来修改相应的数据, 然后触发 setter 更新就行了
- // compile.JS
- Compile.prototype.vUtil = {
- ...
- model: function (vm, node, value) {
- var _self = this,
- val = this.getDataValue(vm, value);
- if(value){
- node.value = val
- }
- // 绑定 input 事件
- node.addEventListener('input', function (e) {
- var newValue = e.target.value;
- if (val === newValue) {
- return;
- }
- // 修改原值, 触发 setter
- _self._setDataValue(vm, value, newValue);
- val = newValue;
- });
- },
- _setDataValue:function (vm, exp, value) {
- var val = vm._data
- exp = exp.split('.')
- exp.forEach(function (k, i) {
- // 非最后一个 key, 更新 val 的值
- if (i <exp.length - 1) {
- val = val[k]
- } else {
- val[k] = value
- }
- });
- }
- ...
- }
最后来看一下效果
- // index.html
- <div id="app">
- <div>{{name}}</div>
- <div>{{age}}</div>
- <div>{{eat.a}}</div>
- <div>{{eataab}}</div>
- <div data-as="asd" v-HTML="htmlT"></div>
- <div data-as="asd2" v-text="textT"></div>
- <div data-as="asd2" v-text="msg"></div>
- <div class="demo" v-class="classT">哈哈</div>
- <button style="width: 100px;height: 30px" @click="show">点我</button>
- <button style="width: 100px;height: 30px" @click="update">更新</button>
- <input type="text" v-model="msg">
- <div>{{msg}}</div>
- </div>
- <script src="js/compile.js"></script>
- <script src="js/watcher.js"></script>
- <script src="js/observer.js"></script>
- <script src="js/dataProxy.js"></script>
- <script>
- var vm = new Mvvmvue({
- el:'#app',
- data: {
- name: 'asd',
- age: 12,
- eat:{
- a: 1,
- b: 2
- },
- htmlT:'<a>haha</a>',
- textT: 'demodemodemo',
- classT: 'test',
- msg: '12'
- },
- methods:{
- show: function () {
- alert(this.eat.a)
- },
- update: function () {
- this.name = "1111"
- }
- }
- })
- console.log(vm)
- </script>
11.PNG
完成!!
代码
一些后话, 可看可不看
其他文章导航:
前端 MVVM 理论 - MVC 和 MVP
前端 MVVM 理论 - MVVM
前端 MVVM 实战 - 常用的几个方法和属性
前端 MVVM 实战 - 数据代理
前端 MVVM 实战 - 模板解析之双括号解析
前端 MVVM 实战 - 模板解析之事件指令和一般指令
前端 MVVM 实战 - 数据绑定(一)
前端 MVVM 实战 - 数据绑定(二)
来源: http://www.jianshu.com/p/21592a132f67