资讯专栏INFORMATION COLUMN

简单实现Vue的observer和watcher

Thanatos / 2260人阅读

非庖丁瞎解牛系列~ =。=

在日常项目开发的时候,我们将js对象传给vue实例中的data选项,来作为其更新视图的基础,事实上是vue将会遍历它的属性,用Object.defineProperty 设置它们的 get/set,从而让 data 的属性能够响应数据变化:

 Object.defineProperty(obj, name, {
   // 获取值的时候先置入vm的_data属性对象中
   get() {
     // 赋值的时候显示的特性
   },
   set() {
     // 值变化的时候可以做点什么
   }
 })

接下来可以利用其实现一个最简单的watcher.既然要绑定数据执行回调函数,data属性和callback属性是少不了的,我们定义一个vm对象(vue中vm对象作为根实例,是全局的):

/**
 * @param {Object} _data 用于存放data值
 * @param {Object} $data data原始数据对象,当前值
 * @param {Object} callback 回调函数
 */
var vm = { _data: {}, $data: {}, callback: {} }

在设置值的时候,如果检测到当前值与存储在_data中的对应值发生变化,则将值更新,并执行回调函数,利用Object.definedProperty方法中的get() & set() 我们很快就可以实现这个功能~

 vm.$watch = (obj, func) => {
    // 回调函数
    vm.callback[ obj ] = func
    // 设置data
    Object.defineProperty(vm.$data, obj, {
      // 获取值的时候先置入vm的_data属性对象中
      get() {
        return vm._data[ obj ]
      },
      set(val) {
        // 比较原值,不相等则赋值,执行回调
        if (val !== vm._data[ obj ]) {
          vm._data[ obj ] = val
          const cb = vm.callback[ obj ]
          cb.call(vm)
        }
      }
   })
}
vm.$watch("va", () => {console.log("已经成功被监听啦")})
vm.$data.va = 1

虽然初步实现了这个小功能,那么问题来了,obj对象如果只是一个简单的值为值类型的变量,那以上代码完全可以满足;但是如果obj是一个具有一层甚至多层树结构对象变量,我们就只能监听到最外层也就是obj本身的变化,内部属性变化无法被监听(没有设置给对应属性设置set和get),因为对象自身内部属性层数未知,理论上可以无限层(一般不会这么做),所以此处还是用递归解决吧~

咱们先将Object.defineProperty函数剥离,一是解耦,二是方便我们递归~

var defineReactive = (obj, key) => {
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
      vm._data[key] = newVal
      const cb = vm.callback[ obj ]
      cb.call(vm)
    }
  })
}

咦,说好的递归呢,不着急,上面只是抽离了加get和set功能的函数,
现在我们加入递归~

var Observer = (obj) => {
  // 遍历,让对象中的每个属性可以加上get set
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key)
  })
}

这里仅仅只是遍历,要达到递归,则需要在defineReactive的时候再加上判断,判断这个属性是否为object类型,如果是,则执行Observer自身~我们改写下defineReactive函数

// 判断是否为object类型,是就继续执行自身
var observe = (value) => {
  // 判断是否为object类型,是就继续执行Observer
  if (!value || typeof value !== "object") {
    return
  }
  return new Observer(value)
}

// 将observe方法置入defineReactive中Object.defineProperty的set中,形成递归
var defineReactive = (obj, key) => {
  // 判断val是否为对象,如果对象有很多层属性,则这边的代码会不断调用自身(因为observe又执行了Observer,而Observer执行defineReactive),一直到最后一层,从最后一层开始执行下列代码,层层返回(可以理解为洋葱模型),直到最前面一层,给所有属性加上get/set
  var childObj = observe(vm._data[key])
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      // 如果设置的值完全相等则什么也不做
      if (vm._data[key] === newVal) {
         return
      }
      // 不相等则赋值
      vm._data[key] = newVal
      // 执行回调
      const cb = vm.callback[ key ]
      cb.call(vm)
      // 如果set进来的值为复杂类型,再递归它,加上set/get
      childObj = observe(val)
    }
  })
}

现在我们来整理下,把我们刚开始实现的功能雏形进行进化

var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key) => {
  // 一开始的时候是不设值的,所以,要在外面做一套observe
  // 判断val是否为对象,如果对象有很多层属性,则这边的代码会不断调用自身(因为observe又执行了Observer,而Observer执行defineReactive),一直到最后一层,从最后一层开始执行下列代码,层层返回(可以理解为洋葱模型),直到最前面一层,给所有属性加上get/set
  var childObj = observe(vm._data[key])
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
    // 如果值有变化的话,做一些操作
    vm._data[key] = newVal
    // 执行回调
    const cb = vm.callback[ key ]
    cb.call(vm)
    // 如果set进来的值为复杂类型,再递归它,加上set/get
    childObj = observe(newVal)
    }
  })
}
var Observer = (obj) => {
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key)
  })
}
var observe = (value) => {
  // 判断是否为object类型,是就继续执行Observer
  if (!value || typeof value !== "object") {
    return
  }
  Observer(value)
}
vm.$watch = (name, func) => {
  // 回调函数
  vm.callback[name] = func
  // 设置data
  defineReactive(vm.$data, name)
}
// 绑定a,a若变化则执行回调方法
var va = {a:{c: "c"}, b:{c: "c"}}
vm._data[va] = {a:{c: "c"}, b:{c: "c"}}
vm.$watch("va", () => {console.log("已经成功被监听啦")})
vm.$data.va = 1

在谷歌浏览器的console中粘贴以上代码,然后回车发现,结果不出所料,va本身被监听了,可以,我们试试va的内部属性有没有被监听,改下vm.$data.va = 1为vm.$data.va.a = 1,结果发现报错了

