vue源码分析之如何实现observer和watcher

前情回顾:

我之前写了一篇没什么干货的文章。。并且刨了一个大坑。。

今天。。打算来填一天。。并再刨一个。。哈哈

不过话说说回来了.看本文之前,,

如果不知道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成功

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

第一步,实现 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方法,我们就发一个通知出来,然后,订阅这个消息的,

就会怎样?。。。对咯。。收到消息。。。触发回调。

第二步,消息-订阅器

很简单,我们维护一个数组,,这个数组,就放订阅着,一旦触发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() 方法

第三步,实现一个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.definePropertyget 要调用的函数,

判断是不是Watcher的构造函数调用,如果是,说明他就是这个属性的订阅者

果断将他 addSub() 中去,那问题来了,

我怎样判断他是 Watcherthis.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.definePropertyget 要调用的函数里,

判断有没有值,就知道到底是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

第四步,实现一个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/fakeVue ,

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

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

原文  http://segmentfault.com/a/1190000004384515
正文到此结束
Loading...