简单实现Vue的observer和watcher


Posted in Javascript onDecember 21, 2016

非庖丁瞎解牛系列~ =。=

在日常项目开发的时候,我们将js对象传给vue实例中的data选项,来作为其更新视图的基础,事实上是vue将会遍历它的属性,用Object.defineProperty 设置它们的 get/set,从而让 data 的属性能够响应数据变化:

Object.defineProperty(obj, name, {
 // 获取值的时候先置入vm的_data属性对象中
 get() {
  // 赋值的时候显示的特性
 },
 set() {
  // 值变化的时候可以做点什么
 }
 })

接下来可以利用其实现一个最简单的watcher.既然要绑定数据执行回调函数,data属性和callback属性是少不了的,我们定义一个vm对象(vue中vm对象作为根实例,是全局的):

/**
 * @param {Object} _data 用于存放data值
 * @param {Object} $data data原始数据对象,当前值
 * @param {Object} callback 回调函数
 */
var vm = { _data: {}, $data: {}, callback: {} }

在设置值的时候,如果检测到当前值与存储在_data中的对应值发生变化,则将值更新,并执行回调函数,利用Object.definedProperty方法中的get() & set() 我们很快就可以实现这个功能~

vm.$watch = (obj, func) => {
 // 回调函数
 vm.callback[ obj ] = func
 // 设置data
 Object.defineProperty(vm.$data, obj, {
  // 获取值的时候先置入vm的_data属性对象中
  get() {
  return vm._data[ obj ]
  },
  set(val) {
  // 比较原值,不相等则赋值,执行回调
  if (val !== vm._data[ obj ]) {
   vm._data[ obj ] = val
   const cb = vm.callback[ obj ]
   cb.call(vm)
  }
  }
 })
}
vm.$watch('va', () => {console.log('已经成功被监听啦')})
vm.$data.va = 1

虽然初步实现了这个小功能,那么问题来了,obj对象如果只是一个简单的值为值类型的变量,那以上代码完全可以满足;但是如果obj是一个具有一层甚至多层树结构对象变量,我们就只能监听到最外层也就是obj本身的变化,内部属性变化无法被监听(没有设置给对应属性设置set和get),因为对象自身内部属性层数未知,理论上可以无限层(一般不会这么做),所以此处还是用递归解决吧~

咱们先将Object.defineProperty函数剥离,一是解耦,二是方便我们递归~

var defineReactive = (obj, key) => {
 Object.defineProperty(obj, key, {
 get() {
  return vm._data[key]
 },
 set(newVal) {
  if (vm._data[key] === newVal) {
  return
  }
  vm._data[key] = newVal
  const cb = vm.callback[ obj ]
  cb.call(vm)
 }
 })
}

咦,说好的递归呢,不着急,上面只是抽离了加get和set功能的函数,
现在我们加入递归~

var Observer = (obj) => {
 // 遍历,让对象中的每个属性可以加上get set
 Object.keys(obj).forEach((key) =>{
 defineReactive(obj, key)
 })
}

这里仅仅只是遍历,要达到递归,则需要在defineReactive的时候再加上判断,判断这个属性是否为object类型,如果是,则执行Observer自身~我们改写下defineReactive函数

// 判断是否为object类型,是就继续执行自身
var observe = (value) => {
 // 判断是否为object类型,是就继续执行Observer
 if (!value || typeof value !== 'object') {
 return
 }
 return new Observer(value)
}

// 将observe方法置入defineReactive中Object.defineProperty的set中,形成递归
var defineReactive = (obj, key) => {
 // 判断val是否为对象,如果对象有很多层属性,则这边的代码会不断调用自身(因为observe又执行了Observer,而Observer执行defineReactive),一直到最后一层,从最后一层开始执行下列代码,层层返回(可以理解为洋葱模型),直到最前面一层,给所有属性加上get/set
 var childObj = observe(vm._data[key])
 Object.defineProperty(obj, key, {
 get() {
  return vm._data[key]
 },
 set(newVal) {
  // 如果设置的值完全相等则什么也不做
  if (vm._data[key] === newVal) {
   return
  }
  // 不相等则赋值
  vm._data[key] = newVal
  // 执行回调
  const cb = vm.callback[ key ]
  cb.call(vm)
  // 如果set进来的值为复杂类型,再递归它,加上set/get
  childObj = observe(val)
 }
 })
}

