简单实现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 相关文章推荐
jQuery中的一些常见方法小结(推荐)
Jun 13 Javascript
JS基于HTML5的canvas标签实现炫目的色相球动画效果实例
Aug 24 Javascript
Angular 4 指令快速入门教程
Jun 07 Javascript
基于JavaScript实现简单的音频播放功能
Jan 07 Javascript
Nginx 配置多站点vhost 的方法
Jan 07 Javascript
vue中实现在外部调用methods的方法(推荐)
Feb 08 Javascript
vue-cli webpack 引入swiper的操作方法
Sep 15 Javascript
对VUE中的对象添加属性
Sep 18 Javascript
Vue动态路由缓存不相互影响的解决办法
Feb 19 Javascript
详解Node.js异步处理的各种写法
Jun 09 Javascript
ES6 Generator函数的应用实例分析
Jun 26 Javascript
js数组相减简单示例【删除a数组所有与b数组相同元素】
Mar 04 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
探讨PHP中OO之静态关键字以及类常量的详解
2013/06/07 PHP
Laravel中间件实现原理详解
2016/10/09 PHP
Laravel 中创建 Zip 压缩文件并提供下载的实现方法
2019/04/02 PHP
Laravel5.4框架中视图共享数据的方法详解
2019/09/05 PHP
jQuery的一些注意
2006/12/06 Javascript
Jquery判断IE6等浏览器的代码
2011/04/05 Javascript
js setTimeout 常见问题小结
2013/08/13 Javascript
button没写type=button会导致点击时提交
2014/03/06 Javascript
谷歌地图打不开的解决办法
2014/08/07 Javascript
如何动态加载外部Javascript文件
2015/12/02 Javascript
jquery submit()不能提交表单的解决方法
2017/04/24 jQuery
Node接收电子邮件的实例代码
2017/07/21 Javascript
JavaScript解析任意形式的json树型结构展示
2017/07/23 Javascript
jQuery实现IE输入框完成placeholder标签功能的方法
2017/09/20 jQuery
javascript按顺序加载运行js方法
2017/12/01 Javascript
JavaScript中的一些隐式转换和总结(推荐)
2017/12/22 Javascript
vue 子组件watch监听不到prop的解决
2020/08/09 Javascript
[05:24]TI9采访——教练
2019/08/24 DOTA
在Django同1个页面中的多表单处理详解
2017/01/25 Python
Python使用PyCrypto实现AES加密功能示例
2017/05/22 Python
Python实现的远程登录windows系统功能示例
2018/06/21 Python
python bmp转换为jpg 并删除原图的方法
2018/10/25 Python
Python StringIO如何在内存中读写str
2020/01/07 Python
python实现全排列代码(回溯、深度优先搜索)
2020/02/26 Python
解决PDF 转图片时丢文字的一种可能方式
2021/03/04 Python
HTML5 Canvas旋转动画的2个代码例子(一个旋转的太极图效果)
2014/04/10 HTML / CSS
King Apparel官网:英国街头服饰品牌
2019/09/05 全球购物
医学院毕业生自荐信
2013/11/08 职场文书
审计主管岗位职责
2014/01/31 职场文书
企业管理毕业生求职信
2014/03/11 职场文书
原料仓管员岗位职责
2015/04/01 职场文书
2015年银行工作总结范文
2015/04/01 职场文书
浪漫的婚礼主持词
2015/06/30 职场文书
2016年大学生寒假社会实践心得体会
2015/10/09 职场文书
2019职场单身人才调研报告:互联网行业单身比例最高
2019/08/07 职场文书
七个Python必备的GUI库
2021/04/27 Python