Vue响应式系统如何操作运用?本文详解

前言

之前学习 vue 的时候,一直没刨根问底过。在看到网上这类文章比较多,参差不齐的质量有时候看的一头雾水。当然也有不错的文章,但是终究是别人的理解。于是写一篇关于自己的理解记录下来,亲身实践才能收获更多!

初阶:响应式原理

在说明之前,我们先了解一个 Object.defineProperty() 。引用 MDN 上的权威介绍 developer.mozilla.org/zh-CN/docs/… :

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

语法

Object.defineProperty(obj, prop, descriptor)
复制代码

参数

  • obj

    要定义属性的对象。

  • prop

    要定义或修改的属性的名称或 Symbol 。

  • descriptor

    要定义或修改的属性描述符。

返回值

被传递给函数的对象。

注意:除了本文项目。我还结合多年开发经验整理出2020最新企业级实战视频教程, 包括 Vue3.0/Js/ES6/TS/React/node等,有兴趣的进扣扣裙 519293536 免费获取,小白勿进哦!接下来我们继续
在了解了这个之后,我们就可以用它来实现一个响应式的初级样子。

const TestObject = {
  name:"",
  age:10
}
let tempValue = '';
Object.defineProperty(TestObject,'name',{

  get:function (){
    console.log('我被获取了,我可以在这里搞点事情!')
    return tempValue;
  },
  set:function (newValue){
    console.log('我被写入了,我可以在这里搞点事情!')
    tempValue = newValue
  }
})
复制代码

此时使用 TestObject.name 方法可以让 get 和 set 里面的对应生效。

我想看到这里,聪明的同学可能会有一个疑问,为啥要搞一个 tempValue ?我直接 TestObject.name 在 get 和 set 里面赋值不行么?就像这样:

const TestObject = {
  name:"",
  age:10
}
Object.defineProperty(TestObject,'name',{
  get:function (){
    console.log('我被获取了,我可以在这里搞点事情!')
    return TestObject.name;
  },
  set:function (newValue){
    console.log('我被写入了,我可以在这里搞点事情!')
    TestObject.name = newValue
  }
})
复制代码

真正运行的时候,其实会发现,陷入了死循环,这里大家切忌要避免坑!

我们重读MDN上的文档可以发现,get 和 set 本身就是在获取和设置的时候触发的函数,在里面写了 TestObject.name ,那么就会继续调用 set ,然后继续 TestObject.name ,继续 set ,继续.....无限循环。所以使用一个临时变量在外面,是比较安全的做法。

中阶:接管对象

在前面基础上,我们现在可以开始接管整个对象了,逻辑非常简单,套个循环,上代码:

function ProxyObj(obj){
  Object.keys(obj).forEach(key=>{
    DefineObj(obj,key,obj[key])
  })
}
function DefineObj(obj,key,value){
  Object.defineProperty(obj,key,{
    get:function (){
      console.log('我被获取了,我可以在这里搞点事情!')
      return value;
    },
    set:function (newValue){
      console.log('我被写入了,我可以在这里搞点事情!')
      value = newValue
    }
  })
}
const TestObject = {
  name:"",
  age:10
}
ProxyObj(TestObject)
复制代码

这时,聪明的同学又会有疑问了?为什么要创建两个 function ,ProxyObj 和 DefineObj 不能堆在一个里面么?就像这样

function ProxyObj(obj){
  Object.keys(obj).forEach(key=>{
    Object.defineProperty(obj,key,{
    get:function (){
      console.log('我被获取了,我可以在这里搞点事情!')
      return obj[key];
    },
    set:function (newValue){
      console.log('我被写入了,我可以在这里搞点事情!')
      obj[key] = newValue
    }
  })
  })
}
const TestObject = {
  name:"",
  age:10
}
ProxyObj(TestObject)
复制代码

不能,其实原因跟之前提到的问题一样,存在死循环的问题。那为什么我们拆分开来就不存在呢?因为这里的 DefineObj 传入的形参。用我们了解到的 js 基础知识来解释,一个函数内的形参,相当于是函数内预设的变量,一般情况下(也会有二般情况)这个变量的生命周期仅在函数内。所以利用了这个特性,案例巧妙的将形参 value 拿出来传递和赋值,就不存在无限死循环的问题了。

划重点:使用 Object.defineProperty 切忌不要陷入到死循环当中!

高阶:收集依赖

理解需求

