vue2.x响应式原理

472 阅读4分钟

响应式原理

vue2.x响应式,借助es5的Object.defineProperty(obj, key, {})实现响应式监听。

响应式:就是更改某个数据变量之后,使得视图也进行相应的重新渲染;这就需要监听数据的变化,拦截变化并做重新渲染处理等操作

基本原理

let data = {}

let name = '张三'

Object.defineProperty(data, 'name', {
    get: function(){
        console.log('get')
        return name
    },
    set: function(newValue){
        console.log('set')
        name = newValue
        // 视图重新渲染
    }
})

console.log(data.name) // 触发get拦截,打印张三
data.name = '李四'      // 触发set拦截,在set中进行了视图重新渲染
console.log(data.name) // 触发get拦截,打印李四

响应式原理

let person = {
  name: '张三',
  age: 18,
  habby: {
    ball: '羽毛球',
    sing: '唱歌'
  },
  colors: ['red', 'blue', 'pink']
}

//! 数组方法响应式思想:克隆数组原型上的方法,进行适当修改
const protoMethod = Array.prototype
const protoTemp = Object.create(protoMethod)

let methodList = ['push', 'pop', 'reserve', 'shift']

methodList.forEach(method => {
    protoTemp[method] = function(){
        protoMethod[method].call(this, ...arguments)
        renderView()
    }
})


observer(person)

function observer(target) {
  // 如果是基本类型直接返回
  if (typeof target !== 'object' || typeof target === null) {
    return target
  }
  // 如果是数组
  if (Array.isArray(target)){
      target.__proto__ = protoTemp
  }

  // 如果是正常对象
  for (let key in target) {
    defineReactive(target, key, target[key])
  }
}

function defineReactive(target, key, value) {
  // 深度监听 直到不是一个对象为止(防止value还是个对象。如person.habby)
  // 如果初始数据结构嵌套很深很复杂,则初次渲染可能出现卡死,因为这里一上来就直接进行递归深度监听下去,直到最深层的普通值为止(这个问题vue3中proxy解决,只有到使用到数据时才会做相应的监听)
  observer(value)

  // 响应式重新定义
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      // 深度监听(防止属性被外界又赋值为对象  person.age = {num:10})
      observer(newValue)
      if (value !== newValue) {
        value = newValue
        renderView()
      }

    }
  })
}

function renderView(){
    console.log('视图更新')
}


person.age = { num: 10 }     //视图更新
person.age.num = 20          //视图更新
person.colors.push('white')  //视图更新

对象响应式缺点

  1. 无法监听对象属性的新增和删除(vue中可以通过Vue.delete和Vue.set进行对象属性新增和删除的响应式。)

  2. 如果person的数据结构嵌套层次很深,则首次渲染会很耗时,有可能卡死。(因为一上来就直接进行递归深度监听下去,直到最深层的普通值为止(这个问题vue3中proxy解决,只有到使用到数据时才会做相应的监听))


数组响应式缺点

  1. 数组通过下标更改数据不具有响应式(vue中可以通过Vue.set进行强制响应式)
  2. 数组修改length不具有响应式

心得:重写函数时,外部函数给内部函数传参,可以直接通过具名参数传入,也可以通过绑定内部函数的this+arguments传入

依赖收集

来源:juejin.cn/post/684490…

虽然可以监听到数据的变化了,那我们怎么处理通知视图就更新呢?

Dep就是帮我们收集【究竟要通知到哪里的】。比如下面的代码案例,我们发现,虽然data中有text和message属性,但是只有message被渲染到页面上,至于text无论怎么变化都影响不到视图的展示,因此我们仅仅对message进行收集即可,可以避免一些无用的工作。

那这个时候messageDep就收集到了一个依赖,这个依赖就是用来管理datamessage变化的。

<div>
    <p>{{message}}</p>
</div>

data: {
    text: 'hello world',
    message: 'hello vue',
}

当使用watch属性时,也就是开发者自定义的监听某个data中属性的变化。比如监听message的变化,message变化时我们就要通知到watch这个钩子,让它去执行回调函数。

这个时候messageDep就收集到了两个依赖,第二个依赖就是用来管理watchmessage变化的。

watch: {
    message: function (val, oldVal) {
        console.log('new: %s, old: %s', val, oldVal)
    },
} 

当开发者自定义computed计算属性时,如下messageT属性,是依赖message的变化的。因此message变化时我们也要通知到computed,让它去执行回调函数。

这个时候messageDep就收集到了三个依赖,这个依赖就是用来管理computed中message变化的。

computed: {
    messageT() {
        return this.message + '!';
    }
}

image.png

如何收集依赖?

我们如何知道data中的某个属性被使用了,答案就是Object.defineProperty,因为读取某个属性就会触发get方法。可以将代码进行如下改造:

function defineReactive (obj, key, val) {
    let Dep; // 依赖

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            console.log('我被读了,我要不要做点什么好?');
            // 被读取了,将这个依赖收集起来
            Dep.depend(); // 本次新增
            return val;
        },
        set: newVal => {
            if (val === newVal) {
                return;
            }
            val = newVal;
            // 被改变了,通知依赖去更新
            Dep.notify(); // 本次新增
            console.log("数据被改变了,我要把新的值渲染到页面上去!");
        }
    })
}

参考

www.bilibili.com/video/BV1d7…

www.bilibili.com/video/BV1VA…

juejin.cn/post/684490…