简单实现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 相关文章推荐
JScript中的undefined和"undefined"的区别
Mar 08 Javascript
类似CSDN图片切换效果脚本
Sep 17 Javascript
javascript实现上传图片并预览的效果实现代码
Apr 11 Javascript
Extjs grid添加一个图片状态或者按钮的方法
Apr 03 Javascript
javascript之with的使用(阿里云、淘宝使用代码分析)
Oct 11 Javascript
Vue.js组件tree实现省市多级联动
Dec 02 Javascript
js分页之前端代码实现和请求处理
Aug 04 Javascript
详解Vue-Cli 异步加载数据的一些注意点
Aug 12 Javascript
mint-ui在vue中的使用示例
Apr 05 Javascript
vue写一个组件
Apr 09 Javascript
vue中如何实现后台管理系统的权限控制的方法示例
Sep 19 Javascript
Node.js实现简单的爬取的示例代码
Jun 25 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
第1次亲密接触PHP5(1)
2006/10/09 PHP
如何使用PHP获取网络上文件
2006/10/09 PHP
PHP产生不重复随机数的5个方法总结
2014/11/12 PHP
php post大量数据时发现数据丢失问题解决方法
2015/06/20 PHP
javascript真的不难-回顾一下基础知识
2013/01/15 Javascript
js二维数组定义和初始化的三种方法总结
2014/03/03 Javascript
javascript学习笔记_浅谈基础语法,类型,变量
2016/09/19 Javascript
微信小程序 简单DEMO布局,逻辑,样式的练习
2016/11/30 Javascript
jfinal与bootstrap的登出实战详解
2017/11/27 Javascript
vue 根据数组中某一项的值进行排序的方法
2018/08/30 Javascript
Vuerouter的beforeEach与afterEach钩子函数的区别
2018/12/26 Javascript
vue实现淘宝购物车功能
2020/04/20 Javascript
微信小程序实现多张图片上传功能
2020/11/18 Javascript
js面向对象封装级联下拉菜单列表的实现步骤
2021/02/08 Javascript
Flask入门教程实例:搭建一个静态博客
2015/03/27 Python
python+pandas分析nginx日志的实例
2018/04/28 Python
使用NumPy和pandas对CSV文件进行写操作的实例
2018/06/14 Python
Python对象属性自动更新操作示例
2018/06/15 Python
Python使用matplotlib绘制三维参数曲线操作示例
2019/09/10 Python
python将邻接矩阵输出成图的实现
2019/11/21 Python
TensorFlow实现自定义Op方式
2020/02/04 Python
python进行参数传递的方法
2020/05/12 Python
学生如何注册Pycharm专业版以及pycharm的安装
2020/09/24 Python
Python 用__new__方法实现单例的操作
2020/12/11 Python
浅析Python打包时包含静态文件处理方法
2021/01/15 Python
LN-CC美国:伦敦时尚生活的缩影
2019/02/19 全球购物
大学四年个人自我小结
2014/03/05 职场文书
预备党员2014全国两会学习心得体会
2014/03/10 职场文书
大学社团活动总结
2014/04/26 职场文书
2014年语文教研组工作总结
2014/12/06 职场文书
特岗教师个人总结
2015/02/10 职场文书
应届毕业生自荐信
2015/03/04 职场文书
2019各种承诺书范文
2019/06/24 职场文书
css实现文章分割线样式的多种方法总结
2021/04/21 HTML / CSS
Vue+TypeScript中处理computed方式
2022/04/02 Vue.js
使用 Koa + TS + ESLlint 搭建node服务器的过程详解
2022/05/30 NodeJs