终于来到了收集依赖环节,这块也是我之前一直没有想通的地方。直到第二天醒来朦胧中看着屏幕前的代码,突然若有所思了,话不多说,直接来看看需求,对于需求都没有非常清晰的概念,直接上代码有点晕。

const TestObject = {
  name:"",
  age:10
}

watcher(TestObject,'type',()=>{
 return TestObject.age>18?'成年人':'未成年人';
})
复制代码

简单实现

我们需要实现这个watcher函数,里面可以填入对象,并且设置关注的对象 type 属性,此时该属性还没有在对象身上,我们需要赋予一下,当 age 变化以后,type 也要对应着改变一下。这就是我们所说的依赖收集需求。

我们这样简单实现一下:

function watcher(obj,key,cb){
  Object.defineProperty(obj,key,{
    get:function (){
      const val = cb();
      return val;
    },
    set:function(newValue){
      console.log('该属性是被用于去自动计算的哦~不要人工赋值!')
    }
  })
}

const TestObject = {
  name:"",
  age:10
}

watcher(TestObject,'type',()=>{
  return TestObject.age>18?'成年人':'未成年人';
})
console.log(TestObject.type)//未成年人
TestObject.age=19;
console.log(TestObject.type)//成年人
复制代码

可以看到,我们通过这样的方式,就简单的实现了 TestObject 属性上的依赖计算属性 。

完善需求

但是问题又来了,我如果不去读取 type 它是不会主动更新的。如何做到 age 变化以后,type 自动更新呢?

在前面的基础上,我们先定义一个依赖更新时候的函数 updateTodo 此时代码变这样:

let target = '';

function watcher(obj,key,cb){
  function updateTodo(){
    const val = cb();
    console.log('更新啦',val)
    }
  Object.defineProperty(obj,key,{
    get:function (){
      target = updateTodo;//重点代码
      const val = cb();//重点代码
      target = '';//重点代码
      return val;
    },
    set:function(newValue){
      console.log('该属性是被用于去自动计算的哦~不要人工赋值!')
    }
  })
}

const TestObject = {
  name:"",
  age:10
}

watcher(TestObject,'type',()=>{
  return TestObject.age>18?'成年人':'未成年人';//重点代码
})
console.log(TestObject.type)
TestObject.age = 19;
console.log(TestObject.type)
复制代码

会发现我们顺便在全局还定义一个target,储存当前 callback,这里是我觉得最重要的部分,一定要认真看里面的这段,思考一下,我单独拿出来:

target = updateTodo;
const val = cb();
target = '';
复制代码

callback函数长这样:

()=>{
  return TestObject.age>18?'成年人':'未成年人';
}
复制代码

可能大家会有疑问,target 赋一下有重置是什么骚操作?有何意义?

注意, callback 里面有一个 TestObject.age !这个一旦被访问,它的 get 函数就被调用了!那么此刻,前面target里面储存的 updateTodo 函数,是不是就可以在 get 里面取到了呢?

所以在 cb() 执行完之后,实际上里面就有机会收集依赖了,target 就是这个作用,作为一个临时的 callback 缓存着,那么我们的需求也很好解决了,只需要在这里进行一次依赖的收集和释放即可!

这里的疑问解决了以后,就开始直接上代码吧!后面就比较好理解,跟我们前面的接管对象章节做一个合并,直接展示完整代码,细细品味,非常有意思:

function ProxyObj(obj){
  Object.keys(obj).forEach(key=>{
    DefineObj(obj,key,obj[key])
  })
}

function DefineObj(obj,key,value){
  let deps = [];
  Object.defineProperty(obj,key,{
    get:function (){
      console.log('我被获取了,我可以在这里搞点事情!')
      //依赖收集
      if(target && !deps.includes(target)){//判断是否存在或重复依赖
        deps.push(target)
      }
      return value;
    },
    set:function (newValue){
      console.log('我被写入了,我可以在这里搞点事情!')
      value = newValue;
      deps.forEach(fn=>{//依赖释放
        fn()
      })
    }
  })
}

function watcher(obj,key,cb){
  function updateTodo(){
    const val = cb();
    console.log('更新啦',val)
    }
  Object.defineProperty(obj,key,{
    get:function (){
      target = updateTodo;
      const val = cb();
      target='';
      return val;
    },
    set:function(newValue){
      console.log('该属性是被用于去自动计算的哦~不要人工赋值!')
    }
  })
}

let target = '';
const TestObject = {
  name:"",
  age:10
}

