vue如何实现observer和watcher源码解析


Posted in Javascript onMarch 09, 2017

本文能帮你做什么?好奇vue双向绑定的同学,可以部分缓解好奇心,还可以帮你了解如何实现$watch。

前情回顾

我之前写了一篇没什么干货的文章,并且刨了一个大坑。
今天,打算来填一天,并再刨一个。

不过话说说回来了,看本文之前,如果不知道Object.defineProperty,还必须看看解析神奇的Object.defineProperty
不得不感慨vue的作者,人长得帅,码写的也好,本文是根据作者源码,摘取出来的

本文将实现什么

正如上一篇许下的承诺一样,本文要实现一个$wacth

const v = new Vue({
 data:{
 a:1,
 b:2
 }
})
v.$watch("a",()=>console.log("哈哈,$watch成功"))
setTimeout(()=>{
 v.a = 5
},2000) //打印 哈哈,$watch成功

为了帮助大家理清思路。。我们就做最简单的实现。。只考虑对象不考虑数组

1. 实现 observer

思路:我们知道Object.defineProperty的特性了,我们就利用它的set和get。我们将要observe的对象,通过递归,将它所有的属性,包括子属性的属性,都给加上set和get。这样的话,给这个对象的某个属性赋值,就会触发set。开始吧

export default class Observer{
 constructor(value) {
 this.value = value
 this.walk(value)
 }
 //递归。。让每个字属性可以observe
 walk(value){
 Object.keys(value).forEach(key=>this.convert(key,value[key]))
 }
 convert(key, val){
 defineReactive(this.value, key, val)
 }
}


export function defineReactive (obj, key, val) {
 var childOb = observe(val)
 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: ()=>val,
 set:newVal=> { 
 childOb = observe(newVal)//如果新赋值的值是个复杂类型。再递归它,加上set/get。。
 }
 })
}


export function observe (value, vm) {
 if (!value || typeof value !== 'object') {
 return
 }
 return new Observer(value)
}

代码很简单,就给每个属性(包括子属性)都加上get/set,这样的话,这个对象的,有任何赋值,就会触发set方法。。
所以,我们是不是应该写一个消息-订阅器呢?

这样的话,一触发set方法,我们就发一个通知出来,然后,订阅这个消息的,就会怎样?对咯。、收到消息、触发回调。

2. 消息-订阅器

很简单,我们维护一个数组,,这个数组,就放订阅着,一旦触发notify,订阅者就调用自己的update方法

export default class Dep {
 constructor() {
 this.subs = []
 }
 addSub(sub){
 this.subs.push(sub)
 }
 notify(){
 this.subs.forEach(sub=>sub.update())
 }
}

所以,每次set函数,调用的时候,我们是不是应该,触发notify,对吧。所以我们把代码补充完整

export function defineReactive (obj, key, val) {
 var dep = new Dep()
 var childOb = observe(val)
 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: ()=>val,
 set:newVal=> {
  var value = val
  if (newVal === value) {
  return
  }
  val = newVal
  childOb = observe(newVal)
  dep.notify()
 }
 })
 }

那么问题来了。谁是订阅者。对,是Watcher。一旦 dep.notify()就遍历订阅者,也就是Watcher,并调用他的update()方法

3. 实现一个Watcher
我们想象这个Watcher,应该用什么东西。update方法,嗯这个毋庸置疑,还有呢。

v.$watch("a",()=>console.log("哈哈,$watch成功"))

对表达式(就是那个“a”) 和 回调函数,这是最基本的,所以我们简单写写

export default class Watcher {
 constructor(vm, expOrFn, cb) {
 this.cb = cb
 this.vm = vm
 //此处简化.要区分fuction还是expression,只考虑最简单的expression
 this.expOrFn = expOrFn
 this.value = this.get()
 }
 update(){
 this.run()
 }
 run(){
 const value = this.get()
 if(value !==this.value){
 this.value = value
 this.cb.call(this.vm)
 }
 }
 get(){
 //此处简化。。要区分fuction还是expression
 const value = this.vm._data[this.expOrFn]
 return value
 }
}

那么问题来了,我们怎样将通过addSub(),Watcher加进去呢。
我们发现var dep = new Dep() 处于闭包当中,我们又发现Watcher的构造函数里会调用this.get,所以,我们可以在上面动动手脚,修改一下Object.defineProperty的get要调用的函数,判断是不是Watcher的构造函数调用,如果是,说明他就是这个属性的订阅者,果断将他addSub()中去,那问题来了?
我怎样判断他是Watcher的this.get调用的,而不是我们普通调用的呢

对,在Dep定义一个全局唯一的变量,跟着思路我们写一下

export default class Watcher {
 ....省略未改动代码....
 get(){
 Dep.target = this
 //此处简化。。要区分fuction还是expression
 const value = this.vm._data[this.expOrFn]
 Dep.target = null
 return value
 }
}

这样的话,我们只需要在Object.defineProperty的get要调用的函数里,判断有没有值,就知道到底是Watcher 在get,还是我们自己在查看赋值,如果是Watcher的话就addSub(),代码补充一下

export function defineReactive (obj, key, val) {
 var dep = new Dep()
 var childOb = observe(val)

 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: ()=>{
 // 说明这是watch 引起的
 if(Dep.target){
 dep.addSub(Dep.target)
 }
 return val
 },
 set:newVal=> {
 var value = val
 if (newVal === value) {
 return
 }
 val = newVal
 childOb = observe(newVal)
 dep.notify()
 }
 })
}

最后不要忘记,在Dep.js中加上这么一句

Dep.target = null

4. 实现一个 Vue

还差一步就大功告成了,我们要把以上代码配合Vue的$watch方法来用,要watch Vue实例的属性,算了,不要理会我在说什么,直接看代码吧