现在我们来整理下,把我们刚开始实现的功能雏形进行进化

var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key) => {
 // 一开始的时候是不设值的,所以,要在外面做一套observe
 // 判断val是否为对象,如果对象有很多层属性,则这边的代码会不断调用自身(因为observe又执行了Observer,而Observer执行defineReactive),一直到最后一层,从最后一层开始执行下列代码,层层返回(可以理解为洋葱模型),直到最前面一层,给所有属性加上get/set
 var childObj = observe(vm._data[key])
 Object.defineProperty(obj, key, {
 get() {
  return vm._data[key]
 },
 set(newVal) {
  if (vm._data[key] === newVal) {
  return
  }
 // 如果值有变化的话,做一些操作
 vm._data[key] = newVal
 // 执行回调
 const cb = vm.callback[ key ]
 cb.call(vm)
 // 如果set进来的值为复杂类型,再递归它,加上set/get
 childObj = observe(newVal)
 }
 })
}
var Observer = (obj) => {
 Object.keys(obj).forEach((key) =>{
 defineReactive(obj, key)
 })
}
var observe = (value) => {
 // 判断是否为object类型,是就继续执行Observer
 if (!value || typeof value !== 'object') {
 return
 }
 Observer(value)
}
vm.$watch = (name, func) => {
 // 回调函数
 vm.callback[name] = func
 // 设置data
 defineReactive(vm.$data, name)
}
// 绑定a,a若变化则执行回调方法
var va = {a:{c: 'c'}, b:{c: 'c'}}
vm._data[va] = {a:{c: 'c'}, b:{c: 'c'}}
vm.$watch('va', () => {console.log('已经成功被监听啦')})
vm.$data.va = 1

在谷歌浏览器的console中粘贴以上代码,然后回车发现,结果不出所料,va本身被监听了,可以,我们试试va的内部属性有没有被监听,改下vm.data.va=1为vm.data.va.a = 1,结果发现报错了

什么鬼?

我们又仔细检查了代码,WTF,原来我们在递归的时候,Object.defineProperty中的回调函数cb的key参数一直在发生变化,我们希望的是里面的属性变化的时候执行的是我们事先定义好的回调函数~那么我们来改下方法,将一开始定义好的回调作为参数传进去,确保每一层递归set的回调都是我们事先设置好的~

var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key, cb) => {
 // 一开始的时候是不设值的,所以,要在外面做一套observe
 var childObj = observe(vm._data[key], cb)
 Object.defineProperty(obj, key, {
 get() {
  return vm._data[key]
 },
 set(newVal) {
  if (vm._data[key] === newVal) {
  return
  }
  // 如果值有变化的话,做一些操作
  vm._data[key] = newVal
  // 执行回调
  cb()
  // 如果set进来的值为复杂类型,再递归它,加上set/get
  childObj = observe(newVal)
 }
 })
}
var Observer = (obj, cb) => {
 Object.keys(obj).forEach((key) =>{
 defineReactive(obj, key, cb)
 })
}
var observe = (value, cb) => {
 // 判断是否为object类型,是就继续执行Observer
 if (!value || typeof value !== 'object') {
 return
 }
 Observer(value, cb)
}
vm.$watch = (name, func) => {
 // 回调函数
 vm.callback[name] = func
 // 设置data
 defineReactive(vm.$data, name, func)
}
// 绑定a,a若变化则执行回调方法
var va = {a:{c: 'c'}, b:{c: 'c'}}
vm._data.va = {a:{c: 'c'}, b:{c: 'c'}}
vm.$watch('va', () => {console.log('又成功被监听啦')})
vm.$data.va.a = 1

再执行一次以上代码,发现内部的a属性也被监听到了,而且属性值变化的时候执行了我们事先定义好的回调函数~嘻嘻嘻~

虽然实现了$watch的基本功能,但是和vue的源码还是有一定的距离,特别是一些扁平化和模块化的思想需要涉及到一些设计模式,其实我们在看源码的时候,常常是逆着作者的思维走的,功能从简单到复杂往往涉及到代码的模块化和解耦,使得代码非常地分散,读起来晦涩难懂,自己动手,从小功能代码块实现,然后结合源码,对比思路,慢慢丰富,也不失为一种学习源码的方式~