ProxyObj(TestObject)
watcher(TestObject,'type',()=>{
  return TestObject.age>18?'成年人':'未成年人';
})
console.log(TestObject.type)
TestObject.age=19;
console.log(TestObject.type)
复制代码

当然,如果你对设计模式和一些代码整洁有要求,看到这里你可能会觉得非常不爽,没有关系,尝试着自己来封装下吧,或者参看 Vue 内的源码部分。

数组类型检测原理

1.关于数组方面,可以参看 vue 源码部分。
主要逻辑是通过新建一个 Array 对象来代理实现原生 Array 对象的7大操作。当我们在 vue 里面使用数组方法的时候,实际上使用的是 vue 替我们实现的一些方法,而不是原生的数组操作。
2.这也很好解释了,为什么 vue 不推荐大家使用 [] 方式修改数组。因为受限于 js 语言本身,无法实现对 [] 修改的检测。
3.注意:除了本文项目。我还结合多年开发经验整理出2020最新企业级实战视频教程, 包括 Vue3.0/Js/ES6/TS/React/node等,有兴趣的进扣扣裙 519293536 免费获取,小白勿进哦!

本文的文字及图片来源于网络加上自己的想法,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理

(0)

相关推荐

  • 手摸手带你理解Vue的Watch原理

    前言 watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用.在面试时,也是必问知识点,一般会用作和 computed 进行比较. 那么本文就来带大家从源码理解 ...

  • 一探 Vue 数据响应式原理

    本文写于 2020 年 8 月 5 日 相信在很多新人第一次使用 Vue 这种框架的时候,就会被其修改数据便自动更新视图的操作所震撼. Vue 的文档中也这么写道: Vue 最独特的特性之一,是其非侵 ...

  • JavaScript 里三个点 ... 的用法

    Three dots ( - ) in JavaScript Rest Parameters 使用 rest 参数,我们可以将任意数量的参数收集到一个数组中,然后用它们做我们想做的事情. 引入了其余参 ...

  • Vue2.x 响应式部分源码阅读记录

    之前也用了一段时间Vue,对其用法也较为熟练了,但是对各种用法和各种api使用都是只知其然而不知其所以然.最近利用空闲时间尝试的去看看Vue的源码,以便更了解其具体原理实现,跟着学习学习. Proxy ...

  • JavaScript 奇怪又实用的知识又增加了 6 个

    来源 | 前端印象 作者 | 零一 今天给大家带来一些JavaScript的冷知识,可能你有所耳闻,但也有可能会让你大吃一惊.废话不多说,一起来看看吧! 一.解构小技巧 平常我们需要用到一个嵌套多层的 ...

  • 石桥码农:Vue3 与 Vue2 在响应机制的实现上有什么差别?

    文 / 李艺 目录 一.问题:vue2 通过数组索引改变数据不能触发视图更新是怎么回事? 二.分析:在 vue3 不存在这个问题,vue2 与 vue3 的响应机制分别是怎么实现的? 三.实践:现在如 ...

  • Vue3.0 新特性以及使用变更总结(实际工作用到的)

    前言 Vue3.0 在去年9月正式发布了,也有许多小伙伴都热情的拥抱Vue3.0.去年年底我们新项目使用Vue3.0来开发,这篇文章就是在使用后的一个总结, 包含Vue3新特性的使用以及一些用法上的变 ...

  • 理解vue数据驱动

    vue是双向数据绑定的框架,数据驱动是他的灵魂,他的实现原理众所周知是Object.defineProperty方法实现的get.set重写,但是这样说太牵强外门了.本文将宏观介绍他的实现 使用vue ...

  • 干货|图解 Vue 响应式原理

    Vue 初始化 模板渲染 组件渲染 本文 Vue 源码版本:2.6.11,为了便于理解,均有所删减. 本文将从以下两个方面进行探索: 从 Vue 初始化,到首次渲染生成 DOM 的流程. 从 Vue ...

  • 石桥码农:20 vue计算属性和侦听器

    目录 计算属性 替代计算属性的计算方法 计算属性实现的原理 侦听属性 把侦听属性当作事件监听用 handler方法和immediate属性:监听属性时要立即执行函数怎么做? 想监听子属性.子子属性的变 ...

  • 如何手写Vue-next响应式呢?本文详解

    前言 1.本文将从零开始手写一份vue-next中的响应式原理,出于篇幅和理解的难易程度,我们将只实现核心的api并忽略一些边界的功能点 本文将实现的api包括 track trigger effec ...