简单实现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 相关文章推荐
js读写(删除)Cookie实例详解
Apr 17 Javascript
javascript预加载图片、css、js的方法示例介绍
Oct 14 Javascript
json字符串之间的相互转换示例代码
Aug 21 Javascript
JavaScript实现Java中StringBuffer的方法
Feb 09 Javascript
JavaScript事件代理和委托详解
Apr 08 Javascript
jQuery模拟实现天猫购物车动画效果实例代码
May 25 jQuery
vue2中使用sass并配置全局的sass样式变量的方法
Sep 04 Javascript
微信小程序webview实现长按点击识别二维码功能示例
Jan 24 Javascript
初试vue-cli使用HBuilderx打包app的坑
Jul 17 Javascript
js 下拉菜单点击旁边收起实现(踩坑记)
Sep 29 Javascript
微信小程序实现发微博功能的示例代码
Jun 24 Javascript
浅谈Vue使用Elementui修改默认的最快方法
Dec 05 Vue.js
使用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
在普通HTTP上安全地传输密码
2007/07/21 PHP
腾讯QQ php程序员面试题目整理
2010/06/08 PHP
PHP采用XML-RPC构造Web Service实例教程
2014/07/16 PHP
浅谈PHP中output_buffering
2015/07/13 PHP
Zend Framework教程之分发器Zend_Controller_Dispatcher用法详解
2016/03/07 PHP
PHP实现二维数组根据key进行排序的方法
2016/12/30 PHP
Docker搭建自己的PHP开发环境
2018/02/24 PHP
PHP实现数组转JSon和JSon转数组的方法示例
2018/06/14 PHP
PHP 结合 Boostrap 结合 js 实现学生列表删除编辑及搜索功能
2019/05/21 PHP
javascript阻止scroll事件多次执行的思路及实现
2013/11/08 Javascript
几种设置表单元素中文本输入框不可编辑的方法总结
2013/11/25 Javascript
node.js入门学习之url模块
2017/02/25 Javascript
jquery加载单文件vue组件的方法
2017/06/20 jQuery
vue中使用localstorage来存储页面信息
2017/11/04 Javascript
JavaScript函数节流和函数去抖知识点学习
2018/07/31 Javascript
vue使用better-scroll实现下拉刷新、上拉加载
2018/11/23 Javascript
node读写Excel操作实例分析
2019/11/06 Javascript
js实现双人五子棋小游戏
2020/05/28 Javascript
Python  __getattr__与__setattr__使用方法
2008/09/06 Python
Django ManyToManyField 跨越中间表查询的方法
2018/12/18 Python
浅谈Python接口对json串的处理方法
2018/12/19 Python
python实现淘宝秒杀脚本
2020/06/23 Python
python实现飞机大战小游戏
2019/11/08 Python
Python利用matplotlib绘制约数个数统计图示例
2019/11/26 Python
Python监控服务器实用工具psutil使用解析
2019/12/19 Python
Pytorch之contiguous的用法
2019/12/31 Python
python让函数不返回结果的方法
2020/06/22 Python
python实现测试工具(二)——简单的ui测试工具
2020/10/19 Python
基于python+selenium自动健康打卡的实现代码
2021/01/13 Python
matplotlib绘制正余弦曲线图的实现
2021/02/22 Python
英国儿童设计师服装的领先零售商:Base
2019/03/17 全球购物
安全生产目标责任书
2014/04/14 职场文书
副检察长四风问题对照检查材料思想汇报
2014/10/07 职场文书
领导干部个人整改措施落实情况汇报
2014/10/29 职场文书
2014年学校安全工作总结
2014/11/13 职场文书
Python创建SQL数据库流程逐步讲解
2022/09/23 Python