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 相关文章推荐
表格单元格交错着色实现思路及代码
Apr 01 Javascript
利用cookie记住背景颜色示例代码
Nov 04 Javascript
javascript 实现 原路返回
Jan 21 Javascript
JavaScript中的闭包
Feb 24 Javascript
JQuery组件基于Bootstrap的DropDownList(完整版)
Jul 05 Javascript
JS实现随机颜色的3种方法与颜色格式的转化
Jan 05 Javascript
Vue常用指令V-model用法
Mar 08 Javascript
js判断用户是输入的地址请求的路径(实例讲解)
Jul 18 Javascript
详解create-react-app 自定义 eslint 配置
Jun 07 Javascript
深入浅析js原型链和vue构造函数
Oct 25 Javascript
解决VUE自定义拖拽指令时 onmouseup 与 click事件冲突问题
Jul 24 Javascript
原生js实现自定义滚动条组件
Jan 20 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
PHP4实际应用经验篇(5)
2006/10/09 PHP
SMARTY学习手记
2007/01/04 PHP
php支付宝APP支付功能
2020/07/29 PHP
JavaScript 创建对象和构造类实现代码
2009/07/30 Javascript
jQuery的Ajax的自动完成功能控件简要说明
2013/02/22 Javascript
js 实现 input type="file" 文件上传示例代码
2013/08/07 Javascript
js图片处理示例代码
2014/05/12 Javascript
教你在heroku云平台上部署Node.js应用
2014/07/30 Javascript
7个让JavaScript变得更好的注意事项
2015/01/28 Javascript
Jquery promise实现一张一张加载图片
2015/11/13 Javascript
jQuery解析json格式数据简单实例
2016/01/22 Javascript
Node.js的项目构建工具Grunt的安装与配置教程
2016/05/12 Javascript
JS实现太极旋转思路分析
2016/12/09 Javascript
Node.js学习之TCP/IP数据通讯(实例讲解)
2017/10/11 Javascript
解决vue-router中的query动态传参问题
2018/03/20 Javascript
Vue3+elementui plus创建项目的方法
2020/12/01 Vue.js
使用python调用浏览器并打开一个网址的例子
2014/06/05 Python
python制作websocket服务器实例分享
2016/11/20 Python
对Python中数组的几种使用方法总结
2018/06/28 Python
浅析python3中的os.path.dirname(__file__)的使用
2018/08/30 Python
python制作mysql数据迁移脚本
2019/01/01 Python
Python中的list与tuple集合区别解析
2019/10/12 Python
python 消费 kafka 数据教程
2019/12/21 Python
django在开发中取消外键约束的实现
2020/05/20 Python
英国领先的体验日提供商:Buyagift
2019/04/19 全球购物
汇智创新科技发展有限公司
2015/12/06 面试题
保险专业自荐信范文
2014/02/20 职场文书
母亲节感恩活动记录
2014/03/16 职场文书
趣味运动会策划方案
2014/06/02 职场文书
运动会广播稿100字
2015/08/19 职场文书
党员读书活动心得体会
2016/01/14 职场文书
应届毕业生的自我评价
2019/06/21 职场文书
MySQL库表名大小写的选择
2021/06/05 MySQL
一篇文章学会Vue中间件管道
2021/06/20 Vue.js
关于python pygame游戏进行声音添加的技巧
2021/10/24 Python
Netty客户端接入流程NioSocketChannel创建解析
2022/03/25 Java/Android