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 相关文章推荐
Sample script that displays all of the users in a given SQL Server DB
Jun 16 Javascript
网络图片延迟加载实现代码 超越jquery控件
Mar 27 Javascript
IE6已终止操作问题的2种情况及解决
Apr 23 Javascript
使用时间戳解决ie缓存的问题
Aug 20 Javascript
Mvc提交表单的四种方法全程详解
Aug 10 Javascript
网络传输协议(http协议)
Nov 18 Javascript
JS仿QQ好友列表展开、收缩功能(第一篇)
Jul 07 Javascript
webstorm添加*.vue文件支持
May 08 Javascript
详解Axios 如何取消已发送的请求
Oct 20 Javascript
Vue.js项目实战之多语种网站的功能实现(租车)
Aug 07 Javascript
layui前端时间戳转化实例
Nov 15 Javascript
JavaScript如何使用插值实现图像渐变
Jun 28 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
Oracle 常见问题解答
2006/10/09 PHP
杏林同学录(八)
2006/10/09 PHP
php数据结构与算法(PHP描述) 查找与二分法查找
2012/06/21 PHP
PHP get_html_translation_table()函数用法讲解
2019/02/16 PHP
PHP解决高并发的优化方案实例
2020/12/10 PHP
js+css在交互上的应用
2010/07/18 Javascript
初识JQuery 实例一(first)
2011/03/16 Javascript
25个优雅的jQuery Tooltip插件推荐
2011/05/25 Javascript
JS案例分享之金额小写转大写
2014/05/15 Javascript
在css加载完毕后自动判断页面是否加入css或js文件
2014/09/10 Javascript
JavaScript取得键盘按下方向键是哪个的方法
2015/08/04 Javascript
jQuery EasyUI中的日期控件DateBox修改方法
2016/11/09 Javascript
JavaScript使用链式方法封装jQuery中CSS()方法示例
2017/04/07 jQuery
基于javaScript的this指向总结
2017/07/22 Javascript
vue+webpack实现异步加载三种用法示例详解
2018/04/24 Javascript
python从ftp下载数据保存实例
2013/11/20 Python
十个Python程序员易犯的错误
2015/12/15 Python
django 控制页面跳转的例子
2019/08/06 Python
在django中实现页面倒数几秒后自动跳转的例子
2019/08/16 Python
解决Tensorflow sess.run导致的内存溢出问题
2020/02/05 Python
Tensorflow 定义变量,函数,数值计算等名字的更新方式
2020/02/10 Python
Python抓包并解析json爬虫的完整实例代码
2020/11/03 Python
CSS3点击按钮实现背景渐变动画效果
2016/10/19 HTML / CSS
Footshop法国:购买运动鞋
2020/01/19 全球购物
英国最大的在线照明商店:Litecraft
2020/08/31 全球购物
什么是方法的重载
2013/06/24 面试题
酒店管理求职信
2014/06/09 职场文书
二人合伙经营协议书
2014/09/13 职场文书
教师自我剖析材料范文
2014/09/30 职场文书
中学生旷课检讨书模板
2014/10/08 职场文书
镇党委书记群众路线整改措施思想汇报
2014/10/13 职场文书
以权谋私检举信范文
2015/03/02 职场文书
一般纳税人申请报告
2015/05/18 职场文书
吧主申请感言怎么写
2015/08/03 职场文书
Django对接elasticsearch实现全文检索的示例代码
2021/08/02 Python
Python实现Matplotlib,Seaborn动态数据图
2022/05/06 Python