provide vue 响应式_通俗易懂的Vue响应式原理以及依赖收集

provide vue 响应式_通俗易懂的Vue响应式原理以及依赖收集_第1张图片

作者:hello等风来

转发链接:https://juejin.im/post/5efd3282e51d4534c45511e3

前言

最近在看一些底层方面的知识。所以想做个系列尝试去聊聊这些比较复杂又很重要的知识点。学习就好比是座大山,只有自己去登山,才能看到不一样的风景,体会更加深刻。今天我们就来聊聊Vue中比较重要的响应式原理以及依赖收集。

响应式原理

Object.defineProperty() 和 Proxy 对象,都可以用来对数据的劫持操作。何为数据劫持呢?就是在我们访问或者修改某个对象的某个属性的时候,通过一段代码进行拦截,然后进行额外的操作,返回结果。vue中双向数据绑定就是一个典型的应用。

Vue2.x 是使用 Object.defindProperty(),来实现对对象的监听。

Vue3.x 版本之后就改用Proxy实现。

在MDN中是这样定义:

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

Object.defineProperty(obj, prop, descriptor)

  • obj:要定义属性的对象
  • prop:要定义或修改的属性名称
  • descriptor:要定义或修改的属性描述符(configurable: 可改变的;writable:可写的;enumerable:可枚举的;getset:设置或获取对象的某个属性的值)
const data = {}const name = 'zhangsan'Object.defineProperty(data, 'name', {    writable: true,    configurable: true,    get: function () {        console.log('get')        return name    },    set: function (newVal) {        console.log('set')        name = newVal    }})复制代码

当把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。简单理解就是在data和用户之间做了一层代理中间层,在vue initData的时候,将_data上面的数据代理到vm上,通过observer类将所有的data变成可观察的,及对data定义的每一个属性进行gettersetter操作,这就是Vue实现响应式的基础。

provide vue 响应式_通俗易懂的Vue响应式原理以及依赖收集_第2张图片

Vue数据响应式变化主要涉及 Observer, Watcher , Dep 这三个主要的类。因此要弄清Vue响应式变化需要明白这个三个类之间是如何运作联系的;以及它们的原理,负责的逻辑操作。

响应式原理(Observer)

Observer类是将每个目标对象(即data)的键值转换成getter/setter形式,用于进行依赖收集以及调度更新。那么在vue这个类是如何实现的:

  • 1、observer实例绑定在data的ob属性上面,防止重复绑定;
  • 2、若data为数组,先实现对应的变异方法(Vue重写了数组的7种原生方法)再将数组的每个成员进行observe,使之成响应式数据;
  • 3、否则执行walk()方法,遍历data所有的数据,进行getter/setter绑定。这里的核心方法就是 defineReative(obj, keys[i], obj[keys[i]])
