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 相关文章推荐
番茄的表单验证类代码修改版
Jul 18 Javascript
当滚动条滚动到页面底部自动加载增加内容的js代码
May 13 Javascript
clipboard.js无需Flash无需依赖任何JS库实现文本复制与剪切
Oct 10 Javascript
Node.js刷新session过期时间的实现方法推荐
May 18 Javascript
JS 实现Base64编码与解码实例详解
Nov 07 Javascript
值得学习的bootstrap fileinput文件上传工具
Nov 08 Javascript
利用JS实现文字的聚合动画效果
Jan 22 Javascript
html5+canvas实现支持触屏的签名插件教程
May 08 Javascript
解决iview打包时UglifyJs报错的问题
Mar 07 Javascript
详解使用VueJS开发项目中的兼容问题
Aug 02 Javascript
深入解读Node.js中的koa源码
Jun 17 Javascript
新手如何快速理解js异步编程
Jun 24 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
php快速url重写 更新版[需php 5.30以上]
2010/04/20 PHP
Laravel框架集成UEditor编辑器的方法图文与实例详解
2019/04/17 PHP
解决Laravel blade模板转义html标签的问题
2019/09/03 PHP
浅析PHP7 的垃圾回收机制
2019/09/06 PHP
浅谈Laravel中的三种中间件的作用
2019/10/13 PHP
Aster vs KG BO3 第一场2.19
2021/03/10 DOTA
jquery form表单提交插件asp.net后台中文解码
2010/06/12 Javascript
jquery仿搜索自动联想功能代码
2014/05/23 Javascript
JS自定义选项卡函数及用法实例分析
2015/09/02 Javascript
有关suggest快速删除后仍然出现下拉列表的bug问题
2016/12/02 Javascript
详解Vue CLI3配置之filenameHashing使用和源码设计使用和源码设计
2018/08/31 Javascript
elementUI Vue 单个按钮显示和隐藏的变换功能(两种方法)
2018/09/04 Javascript
element ui 表格动态列显示空白bug 修复方法
2018/09/04 Javascript
解决vue动态为数据添加新属性遇到的问题
2018/09/18 Javascript
微信小程序 组件的外部样式externalClasses使用详解
2019/09/06 Javascript
vue-router的钩子函数用法实例分析
2019/10/26 Javascript
jQuery实现评论模块
2020/08/19 jQuery
Python字符串和文件操作常用函数分析
2015/04/08 Python
Python中自定义函数的教程
2015/04/27 Python
python 打印对象的所有属性值的方法
2016/09/11 Python
python pandas读取csv后,获取列标签的方法
2018/11/12 Python
Python with用法:自动关闭文件进程
2019/07/10 Python
python requests指定出口ip的例子
2019/07/25 Python
Python简易版图书管理系统
2019/08/12 Python
浅谈python中统计计数的几种方法和Counter详解
2019/11/07 Python
Python虚拟环境venv用法详解
2020/05/25 Python
如何在mac下配置python虚拟环境
2020/07/06 Python
用pip给python安装matplotlib库的详细教程
2021/02/24 Python
html5新增的定时器requestAnimationFrame实现进度条功能
2018/12/13 HTML / CSS
印度婴儿用品在线商店:Firstcry.com
2016/12/05 全球购物
Java面试中常遇到的问题,也是需要注意的几点
2013/08/30 面试题
销售经理工作职责
2014/02/03 职场文书
电厂职工自我鉴定
2014/02/20 职场文书
有兼职工作经历的简历自我评价
2014/03/07 职场文书
大卫科波菲尔读书笔记
2015/06/30 职场文书
SpringBoot整合Mybatis Generator自动生成代码
2021/08/23 Java/Android