下一篇将会结合源码来浅谈下vue的watcher和observer

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
设置jsf的选择框h:selectOneMenu为不可编辑状态的方法
Jan 07 Javascript
javascript 模拟坦克大战游戏(html5版)附源码下载
Apr 08 Javascript
jQuery获取当前点击的对象元素(实现代码)
May 19 Javascript
jQuery旋转插件jqueryrotate用法详解
Oct 13 Javascript
前端JS面试中常见的算法问题总结
Dec 23 Javascript
浅谈DOM的操作以及性能优化问题-重绘重排
Jan 08 Javascript
将angular-ui的分页组件封装成指令的方法详解
May 10 Javascript
JavaScript之Canvas_动力节点Java学院整理
Jul 04 Javascript
微信小程序 input输入及动态设置按钮的实现
Oct 27 Javascript
利用JQUERY实现多个AJAX请求等待的实例
Dec 14 jQuery
JS 验证码功能的三种实现方式
Nov 26 Javascript
判断文字超过2行添加展开按钮,未超过则不显示,溢出部分显示省略号
Apr 28 Javascript
使用BootStrap建立响应式网页——通栏轮播图(carousel)
Dec 21 #Javascript
js实现开启密码大写提示
Dec 21 #Javascript
js实现的在线调色板功能完整实例
Dec 21 #Javascript
Bootstrap 模态框(Modal)插件代码解析
Dec 21 #Javascript
清除输入框内的空格
Dec 21 #Javascript
利用BootStrap的Carousel.js实现轮播图动画效果
Dec 21 #Javascript
jQuery延迟执行的实现方法
Dec 21 #Javascript
You might like
Win2003服务器安全加固设置--进一步提高服务器安全性
2007/05/23 PHP
destoon实现公司新闻详细页添加评论功能的方法
2014/07/15 PHP
destoon首页调用求购供应信息的地区名称的方法
2014/08/21 PHP
Yii框架获取当前controlle和action对应id的方法
2014/12/03 PHP
PHP与SQL语句常用大全
2016/12/10 PHP
微博@符号的用户名提示效果。(想@到谁?)
2010/11/05 Javascript
Javascript前端UI框架Kit使用指南之kitjs的对话框组件
2014/11/28 Javascript
javascript消除window.close()的提示窗口
2015/05/20 Javascript
Bootstrap表单布局样式代码
2016/05/31 Javascript
微信小程序 wx.uploadFile在安卓手机上面the same task is working问题解决
2016/12/14 Javascript
vue实现列表的添加点击
2016/12/29 Javascript
angular实现form验证实例代码
2017/01/17 Javascript
jQuery+CSS3实现点赞功能
2017/03/13 Javascript
如何让你的JS代码更好看易读
2017/12/01 Javascript
使用classList来实现两个按钮样式的切换方法
2018/01/24 Javascript
[07:55]2014DOTA2 TI正赛第三日 VG上演推进荣耀DKEG告别
2014/07/21 DOTA
[15:07]lgd_OG_m2_BP
2019/09/10 DOTA
Python封装shell命令实例分析
2015/05/05 Python
Python新手们容易犯的几个错误总结
2017/04/01 Python
使用paramiko远程执行命令、下发文件的实例
2017/10/01 Python
50行Python代码实现人脸检测功能
2018/01/23 Python
python 读取文本文件的行数据,文件.splitlines()的方法
2018/07/12 Python
django中SMTP发送邮件配置详解
2019/07/19 Python
python flask几分钟实现web服务的例子
2019/07/26 Python
Python标准库shutil模块使用方法解析
2020/03/10 Python
Python matplotlib读取excel数据并用for循环画多个子图subplot操作
2020/07/14 Python
结合 CSS3 transition transform 实现简单的跑马灯效果的示例
2018/02/07 HTML / CSS
HTML5网页录音和上传到服务器支持PC、Android,支持IOS微信功能
2019/04/26 HTML / CSS
土耳其玩具商店:Toyzz Shop
2019/08/02 全球购物
Tessabit日本:集世界奢侈品和设计师品牌的意大利精品买手店
2020/01/07 全球购物
一个J2EE项目团队的主要人员组成是什么
2012/06/04 面试题
内业资料员岗位职责
2014/01/04 职场文书
应用心理学专业求职信
2014/08/04 职场文书
2016年重阳节慰问信
2015/12/01 职场文书
利用ajax+php实现商品价格计算
2021/03/31 PHP
Golang 入门 之url 包
2022/05/04 Golang