Interviewer: Can you write Vue responsive by hand? (Vue responsive principle [full version])

write in front

Because of the article I wrote yesterday [ Interviewer: Can't even write Vue view updates? (The principle of Vue view update [1]) ] is somewhat related to today's content code, so a brief review and elaboration at the beginning will facilitate the complete reading of the content.

Vue view update principle

Vue's view update principle mainly involves the use of responsive API Object.defineProperty . Its function is to provide get and set methods for a certain property of the object, so that the external read and write operations of the property can be read and written. Internal monitoring to realize subsequent synchronous view update function

1. Implement the core API of responsiveness: Object.defineProperty

Introduction to the usage of Object.defineProperty: MDN-Object.defineProperty , the following is a preliminary understanding of the API interface by simulating the update of the Vue data value

// 模拟 Vue 中的 data
const data = {}
// 对外不可见的内部变量
let _myName = 'Yimwu'
// 响应式监听 data 中的 name
Object.defineProperty(data, "name", {
    // 使用 data.name 时 get 方法被调用,返回内部存储变量值
    get: () => {
      console.log('get')
      return _myName
    },
    // 使用 data.name = xxx 修改变量时,set 方法被调用,设置内部存储变量值
    set: (newVal) => {
      console.log('set')
      _myName = newVal
    }
})
console.log(data.name) // 输出 Yimwu  get
data.name = 'Mr.Wu' // 输出 set (监听成功)
复制代码

Second, the initial realization of view update

1、updateView

In order to facilitate the simulation of view update , a function updateView is created here . When the data is updated, updateView is called to simulate the view update (in Vue, it is expressed as the change of the DOM element that references the variable value in the template template )

// 验证更新是否触发
function updateView(){
  console.log('视图更新')
}
复制代码

2、defineReactive

创建函数 defineReactive ,对 API Object.defineProperty 进行封装,接受三个参数,监听的目标对象、属性名,以及属性值,一个target(对象)通过调用 defineReactive 就能够实现对 key(对应属性名)进行监听,类比到 Vue 中:

<script>
export default {
    data(){            // data ---> target
        name: 'yimwu'  // name ---> key
    }                  // 'yimwu'---> value
}
</script>
复制代码

具体实现如下:

// 重新定义属性,监听起来
function defineReactive(target, key, value){
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newVal){
      // value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
      // 这里有个小优化,若相同则不触发更新
      if(newVal !== value){
        value = newVal
        // 触发更新
        updateView()
      }
    }
  })
}
复制代码

3、observe

observe 主要是用于对对象中的每个属性进行 defineReactive 监听

// 监听对象属性
function observe(target){
  if(typeof target !== 'object' || target === null) {
    // 不是数组或对象不适合监听
    return target
  }
  // 将对象的属性用 defineProperty 重新定义
  for(let key in target) {
    defineReactive(target, key, target[key])
  }
}
复制代码

4、完整代码以及测试例子

// 验证更新是否触发
function updateView(){
  console.log('视图更新')
}
// 重新定义属性,监听起来
function defineReactive(target, key, value){
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newVal){
      // value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
      // 这里有个小优化,若相同则不触发更新
      if(newVal !== value){
        value = newVal
        // 触发更新
        updateView()
      }
    }
  })
}

// 监听对象属性
function observe(target){
  if(typeof target !== 'object' || target === null) {
    // 不是数组或对象不适合监听
    return target
  }
  // 将对象的属性用 defineProperty 重新定义
  for(let key in target) {
    defineReactive(target, key, target[key])
  }
}

// 准备数据
const data = {
  name: 'Yimwu',
  id: 001,
  information: {
    tel: '135xxxxx354',
    email: '[email protected]' 
  }
}

// 监听数据
observe(data)

// 测试
data.name = 'YI' // (监听成功)输出 --> 数据更新
data.age = { num: 21 }  (监听成功)输出 --> 数据更新
data.information.tel = '13456xxx234' // (监听失败)
data.age.num = 110 // (监听失败)
复制代码

5、视图更新优化(实现对象深度监听)

从上面测试的例子可以看出,对于data.information.tel这种嵌套的对象,初版的 defineReactive 是无法进行监听的,解决的方法也很简单,对对象的所有属性进行监听函数的递归调用,即在执行 Object.defineProperty 前先进行递归调用 observe,如果该属性为对象,则 observe 会递归调用 defineReactive,不是则observe 直接返回,继续执行 Object.defineProperty,完整代码及测试例子如下:

// 重新定义属性,监听起来
function defineReactive(target, key, value){
  // 再次用value嵌套调用 observe 深,若为对象,则进行进一步监听,若非value非对象则直接返回
  observe(value)
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newVal){
      // value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
      if(newVal !== value){
        value = newVal
        // 触发更新
        updateView()
      }
    }
  })
}
// 测试数据
const data = {
  name: 'Yimwu',
  id: 001,
  information: {
    tel: '135xxxxx354',
    email: '[email protected]' 
  }
}

// 监听数据
observe(data)

// 测试
data.name = 'YI' // (监听成功)输出 --> 数据更新
data.information.tel = '00000000000' (监听成功)输出 --> 数据更新
复制代码

6、如何理解 Vue.set

