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

  • 相关阅读:
    php 注册与登录
    php 多条件查询
    php(ajax)异步刷新(转)
    PHP中的6种加密方式
    ajax 异步刷新,需要填写的参数
    php+Mysql 页面登录代码
    php+Mysql页面注册代码
    jquery入门知识点总结(转)
    php+Mysql中网页出现乱码的解决办法详解
    php代码常见错误详解整理
  • 原文地址:https://www.cnblogs.com/heavenhome/p/8018246.html
Copyright © 2011-2022 走看看