import Watcher from '../watcher'
import {observe} from "../observer"

export default class Vue {
 constructor (options={}) {
 //这里简化了。。其实要merge
 this.$options=options
 //这里简化了。。其实要区分的
 let data = this._data=this.$options.data
 Object.keys(data).forEach(key=>this._proxy(key))
 observe(data,this)
 }


 $watch(expOrFn, cb, options){
 new Watcher(this, expOrFn, cb)
 }

 _proxy(key) {
 var self = this
 Object.defineProperty(self, key, {
 configurable: true,
 enumerable: true,
 get: function proxyGetter () {
 return self._data[key]
 },
 set: function proxySetter (val) {
 self._data[key] = val
 }
 })
 }
}

非常简单。两件事,observe自己的data,代理自己的data,使访问自己的属性,就是访问子data的属性。。
截止到现在,在我们只考虑最简单情况下,整个流程终于跑通了。肯定会有很多bug,本文主要目的是展示整个工作流,帮助读者理解。
代码在https://github.com/georgebbbb...,

我是一万个不想展示自己代码,因为很多槽点,还请见谅

下一篇,有两个方向,将聊一聊如何实现双向绑定,或者是如何watch数组。

关于vue2.0的新文章

本文已被整理到了《Vue.js前端组件学习教程》,欢迎大家学习阅读。

关于vue.js组件的教程,请大家点击专题vue.js组件学习教程进行学习。

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

Javascript 相关文章推荐
JavaScript 使用技巧精萃(.net html
Apr 25 Javascript
JSON 学习之JSON in JavaScript详细使用说明
Feb 23 Javascript
AngularJs bootstrap详解及示例代码
Sep 01 Javascript
JS仿hao123导航页面图片轮播效果
Sep 01 Javascript
详解Angular.js中$http拦截器的介绍及使用
Jul 04 Javascript
Angular如何在应用初始化时运行代码详解
Jun 11 Javascript
Vue动态控制input的disabled属性的方法
Jun 26 Javascript
Node.js实现一个HTTP服务器的方法示例
May 13 Javascript
js使用cookie实现记住用户名功能示例
Jun 13 Javascript
layer插件实现在弹出层中弹出一警告提示并关闭弹出层的方法
Sep 24 Javascript
vuex 多模块时 模块内部的mutation和action的调用方式
Jul 24 Javascript
微信小程序实现底部弹出模态框
Nov 18 Javascript
详解VueJs异步动态加载块
Mar 09 #Javascript
微信小程序 设置启动页面的两种方法
Mar 09 #Javascript
js实现登录框鼠标拖拽效果
Mar 09 #Javascript
解决Node.js使用MySQL出现connect ECONNREFUSED 127.0.0.1:3306的问题
Mar 09 #Javascript
基于vue实现分页/翻页组件paginator示例
Mar 09 #Javascript
vue.js利用Object.defineProperty实现双向绑定
Mar 09 #Javascript
javascript遍历json对象的key和任意js对象属性实例
Mar 09 #Javascript
You might like
thinkPHP5.0框架自动加载机制分析
2017/03/18 PHP
Laravel利用gulp如何构建前端资源详解
2018/06/03 PHP
laravel实现Auth认证,登录、注册后的页面回跳方法
2019/09/30 PHP
Jquery 动态添加按钮实现代码
2010/05/06 Javascript
JavaScript使用cookie记录临时访客信息的方法
2015/04/07 Javascript
javascript函数特点实例分析
2015/05/14 Javascript
对于jQuery性能的一些优化建议
2015/08/13 Javascript
js实现超酷的照片墙展示效果图附源码下载
2015/10/08 Javascript
jQuery中的Deferred和promise 的区别
2016/04/03 Javascript
基于jQuery插件实现点击小图显示大图效果
2016/05/11 Javascript
js实现文本上下来回滚动
2017/02/03 Javascript
详解bootstrap用dropdown-menu实现上下文菜单
2017/09/22 Javascript
mint-ui在vue中的使用示例
2018/04/05 Javascript
Vue列表渲染的示例代码
2018/11/01 Javascript
nvm、nrm、npm 安装和使用详解(小结)
2019/01/17 Javascript
深入解析koa之中间件流程控制
2019/06/17 Javascript
如何在Vue中抽离接口配置文件
2019/10/31 Javascript
Javascript柯里化实现原理及作用解析
2020/10/22 Javascript
python时间日期函数与利用pandas进行时间序列处理详解
2018/03/13 Python
python中copy()与deepcopy()的区别小结
2018/08/03 Python
PyQt5实现简易计算器
2020/05/30 Python
PyCharm中如何直接使用Anaconda已安装的库
2020/05/28 Python
python识别验证码的思路及解决方案
2020/09/13 Python
python3.8.3安装教程及环境配置的详细教程(64-bit)
2020/11/28 Python
requests在python中发送请求的实例讲解
2021/02/17 Python
英国著名的美容护肤和护发产品购物网站:Lookfantastic
2020/11/23 全球购物
mysql的最长数据库名,表名,字段名可以是多长
2014/04/21 面试题
软件测试面试题
2014/01/05 面试题
大学毕业生求职自荐信
2014/02/20 职场文书
护理目标管理责任书
2014/07/25 职场文书
大学新生军训自我鉴定
2014/09/18 职场文书
教师考核表个人总结
2015/02/12 职场文书
解决Pytorch半精度浮点型网络训练的问题
2021/05/24 Python
pandas中DataFrame重置索引的几种方法
2021/05/24 Python
mysql如何查询连续记录
2022/05/11 MySQL
Java Spring Boot请求方式与请求映射过程分析
2022/06/25 Java/Android