// 监听对象属性Observer类class Observer {  constructor(value) {    this.value = value    if (!value || (typeof value !== 'object')) {      return    } else {      this.walk(value)    }  }  walk(obj) {    Object.keys(obj).forEach(key => {      defineReactive(obj, key, obj[key])    })  }}复制代码
function defineReactive(obj, key, val) {  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: function reactiveGetter() {      return val    },    set: function reactiveSetter(newVal) {      // 注意:value一直在闭包中,此处设置完之后,再get时也是会得到最新的值      if (newVal === val) return      updateView()    }  })}function updateView() {  console.log('视图更新了')}const data = {  name: 'zhangsan',  age: 20}new Observer(data)data.name = 'lisi'  // 打印‘视图更新了’复制代码

这就是简单的一个Observer类,这也是vue响应式的基本原理。但我们都知道 object.defineproperty的存在一些缺点:

1、对于复杂的对象需要深度监听,回归到底,一次性计算量大

2、无法监听新增属性/删除属性(Vue.set Vue.delete)

3、无法监听数组,需特殊处理,也就是上面说的变异方法

这也就是vue3改进的一方面,后文我们也会着重讲解vue3 proxy如何做响应式的。

扩展一、vue如何深度监听

上图中我们看到data中的一级目录name、age在值改变的时候,会触发视图更新,但在我们实际开发过程中,data可能会是比较复杂的对象,嵌套了好几层:

const data = {  name: 'zhangsan',  age: 20,  info: {      address: '北京'  }}data.info.address = '上海' // 并没有执行。复制代码

造成这种原因是,代码中defineReactive接收到的val是一个对象,为了避免这种复杂的对象vue采用递归的思想在defineReactive函数中再执行一次observer函数就行,递归将对象在遍历一次获取key/value值,new Observer(val)。同样在设置值的时候可能会把name也设置成一个对象,因此在data值更新的时候也需要进行判断深度监听

function defineReactive(obj, key, val) {  new Observer(val) // 深度监听  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: function reactiveGetter() {      return val    },    set: function reactiveSetter(newVal) {      // 注意:value一直在闭包中,此处设置完之后,再get时也是会得到最新的值      if (newVal === val) return      new Observer(val) // 深度监听      updateView()    }  })}复制代码

扩展二、vue数组的监听

object.defineproperty对数组是不起作用的,那么在vue中又是如何去监听数组的变化,其实Vue 将被侦听的数组的变更方法进行了包裹。接下来将用简单代码演示:

// 防止全局污染,重新定义数组原型const oldArrayProperty = Array.prototype// 创建新对象,原型指向oldArrayPropertyconst arrProto = Object.create(oldArrayProperty);['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {  arrProto[methodName] = function () { // 在定义数组的方法    updateView()    oldArrayProperty[methodName].call(this, ...arguments) // 实际执行数组的方法  }})// 在Observer函数中对数组进行处理if (Array.isArray(value)) {    value.__proto__ = arrProto  }复制代码

从代码中看到,在Observer函数有一层对数组进行拦截,将数组的__proto__指向了一个arrProto,arrProto是一个对象,这个对象指向数组的原型,因此arrProto拥有了数组原型上的方法,然后在这对象上重新自定义了数组的7种方法将其包裹,但又不会影响数组原型的方法,这就是变异,再将数组的每个成员进行observe,使之成响应式数据。

依赖收集(Watcher、Dep)

我们现在有这么一个Vue对象

new Vue({    template:         `
text1: { {text1}}
`, data: { text1: 'text1', text2: 'text2' }})复制代码

我们可以从以上代码看出,data中text2并没有被模板实际用到,为了提高代码执行效率,我们没有必要对其进行响应式处理,因此,依赖收集简单理解就是收集只在实际页面中用到的data数据,那么Vue是如何进行依赖收集的,这也就是下面要讲的Watcher、Dep类了。

被Observer的data在触发 getter 时,Dep 就会收集依赖,然后打上标记,这里就是标记为Dep.target

Watcher是一个观察者对象。依赖收集以后的watcher对象被保存在Dep的subs中,数据变动的时候Dep会通知watcher实例,然后由watcher实例回调cb进行视图更新。

Watcher可以接受多个订阅者的订阅,当有data变动时,就会通过 Dep 给 Watcher 发通知进行更新。

provide vue 响应式_通俗易懂的Vue响应式原理以及依赖收集_第3张图片

我们可以用一些简单的代码去实现这个过程。

class Observer {  constructor(value) {    this.value = value    if (!value || (typeof value !== 'object')) {      return    } else {      this.walk(value)    }  }  walk(obj) {    Object.keys(obj).forEach(key => {      defineReactive(obj, key, obj[key])    })  }}// 订阅者Dep,存放观察者对象class Dep {  constructor() {    this.subs = []  }  /*添加一个观察者对象*/  addSub (sub) {    this.subs.push(sub)  }  /*依赖收集,当存在Dep.target的时候添加观察者对象*/  depend() {    if (Dep.target) {      Dep.target.addDep(this)    }  }  // 通知所有watcher对象更新视图  notify () {    this.subs.forEach((sub) => {      sub.update()    })  }}class Watcher {  constructor() {    /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */    Dep.target = this;  }  update () {    console.log('视图更新啦')  }  /*添加一个依赖关系到Deps集合中*/  addDep (dep) {    dep.addSub(this)  }}function defineReactive (obj, key, val) {  const dep = new Dep()  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: function reactiveGetter () {      dep.depend() /*进行依赖收集*/      return val    },    set: function reactiveSetter (newVal) {      if (newVal === val) return      dep.notify()    }  })}class Vue {  constructor (options) {    this._data = options.data    new Observer(this._data) // 所有data变成可观察的    new Watcher() // 创建一个观察者实例    console.log('render~', this._data.test)  }}let o = new Vue({  data: {    test: 'hello vue.'  }})o._data.test = 'hello mvvm!'Dep.target = null复制代码

总结

1、在Vue中模版编译过程中的指令或者数据绑定都会实例化一个Watcher实例,实例化过程中会触发get()将自身指向Dep.target;2、data在Observer时执行getter会触发dep.depend()进行依赖收集,3、当data中被 Observer的某个对象值变化后,触发subs中观察它的watcher执行 update() 方法,最后实际上是调用watcher的回调函数cb,进而更新视图。

Vue3-Proxy实现响应式

Proxy可以理解成在目标对象前架设一个拦截层,外界对该对象的出发必须先通过这层拦截层,因此提供了一种机制可以对外界的访问进行过滤和改写。

function reactive(value = {}) {  if (!value || (typeof value !== 'object')) {    return  }  // 代理配置  const proxyConf = {    get(target, key,receiver) {      // 只处理非原型的属性      let ownKeys = Reflect.ownKeys(target)      if (ownKeys.includes(key)) {        console.log('get', key)      }      const result = Reflect.get(target, key, receiver)       // 深度监听        // 性能如何提升? 什么时候用什么时候递归      return reactive(result)    },    set(target, key, val, receiver) {      // 重复的数据不处理      const oldVal = target[key]      if (val === oldVal) return true            const ownKey = Reflect.ownKeys(target)       if (ownKeys.include(key)) {          console.log('已有的key', key)      } else {          console.log('新增的key', key)      }            const result = Reflect.set(target, key, val, receiver)      console.log('set', key, val)      return result    },    deleteProperty(target, key) {      const result = Reflect.deleteProperty(target, key)      console.log('delete property', key)      return result    }  }  // 生成代理对象  const observed = new Proxy(value, proxyConf)  return observed}const data = {  name: 'zhangsan',  age: 20,  info: {    address: '北京'  },  num: [1, 2, 3]}const proxyData = reactive(data)proxyData.name ='lisi'  // set name lisi复制代码

proxy深度监听的性能提升,在proxy中对于复杂的对象,只会geter()的时候对当前层的监听,比如说在info中

info: {    address: '北京',    a: {        b: {            c: {                d: 2            }        }    }}复制代码

修改proxyData.info.a并不会把后面b、c、d递归出来,避免了object.defineProperty一次性全部递归计算完成。由于proxy原生对数组就能监听,所以也是对object.defineProperty缺点的一个改进。并且从代码中可以看出,在增加/删除时proxy也一样可以监听到,这就是proxy的优势。

扩展一、Reflect

reflect对象的方法和proxy对象的方法一一对应,只要是proxy对象的方法,就能在reflect对象找到对应的方法。这就使得proxy对象可以方便的调用对应的reflect方法来完成默认的行为,作为修改行为的基础。

Reflect有其实是对Object对象的规范化吧,将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty)放到Reflect对象上。

Reflect.get(target, name, receiver): 查到并返回target对象上的name属性,没有该属性会返回undefined

Reflect.set(target, name, value, receiver): 设置target对象的name属性等于value

Reflect.has(object, name): 判断对象上是否有name属性

Reflect.ownKeys(target): 返回对象的所有属性

扩展二、使用proxy实现观察者模式

// 观察者模式指的是函数自动观察数据对象的模式,一旦数据有变化,数据就会自动执行const queuedObservers = new Set()const observe = fn => queuedObservers.add(fn)const observable = obj => new Proxy(obj, {set})function set(target, key, value, receiver) {  const result = Reflect.set(target, key, value, receiver)  queuedObservers.forEach(observe => observe())  return result}const person = observable({ // 观察对象  name: '张三',  age: 20})function print() {  // 观察者  console.log(`${person.name}, ${person.age}`)}observe(print)person.name = '李四'

推荐Vue学习资料文章:

《原生JS +Vue实现框选功能》

《Vue.js轮播库热门精选》

《一文带你搞懂vue/react应用中实现ssr(服务端渲染)》

《Vue+CSS3 实现图片滑块效果》

《教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(上)》

《教你Vue3 Compiler 优化细节,如何手写高性能渲染函数(下)》

《vue实现一个6个输入框的验证码输入组件》

《一用惊人的Vue实践技巧「值得推荐」》

《Vue常见的面试知识点汇总(上)「附答案」》

《Vue常见的面试知识点汇总(下)「附答案」》

《Kbone原理详解与小程序技术选型》

《为什么我不再用Vue,改用React?》

《让Jenkins自动部署你的Vue项目「实践」》

《20个免费的设计资源 UI套件背景图标CSS框架》

《Deno将停止使用TypeScript,并公布五项具体理由》

《前端骨架屏都是如何生成的》

《Vue原来可以这样写开发效率杠杠的》

《用vue简单写一个音乐播放组件「附源码」》

《为什么Vue3.0不再使用defineProperty实现数据监听?》

《「干货」学会这些Vue小技巧,可以早点下班和女神约会》

《探索 Vue-Multiselect》

《细品30张脑图带你从零开始学Vue》

《Vue后台项目中遇到的技术难点以及解决方案》

《手把手教你Electron + Vue实战教程(五)》

《手把手教你Electron + Vue实战教程(四)》

《手把手教你Electron + Vue实战教程(三)》

《手把手教你Electron + Vue实战教程(二)》

《手把手教你Electron + Vue实战教程(一)》

《收集22种开源Vue模板和主题框架「干货」》

《如何写出优秀后台管理系统?11个经典模版拿去不谢「干货」》

《手把手教你实现一个Vue自定义指令懒加载》

《基于 Vue 和高德地图实现地图组件「实践」》

《一个由 Vue 作者尤雨溪开发的 web 开发工具—vite》

《是什么让我爱上了Vue.js》

《1.1万字深入细品Vue3.0源码响应式系统笔记「上」》

《1.1万字深入细品Vue3.0源码响应式系统笔记「下」》

《「实践」Vue 数据更新7 种情况汇总及延伸解决总结》

《尤大大细说Vue3 的诞生之路「译」》

《提高10倍打包速度工具Snowpack 2.0正式发布,再也不需要打包器》

《大厂Code Review总结Vue开发规范经验「值得学习」》

《Vue3 插件开发详解尝鲜版「值得收藏」》

《带你五步学会Vue SSR》

《记一次Vue3.0技术干货分享会》

《Vue 3.x 如何有惊无险地快速入门「进阶篇」》

《「干货」微信支付前后端流程整理(Vue+Node)》

《带你了解 vue-next(Vue 3.0)之 炉火纯青「实践」》

《「干货」Vue+高德地图实现页面点击绘制多边形及多边形切割拆分》

《「干货」Vue+Element前端导入导出Excel》

《「实践」Deno bytes 模块全解析》

《细品pdf.js实践解决含水印、电子签章问题「Vue篇」》

《基于vue + element的后台管理系统解决方案》

《Vue仿蘑菇街商城项目(vue+koa+mongodb)》

《基于 electron-vue 开发的音乐播放器「实践」》

《「实践」Vue项目中标配编辑器插件Vue-Quill-Editor》

《基于 Vue 技术栈的微前端方案实践》

《消息队列助你成为高薪 Node.js 工程师》

《Node.js 中的 stream 模块详解》

《「干货」Deno TCP Echo Server 是怎么运行的?》

《「干货」了不起的 Deno 实战教程》

《「干货」通俗易懂的Deno 入门教程》

《Deno 正式发布,彻底弄明白和 node 的区别》

《「实践」基于Apify+node+react/vue搭建一个有点意思的爬虫平台》

《「实践」深入对比 Vue 3.0 Composition API 和 React Hooks》

《前端网红框架的插件机制全梳理(axios、koa、redux、vuex)》

《深入Vue 必学高阶组件 HOC「进阶篇」》

《深入学习Vue的data、computed、watch来实现最精简响应式系统》

《10个实例小练习,快速入门熟练 Vue3 核心新特性(一)》

《10个实例小练习,快速入门熟练 Vue3 核心新特性(二)》

《教你部署搭建一个Vue-cli4+Webpack移动端框架「实践」》

《2020前端就业Vue框架篇「实践」》

《详解Vue3中 router 带来了哪些变化?》

《Vue项目部署及性能优化指导篇「实践」》

《Vue高性能渲染大数据Tree组件「实践」》

《尤大大细品VuePress搭建技术网站与个人博客「实践」》

《10个Vue开发技巧「实践」》

《是什么导致尤大大选择放弃Webpack?【vite 原理解析】》

《带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】》

《带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】》

《实践Vue 3.0做JSX(TSX)风格的组件开发》

《一篇文章教你并列比较React.js和Vue.js的语法【实践】》

《手拉手带你开启Vue3世界的鬼斧神工【实践】》

《深入浅出通过vue-cli3构建一个SSR应用程序【实践】》

《怎样为你的 Vue.js 单页应用提速》

《聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总》

《【新消息】Vue 3.0 Beta 版本发布,你还学的动么?》

《Vue真是太好了 壹万多字的Vue知识点 超详细!》

《Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5》

《深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】》

《手把手教你深入浅出vue-cli3升级vue-cli4的方法》

《Vue 3.0 Beta 和React 开发者分别杠上了》

《手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件》

《Vue3 尝鲜》

《总结Vue组件的通信》

《Vue 开源项目 TOP45》

《2020 年,Vue 受欢迎程度是否会超过 React?》

《尤雨溪:Vue 3.0的设计原则》

《使用vue实现HTML页面生成图片》

《实现全栈收银系统(Node+Vue)(上)》

《实现全栈收银系统(Node+Vue)(下)》

《vue引入原生高德地图》

《Vue合理配置WebSocket并实现群聊》

《多年vue项目实战经验汇总》

《vue之将echart封装为组件》

《基于 Vue 的两层吸顶踩坑总结》

《Vue插件总结【前端开发必备】》

《Vue 开发必须知道的 36 个技巧【近1W字】》

《构建大型 Vue.js 项目的10条建议》

《深入理解vue中的slot与slot-scope》

《手把手教你Vue解析pdf(base64)转图片【实践】》

《使用vue+node搭建前端异常监控系统》

《推荐 8 个漂亮的 vue.js 进度条组件》

《基于Vue实现拖拽升级(九宫格拖拽)》

《手摸手,带你用vue撸后台 系列二(登录权限篇)》

《手摸手,带你用vue撸后台 系列三(实战篇)》

《前端框架用vue还是react?清晰对比两者差异》

《Vue组件间通信几种方式,你用哪种?【实践】》

《浅析 React / Vue 跨端渲染原理与实现》

《10个Vue开发技巧助力成为更好的工程师》

《手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】》

《1W字长文+多图,带你了解vue的双向数据绑定源码实现》

《深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】》

《干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)》

《基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现》

《手把手教你D3.js 实现数据可视化极速上手到Vue应用》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】》

《吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】》

《Vue3.0权限管理实现流程【实践】》

《后台管理系统,前端Vue根据角色动态设置菜单栏和路由》


作者:hello等风来
转发链接:https://juejin.im/post/5efd3282e51d4534c45511e3

你可能感兴趣的:(provide,vue,响应式)