Vue 视图更新原理 Vue 的视图更新原理 主要涉及的是响应式相关API Object.defineProperty 的使用,它的作用是为对象的某个属性对外提供 get、set 方法,从而实现外部对该属性的读和写操作时能够被内部监听 ,实现后续的同步视图更新功能
一、实现响应式的核心API:Object.defineProperty Object.defineProperty的用法介绍:MDN-Object.defineProperty ,下面是模拟 Vue data 值的更新对API接口进行初步了解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const data = {}let _myName = 'Yimwu' Object .defineProperty (data, "name" , { get : () => { console .log ('get' ) return _myName }, set : (newVal ) => { console .log ('set' ) _myName = newVal } }) console .log (data.name ) data.name = 'Mr.Wu' 复制代码
二、视图更新初步实现 1、updateView 为了方便 模拟视图更新 ,这里创建了一个函数 updateView ,当数据更新时,调用 updateView ,模拟进行了视图更新(在 Vue 中表现为 template 模板中引用了该变量值的 DOM 元素的变化 )
1 2 3 4 5 function updateView ( ){ console .log ('视图更新' ) } 复制代码
2、defineReactive 创建函数 defineReactive ,对 API Object.defineProperty 进行封装,接受三个参数,监听的目标对象、属性名,以及属性值,一个target(对象)通过调用 defineReactive 就能够实现对 key(对应属性名)进行监听 ,类比到 Vue 中:
1 2 3 4 5 6 7 8 <script> export default { data ( ){ name : 'yimwu' } } </script> 复制代码
具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function defineReactive (target, key, value ){ Object .defineProperty (target, key, { get ( ){ return value }, set (newVal ){ if (newVal !== value){ value = newVal updateView () } } }) } 复制代码
3、observe observe 主要是用于对对象中的每个属性进行 defineReactive 监听
1 2 3 4 5 6 7 8 9 10 11 12 function observe (target ){ if (typeof target !== 'object' || target === null ) { return target } for (let key in target) { defineReactive (target, key, target[key]) } } 复制代码
4、完整代码以及测试例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 function updateView ( ){ console .log ('视图更新' ) } function defineReactive (target, key, value ){ Object .defineProperty (target, key, { get ( ){ return value }, set (newVal ){ if (newVal !== value){ value = newVal updateView () } } }) } function observe (target ){ if (typeof target !== 'object' || target === null ) { return target } for (let key in target) { defineReactive (target, key, target[key]) } } const data = { name : 'Yimwu' , id : 001 , information : { tel : '135xxxxx354' , email : '15xxxxx@xx.com' } } 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,完整代码及测试例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function defineReactive (target, key, value ){ observe (value) Object .defineProperty (target, key, { get ( ){ return value }, set (newVal ){ if (newVal !== value){ value = newVal updateView () } } }) } const data = { name : 'Yimwu' , id : 001 , information : { tel : '135xxxxx354' , email : '15xxxxx@xx.com' } } observe (data)data.name = 'YI' data.information .tel = '00000000000' (监听成功)输出 --> 数据更新 复制代码
6、如何理解 Vue.set 在使用 Vue 的过程中,我们或许都有过这样子的经历,在 data 中定义了一个对象,然后在程序执行过程中给他动态添加了属性,然后对当我们对该新增属性进行值更新时并没有触发视图更新 ,作为Vue初学者时,将 data 响应式当成黑盒对待,就很难理解它为啥不更新,而今天拨开原理后,这里就很容易理解了
1 2 3 data.id = { num : 010 } data.id .num = 110 复制代码
如上图所示,当给 id 赋值 为一个对象时,触发了 id 的数据更新 ,而当对 id.num 进行赋值 时,未触发数据更新 ,根据 步骤5 的代码可以看出,这其实是因为执行 set 的时候没有对设置的 value 进行处理,导致了 num 属性没有被设置监听。在这里的实例中,解决办法就比较简单粗暴了,只需要直接在 set 里将 set 接受的 value 放到 observe 函数里执行,就能够对 value 进行监听了,下面是最终的defineReactive 函数代码以及测试例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 function defineReactive (target, key, value ){ observe (value) Object .defineProperty (target, key, { get ( ){ return value }, set (newVal ){ observe (newVal) if (newVal !== value){ value = newVal updateView () } } }) } const data = { name : 'Yimwu' , id : 001 , information : { tel : '135xxxxx354' , email : '15xxxxx@xx.com' } } 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(){} ...
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const oldArrayProperty = Array .prototype const arrProperty = Object .create (oldArrayProperty)const methods = ['push' ,'pop' ,'shift' ,'unshift' ,'splice' ]methods.forEach (method => { arrProperty[method] = function ( ){ updateView () Array .prototype [method].call (this , ...arguments ) } }) 复制代码
2、将需要监听的数组的原型指向自定义的特殊原型 对原来的 observe 进行修改,加入数组判断,如果是数组则修改该数组的原型,至此,数组监听完成,下面是 observe 修改后代码以及测试例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 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' ) 复制代码
四、性能分析 为了实现对象的每个嵌套 属性监听 的 全覆盖 ,需要对对象的属性进行 深度遍历,递归到底 ,所以对于性能的损耗是非常大的,特别是在初始化阶段,如果有大量的层级非常高的对象进行响应式监听的绑定,会 极大耗费 初始化时的 性能 ,导致拖慢 First Paint Time
总结 当使用 Vue 一段时间后,或者说已经能够熟练使用 Vue 的时候,我们就需要开始进一步挖掘 Vue 的高级用法、原理等,从根本上学习、了解 Vue 的 底层原理 ,通过了解 Vue 的相关 设计原理 后,能够使得我们在平时开发的过程中,突破 用得爽 这一层次,来到 用得好、用得巧 这样一种更加高级的层次,从底层原理的角度出发,将是性能优化以及架构设计的最好突破口!