在使用 Vue 的过程中,我们或许都有过这样子的经历,在 data 中定义了一个对象,然后在程序执行过程中给他动态添加了属性,然后对当我们对该新增属性进行值更新时并没有触发视图更新,作为Vue初学者时,将 data 响应式当成黑盒对待,就很难理解它为啥不更新,而今天拨开原理后,这里就很容易理解了

data.id = { num: 010 } // (监听成功)输出 --> 数据更新
data.id.num = 110 // (监听失败)
复制代码

如上图所示,当给 id 赋值为一个对象时,触发了 id 的数据更新,而当对 id.num 进行赋值时,未触发数据更新,根据 步骤5 的代码可以看出,这其实是因为执行 set 的时候没有对设置的 value 进行处理,导致了 num 属性没有被设置监听。在这里的实例中,解决办法就比较简单粗暴了,只需要直接在 set 里将 set 接受的 value 放到 observe 函数里执行,就能够对 value 进行监听了,下面是最终的defineReactive函数代码以及测试例子:

// 重新定义属性,监听起来
function defineReactive(target, key, value){
  // 再次用value嵌套调用 observe 深,若为对象,则进行进一步监听,若非value非对象则直接返回
  observe(value)
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newVal){
      // 对于新增加的值进行深度监听,如 data.id = { num: 101 }, 新增加的 num 也将能够被监听到
      observe(newVal)
      // value 一直在闭包中,此处设置完成后,下次get能够获取最新设置的值
      if(newVal !== value){
        value = newVal
        // 触发更新
        updateView()
      }
    }
  })
}

// 测试数据
const data = {
  name: 'Yimwu',
  id: 001,
  information: {
    tel: '135xxxxx354',
    email: '[email protected]' 
  }
}

// 监听数据
observe(data)

// 测试
data.id = { num: 010 } // (监听成功)输出 --> 数据更新
data.id.num = 110 // (监听成功)输出 --> 数据更新
复制代码

三、视图更新优化———实现数组监听

在上一节【初步实现】中,已经实现了对对象的所有属性、嵌套属性进行监听,但是,如果 某个属性是一个数组 呢,对数组进行 push、pop 等操作,会触发更新吗?很显然是不会的,因为 Object.defineProperty 并不具备监听数组内部变化的能力,那么我们该如何解决呢————重写数组原型上的方法。

1、定义监听数组的原型

我们都知道,在 JS 中,任何对象都有原型,而我们的目的是通过重写数组原型上方法(push、pop等)实现监听,而作为库或是框架,我们都不应该去改变全局原型上的任何原生方法或者属性,污染全局环境,所以,这里分3步:

第一步:创建一个对象,将数组的原型赋值给该对象
const oldArrayProperty = Array.prototype
第二步:创建新对象,原型指向该对象
const arrProperty = Object.create(oldArrayProperty)
第三步:重写该对象上的方法
arrProperty.push = function(){} ...
arrProperty.pop = function(){} ...

// 重新定义数组原型,加入触发更新的机制
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向oldArrayProperty
const arrProperty = Object.create(oldArrayProperty)
// 重写原型上的方法(可以所有都重写,这里只进行少量举例)
// arrProperty.push = function(){} 
// arrProperty.pop = function(){}
// 优化写法
const methods = ['push','pop','shift','unshift','splice']
methods.forEach(method => {
  arrProperty[method] = function(){
    updateView()
    Array.prototype[method].call(this, ...arguments)
  }
})
复制代码

2、将需要监听的数组的原型指向自定义的特殊原型

Modify the original observe, add the array to judge, if it is an array, modify the prototype of the array, so far, the array monitoring is completed, the following is the modified code and test example of the observe

// 监听对象属性
function observe(target){
  if(typeof target !== 'object' || target === null) {
    // 不是数组或对象
    return target
  }
  // 如果是数组则修改该数组的原型
  if(Array.isArray(target)){
    target.__proto__ = arrProperty
    return
  }

  // 重新定义属性
  for(let key in target) {
    defineReactive(target, key, target[key])
  }
}

// 测试数据
const data = {
  myCars: ['Bugatti','Koenigsegg']
}

// 监听数据
observe(data)

// 测试
data.myCars.push('AE86') // (监听成功)输出 --> 数据更新
复制代码

4. Performance Analysis

In order to achieve full coverage of each nested property monitoring of an object, it is necessary to traverse the properties of the object deeply and recursively to the end , so the performance loss is very large, especially in the initialization phase, if there are a large number of very high-level The binding of the object for responsive monitoring will greatly consume the performance , resulting in slowing down the First Paint Time

Summarize

After using Vue for a period of time, or when we have been able to use Vue proficiently , we need to start to further explore the advanced usage and principles of Vue, fundamentally learn and understand the underlying principles of Vue, and after understanding the relevant design principles of Vue , which can enable us to break through the level of cool use and come to a more advanced level of well-used and skillful use. From the perspective of the underlying principles, it will be performance optimization and architecture design. The best breakthrough!

write at the end

The blogger will continue to update good articles in the future, welcome to pay attention to the blogger! !
If the article is helpful to you, please like , collect + follow and grow with the blogger! ! ❤❤❤

Guess you like

Origin juejin.im/post/7079807948830015502