什么鬼?

我们又仔细检查了代码,WTF,原来我们在递归的时候,Object.defineProperty中的回调函数cb的key参数一直在发生变化,我们希望的是里面的属性变化的时候执行的是我们事先定义好的回调函数~那么我们来改下方法,将一开始定义好的回调作为参数传进去,确保每一层递归set的回调都是我们事先设置好的~

var vm = { _data: {}, $data: {}, callback: {}}
var defineReactive = (obj, key, cb) => {
  // 一开始的时候是不设值的,所以,要在外面做一套observe
  var childObj = observe(vm._data[key], cb)
  Object.defineProperty(obj, key, {
    get() {
      return vm._data[key]
    },
    set(newVal) {
      if (vm._data[key] === newVal) {
        return
      }
      // 如果值有变化的话,做一些操作
      vm._data[key] = newVal
      // 执行回调
      cb()
      // 如果set进来的值为复杂类型,再递归它,加上set/get
      childObj = observe(newVal)
    }
  })
}
var Observer = (obj, cb) => {
  Object.keys(obj).forEach((key) =>{
    defineReactive(obj, key, cb)
  })
}
var observe = (value, cb) => {
  // 判断是否为object类型,是就继续执行Observer
  if (!value || typeof value !== "object") {
    return
  }
  Observer(value, cb)
}
vm.$watch = (name, func) => {
  // 回调函数
  vm.callback[name] = func
  // 设置data
  defineReactive(vm.$data, name, func)
}
// 绑定a,a若变化则执行回调方法
var va = {a:{c: "c"}, b:{c: "c"}}
vm._data.va = {a:{c: "c"}, b:{c: "c"}}
vm.$watch("va", () => {console.log("又成功被监听啦")})
vm.$data.va.a = 1

再执行一次以上代码,发现内部的a属性也被监听到了,而且属性值变化的时候执行了我们事先定义好的回调函数~嘻嘻嘻~

虽然实现了$watch的基本功能,但是和vue的源码还是有一定的距离,特别是一些扁平化和模块化的思想需要涉及到一些设计模式,其实我们在看源码的时候,常常是逆着作者的思维走的,功能从简单到复杂往往涉及到代码的模块化和解耦,使得代码非常地分散,读起来晦涩难懂,自己动手,从小功能代码块实现,然后结合源码,对比思路,慢慢丰富,也不失为一种学习源码的方式~

ps: 如果各位读者看到本文的error或者由更好的优化建议,随时联系~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81965.html

相关文章

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

    摘要:所以,我们是不是应该写一个消息订阅器呢这样的话,一触发方法,我们就发一个通知出来,然后,订阅这个消息的,就会怎样。。。截止到现在,在我们只考虑最简单情况下。。关于的新文章行代码,理解和分析的响应式架构 本文能帮你做什么?。。好奇vue双向绑定的同学,可以部分缓解好奇心还可以帮你了解如何实现$watch 前情回顾 我之前写了一篇没什么干货的文章。。并且刨了一个大坑。。今天。。打算来填一天...

    shiina 评论0 收藏0
  • Vue双向绑定实现原理系列(三):监听器Observer订阅者Watcher

    摘要:至此监听器和订阅者功能基本完成,后面再加上指令解析器的功能系列文章的目录双向绑定的实现原理系列一双向绑定的实现原理系列二设计模式双向绑定的实现原理系列三监听器和订阅者双向绑定的实现原理系列四补充指令解析器 监听器Observer和订阅者Watcher 实现简单版Vue的过程,主要实现{{}}、v-model和事件指令的功能 主要分为三个部分 github源码 1.数据监听器Obser...

    widuu 评论0 收藏0
  • Vue双向绑定实现原理系列(三):监听器Observer订阅者Watcher

    摘要:至此监听器和订阅者功能基本完成,后面再加上指令解析器的功能系列文章的目录双向绑定的实现原理系列一双向绑定的实现原理系列二设计模式双向绑定的实现原理系列三监听器和订阅者双向绑定的实现原理系列四补充指令解析器 监听器Observer和订阅者Watcher 实现简单版Vue的过程,主要实现{{}}、v-model和事件指令的功能 主要分为三个部分 github源码 1.数据监听器Obser...

    legendaryedu 评论0 收藏0
  • 深入浅出Vue响应式原理

    摘要:总结最后我们依照下图参考深入浅出,再来回顾下整个过程在后,会调用函数进行初始化,也就是过程,在这个过程通过转换成了的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行函数,而在当被赋值的时候会执行函数。 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解...

    yiliang 评论0 收藏0
  • Vue 数据响应式原理

    摘要:接下来,我们就一起深入了解的数据响应式原理,搞清楚响应式的实现机制。回调函数只是打印出新的得到的新的值,由执行后生成。及异步更新相信读过前文,你应该对响应式原理有基本的认识。 前言 Vue.js 的核心包括一套响应式系统。 响应式,是指当数据改变后,Vue 会通知到使用该数据的代码。例如,视图渲染中使用了数据,数据改变后,视图也会自动更新。 举个简单的例子,对于模板: {{ name ...

    Mike617 评论0 收藏0
  • vue.js动态数据绑定学习

    摘要:对于的动态数据绑定,经过反复地看源码和博客讲解,总算能够理解它的实现了,心累分享一下学习成果,同时也算是做个记录。 对于vue.js的动态数据绑定,经过反复地看源码和博客讲解,总算能够理解它的实现了,心累~ 分享一下学习成果,同时也算是做个记录。完整代码GitHub地址:https://github.com/hanrenguang/Dynamic-data-binding。也可以到仓库...

    UsherChen 评论0 收藏0

发表评论

0条评论

Thanatos

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<