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

https://segmentfault.com/a/1190000004384515

本文能帮你做什么? 。。好奇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.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

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..., 我是一万个不想展示自己代码。。因为很多槽点,还请见谅

时间: 2024-10-09 23:47:45

vue 源码分析之如何实现 observer 和 watcher的相关文章

vue源码解读(一)Observer/Dep/Watcher是如何实现数据绑定的

欢迎star我的github仓库,共同学习~目前vue源码学习系列已经更新了5篇啦~ https://github.com/yisha0307/... 快速跳转: Vue的双向绑定原理(已完成) 说说vue中的Virtual DOM(已完成) React diff和Vue diff实现差别 Vue中的异步更新策略(已完成) Vuex的实现理解 Typescript学习笔记(持续更新ing) Vue源码中闭包的使用(已完成) 介绍 最近在学习vue和vuex的源码,记录自己的一些学习心得.主要借鉴

[Vue源码分析] v-model实现原理

最近小组有个关于vue源码分析的分享会,提前准备一下- 前言:我们都知道使用v-model可以实现数据的双向绑定,及实现数据的变化驱动dom的更新,dom的更新影响数据的变化.那么v-model是怎么实现这一原理的呢?接下来探索一下这部分的源码. 前期准备①:vue2.5.2源码(用于阅读.查看关联等)②:建立vue demo,创建包含v-model指令的实例(用于debugger)以下为demo: genDirectives在模板的编译阶段, v-model跟其他指令一样,会被解析到 el.d

vue源码分析:响应式之依赖收集

今天记录下vue源码的响应式的源码学习,第一步,我们想下,为什么为们写一个data.然后里边的数据变化就能映射到dom的变化上.这里用到了Object.defineProperty(obj,key, {}),如果你还不了解建议去jsmdn上仔细了解下,这个函数的目的就是检测你得赋值和取值的过程用来定义自己的行为,比如obj[key]会拿到配置里边的get函数定义,obj[key] = 'xxx'会调用配置中的set.那么vue也是一样的,在get中做了依赖收集(也就是说哪些dom用到了我定义的这

vue源码分析

双向数据绑定 1. js的eventloop micro 微队列,promise的then,async await,observer, 优先级高于宏队列.window.MutationObserver属于observer macro 宏队列,setTimeout,setInterval, click,event事件,ajax, var MutationObserver = window.MutationObserver; var target = document.querySelector('

Vue源码分析(二) : Vue实例挂载

author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-with-compiler.js & src/platforms/web/runtime/index.js 等文件中都有对Vue.prototype.$mount的定义: // vue/platforms/web/entry-runtime-with-compiler.js Vue.prototype.$mount = function ( e

vue源码分析之目录架构(一)

compiler compiler 目录包含 Vue.js 所有编译相关的代码.它包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能 core core 目录包含了 Vue.js 的核心代码,包括内置组件.全局 API 封装,Vue 实例化.观察者.虚拟 DOM.工具函数等等. platform Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 native 客户端上.platform 是 Vue.js 的入口,2 个目录代表 2

vue2.0源码分析之理解响应式架构

https://segmentfault.com/a/1190000007334535 分享前啰嗦 我之前介绍过vue1.0如何实现observer和watcher.本想继续写下去,可是vue2.0横空出世..所以   直接看vue2.0吧.这篇文章在公司分享过,终于写出来了.我们采用用最精简的代码,还原vue2.0响应式架构实现   以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考.   不过不看也没关系,但是最好了解下Object.defi

Vue系列---理解Vue.nextTick使用及源码分析(五)

_ 阅读目录 一. 什么是Vue.nextTick()? 二. Vue.nextTick()方法的应用场景有哪些? 2.1 更改数据后,进行节点DOM操作. 2.2 在created生命周期中进行DOM操作. 三. Vue.nextTick的调用方式如下: 四:vm.$nextTick 与 setTimeout 的区别是什么? 五:理解 MutationObserver 六:nextTick源码分析 回到顶部 一. 什么是Vue.nextTick()? 官方文档解释为:在下次DOM更新循环结束之

vue源码构建代码分析

这是xue源码学习记录,如有错误请指出,谢谢!相互学习相互进步. vue源码目录为 vue ├── src #vue源码 ├── flow #flow定义的数据类型库(vue通过flow来检测数据类型是否正确) ├── examples #demo ├── scripts #vue构建命令 ├── ... vue内部代码模块比较清晰,这边主要分析scripts内部代码,讲解vue是如何进行构建的.首先你必须要懂一些rollup,vue内部是通过rollup来进行构建的,rollup是一款js的构