likes
comments
collection
share

Vue: 掌握2.x和3.x响应式原理赚到了一杯奶茶^o^

作者站长头像
站长
· 阅读数 64

注:高频面试内容及技术理解应用(建议收藏)

前言

缘起

这里稍微解释下,这个一级标题“缘起”,就是我当时学习的时候所遇到的问题然后去查证,最后深入理解源码后,做出的总结,最后才有这篇文章。

在还没有出Vue3.x之前,自己曾在使用vue2.x的过程中,遇到过动态新增,删除对象属性的时候,响应式无法同步的问题。

我也是跟诸多新手入门的JYM一样,刚上手Vue2.x的时候就经常遇到一个问题,数据更新了啊,为何页面不更新呢?什么时候用$set更新,什么时候用$forceUpdate强制更新。

所以后面查阅官方文档才知道,可以通过特定 set/delete API 来弥补这个无法响应式监听这番操作的缺陷;出于追根究底的我,开始了一番源码学习,才知道这个响应式的实现,最主要是通过defineProperty也可以说根源都是Object.defineProperty。

知识收益

说实话,多掌握点知识真的很有用,或许有时候,你比别人厉害一些,可能就是因为你多深入理解了深层次的原理;这不,有个工作了3年(在北京而且工资都比我高6k)都还没理解这个问题的同行,来找我,叫我帮他看看这个问题,我一看就锁定了问题的原因,请看记录

Vue: 掌握2.x和3.x响应式原理赚到了一杯奶茶^o^

Vue: 掌握2.x和3.x响应式原理赚到了一杯奶茶^o^

Vue: 掌握2.x和3.x响应式原理赚到了一杯奶茶^o^

好家伙,我说“天啊”,下一句的话,有人看到这,可以脑补一下我是怎么说的哈哈😂!,想知道更多后续情节,欢迎评论区发言

看到这,我感觉我是被萌化了,这个时候我已经安静了,后面他按照我说的重新修改了

Vue: 掌握2.x和3.x响应式原理赚到了一杯奶茶^o^

Vue: 掌握2.x和3.x响应式原理赚到了一杯奶茶^o^

就这样一顿奶茶红包到手🐶,总共花费2分钟

言归正传

接下来言归正传,来说说Vue2.x和Vue3.x关于响应式的故事吧

其实在Vue3.x 还没有发布beta的时候, 很火的一个话题就是Vue3.x 将使用Proxy 取代Vue2.x 版本的 Object.defineProperty

这里就简单对比一下Object.definePropertyProxy

  1. Object.defineProperty只能劫持对象的属性, 而Proxy是直接代理对象
  2. 由于Object.defineProperty只能劫持对象属性,需要遍历对象的每一个属性,如果属性值也是对象,就需要递归进行深度遍历。但是Proxy直接代理对象, 不需要遍历操作
  3. Object.defineProperty对新增属性需要手动进行Observe

因为Object.defineProperty劫持的是对象的属性,所以新增属性时,需要重新遍历对象, 对其新增属性再次使用Object.defineProperty进行劫持。也就是Vue2.x中给数组和对象新增属性时,需要使用$set才能保证新增的属性也是响应式的,$set内部也是通过调用Object.defineProperty去处理的

剖析

剖析Vue2.x的响应式

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

具体步骤

第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:

1、在自身实例化时往属性订阅器(dep)里面添加自己

2、自身必须有一个update()方法

3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

Vue: 掌握2.x和3.x响应式原理赚到了一杯奶茶^o^

简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点)

详情步骤:

首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。

然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)

剖析Vue3.x的响应式

这里我简单结合vue2.x对比下,方便理解和记忆

Object.defineProperty()语法

重点:vue为什么对数组对象的深层监听无法实现,因为组件每次渲染都是将data里的数据通过defineProperty进行响应式或者双向绑定上,之前没有后加的属性是不会被绑定上,也就不会触发更新渲染

Object.defineProperty( Obj, 'name', {
    enumerable: true, //可枚举
    configurable: true, //可配置
    // writable:true, //跟可配置不能同时存在
    // value:'name',  //可写死直
    get: function () {
        return def
    },
    set: function ( val ) {
        def = val
    }
} )

Proxy的语法

对比了上面两种语法是不是就懂了,defineProperty只能响应首次渲染时候的属性,Proxy需要的是整体,不需要关心里面有什么属性,而且Proxy的配置项有13种,可以做更细致的事情,这是之前的defineProperty无法达到的,Proxy的做法有点像是springMVC中的AOP切面思想,全局拦截监听或者也可以理解为代理proxy

//两个参数,对象,13个配置项
const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    },
    set:function(){ },
    ...13个配置项
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37

这位字节跳动的大佬讲得更为详细深入,可以细品 Vue3 的响应式和以前有什么区别,Proxy 无敌?

总结

很显然,Vue2 中响应式是存在一些缺陷的

  • 初始化时需要遍历对象所有 key,如果对象层次较深,性能不好
  • 通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
  • 无法监听到数组元素的变化,只能通过劫持重写了几个数组方法
  • 动态新增,删除对象属性无法拦截,只能用特定 set/delete API 代替
  • 不支持 Map、Set 等数据结构

而在 Vue3 中则是用 Proxy 进行重构,采用全局监听思想方法,完全取代了 defineProperty,就不存在上述问题了。

那么看到这里的掘友可能会问,为啥就不存在了呢?我当时也是带着这个疑问去找问题的

先看下Vue2在首次渲染时的响应式处理, 源码地址: src/core/observer/index.js - 157行

...
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () { ... },
    set: function reactiveSetter (newVal) { ... }
})
...

很显然,通过参数可以看出,它是需要根据具体的 key 去 obj 里找 obj[key],来进行拦截处理的,所以就有需要满足一个前置条件,一开始就得知道 key 是啥,所以就需要遍历每一个 key,并定义 getter、setter,这也是为什么后面添加的属性没有响应式的原因

而 Vue3 中则是这样的

// ref 源码      `packages/reactivity/ref.ts -142行`
// reactive 源码 `packages/reactivity/reactive.ts -173行`
new Proxy(target,{  // target 为组件的 data 返回的对象
  get(target, key){},
  set(target, key, value){}
})

同样由参数就可以看出,开始创建响应式的时候,根本不需要知道这个对象里有哪些字段,因为不用传具体的 key,这样就算是后面新增的,自然也能够拦截得到.

也就是说不会上来就递归遍历把所有用到没用到的都设置响应式,从而加快了首次渲染。 能够把概念和源码里面的逻辑理解清楚,能够很大程度上帮助你在开发中定位问题,和快速解决问题

转载自:https://juejin.cn/post/7195327225669877821
评论
请登录