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初学困境—js初学
Dec 29 Javascript
js 操作css实现代码
Jun 11 Javascript
jQuery阻止同类型事件小结
Apr 19 Javascript
javascript 树形导航菜单实例代码
Aug 13 Javascript
浅谈Javascript中匀速运动的停止条件
Dec 19 Javascript
JS实现超精简响应鼠标显示二级菜单代码
Sep 12 Javascript
谈谈javascript中使用连等赋值操作带来的问题
Nov 26 Javascript
JavaScript SHA512加密算法详细代码
Oct 06 Javascript
jQuery-mobile事件监听与用法详解
Nov 23 Javascript
基于bootstrap的选择框插件icheck
Dec 23 Javascript
JavaScript使用prototype属性实现继承操作示例
May 22 Javascript
vue+elementui通用弹窗的实现(新增+编辑)
Jan 07 Vue.js
详解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
使用TinyButStrong模板引擎来做WEB开发
2007/03/16 PHP
snoopy PHP版的网络客户端提供本地下载
2008/04/15 PHP
php判断数组元素中是否存在某个字符串的方法
2014/06/14 PHP
如何使用php实现评委评分器
2015/07/31 PHP
php视频拍照上传头像功能实现代码分享
2015/10/08 PHP
PHP的PDO事务与自动提交
2019/01/24 PHP
浅谈php使用curl模拟多线程发送请求
2019/03/08 PHP
PHP的简单跳转提示的实现详解
2019/03/14 PHP
删除重复数据的算法
2006/11/23 Javascript
js去除空格的12种实用方法
2013/11/08 Javascript
了解Javascript的模块化开发
2015/03/02 Javascript
基于javascript实现简单的抽奖系统
2020/04/15 Javascript
为什么我们要做三份 Webpack 配置文件
2017/09/18 Javascript
JS实现的ajax和同源策略(实例讲解)
2017/12/01 Javascript
从vue基础开始创建一个简单的增删改查的实例代码(推荐)
2018/02/11 Javascript
在vue中使用SockJS实现webSocket通信的过程
2018/08/29 Javascript
在Webpack中用url-loader处理图片和字体的问题
2020/04/28 Javascript
three.js 制作动态二维码的示例代码
2020/07/31 Javascript
[03:07]2015国际邀请赛选手档案EHOME.rOtK 是什么让他落泪?
2015/07/31 DOTA
python两种遍历字典(dict)的方法比较
2014/05/29 Python
Python中的Classes和Metaclasses详解
2015/04/02 Python
python爬虫之urllib3的使用示例
2018/07/09 Python
Django 开发调试工具 Django-debug-toolbar使用详解
2019/07/23 Python
解决windows下python3使用multiprocessing.Pool出现的问题
2020/04/08 Python
python golang中grpc 使用示例代码详解
2020/06/03 Python
浅谈keras中的目标函数和优化函数MSE用法
2020/06/10 Python
如何用Python绘制3D柱形图
2020/09/16 Python
HTML5中FileReader接口使用方法实例详解
2017/08/26 HTML / CSS
SmartBuyGlasses台湾:名牌眼镜,名牌太阳眼镜及隐形眼镜
2017/01/04 全球购物
2019银行员工个人工作自我鉴定
2019/06/27 职场文书
ORACLE数据库应用开发的三十个注意事项
2021/06/07 Oracle
Java网络编程之UDP实现原理解析
2021/09/04 Java/Android
总结高并发下Nginx性能如何优化
2021/11/01 Servers
JavaScript的function函数详细介绍
2021/11/20 Javascript
gojs实现蚂蚁线动画效果
2022/02/18 Javascript
css3 文字断裂效果
2022/04/22 HTML / CSS