深入vue响应式原理(包含vue3.0)
2020/3/2 11:16:07
本文主要是介绍深入vue响应式原理(包含vue3.0),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
熟悉vue
的小伙伴应该都知道,谈到vue
的原理,最重要的莫过于:响应式,虚拟dom
及diff
算法,模版编译,今天,我们一起来深入vue
的响应式,探讨vue2.x
响应式的实现原理与不足,以及vue3.0
版本如何重写响应式实现方案。
1. 什么是响应式
vue
是一个MVVM
框架,所谓MVVM
,最核心的就是数据驱动视图,通俗一点讲就是,用户不直接操作dom
,而是通过操作数据,当数据改变时,vue
内部监听数据变化然后更新视图。同样,用户在视图上的操作(事件)也会反过来改变数据。而响应式,则是实现数据驱动视图的第一步,即监听数据的变化,使得用户在设置数据时,可以通知vue
内部进行视图更新
比如
<template> <div> <div> {{ name }} </div> <button @click="changeName">改名字</button> </div> </template> <script> export default { data () { return { name: 'A' } }, methods: { changeName () { this.name = 'B' } } } </script> 复制代码
上面代码,点击button
按钮后,name
属性会改变,同时页面显示的A
会变成B
2. vue2.x
实现响应式
2.1 核心API --- Object.defineProperty()
我想绝大多数人有了解过vue,都应该或多或少的知道一些,vue响应式的核心就是Object.defineProperty()
, 这里简单做一个回顾
const data = {} let name = 'A' Object.defineProperty(data, 'name', { get () { return name }, set (val) { name = val } }) console.log(data.name) // get() data.name = 'B' // set() 复制代码
上面代码中我们可以看到,Object.defineProperty()的用法就是给一个对象定义一个属性(方法),并提供set和get两个内部实现,让我们可以获取或者设置这个属性(方法)
2.2 如何实现响应式
首先,我们定义一个初始数据如下
const data = { name: 'A', age: 18, isStudent: true, gender: 'male', girlFriend: { name: 'B', age: '19', isStudent: true, gender: 'female', parents: { mother: { name: 'C', age: '44', isStudent: false, gender: 'female' }, father: { name: 'D', age: '46', isStudent: false, gender: 'male' } } }, hobbies: ['basketball', 'one-piece', 'football', 'hiking'] } 复制代码
我们同样定义一个渲染视图的方法
function renderView () { // 数据变化时,渲染视图 } 复制代码
以及一个实现响应式的核心方法,这个方法接收三个参数,target
就是数据对象本身,key
和value
是对象的key
以及对应的value
function bindReactive (target, key, value) { } 复制代码
最后我们定义实现响应式的入口方法
function reactive () { // ... } 复制代码
我们最终调用就是
const reactiveData = reactive(data) 复制代码
2.2.1 对于原始类型和对象
上面的数据,我们模拟了一个人的简单信息介绍,可以看到对象的字断值有字符串,数字,布尔值,对象,数组。对于字符串,数字,布尔值这样的原始类型,我们直接返回就好了
function reactive () { // 首先,不是对象直接返回 if (typeof target !== 'object' || val === null) { return target } } const reactiveData = reactive(data) 复制代码
如果字段值是对象这样的引用类型,我们就需要对对象进行遍历,分别设置对对象的每一个key值做Object.defineProperty()
,注意,这个过程是需要递归调用的,因为如我们给出的数据所示,对象可能是多层嵌套的。我们定义一个函数bindReactive
来描述响应式监听对象的过程
function bindReactive (target, key, value) { Object.defineProperty(target, key, { get () { return value }, set (val) { value = val // 触发视图更新 renderView() } }) } // val是对象key对应的value function reactive (val) { // 首先,不是对象直接返回 if (typeof target !== 'object' || val === null) { return target } // 遍历对象,对每个key进行响应式监听 for (let key in target) { bindReactive(target, key, target[key]) } } const reactiveData = reactive(data) 复制代码
考虑到递归,我们需要在执行核心方法bindReactive
开始时,递归的调用reactive
为对象属性进行响应式监听,同时设置(更新)数据时候也要递归的调用reactive
更新,于是我们的核心方法bindReactive
变为
function bindReactive (target, key, value) { reactive(value) Object.defineProperty(target, key, { get () { return value }, set (val) { reactive(val) value = val // 触发视图更新 renderView() } }) } // val是对象key对应的value function reactive (val) { // 首先,不是对象直接返回 if (typeof target !== 'object' || val === null) { return target } // 遍历对象,对每个key进行响应式监听 for (let key in target) { bindReactive(target, key, target[key]) } } const reactiveData = reactive(data) 复制代码
上面的代码可以做一步优化,就是set的时候,如果新设置的值和之前的值相同,不触发视图更新,于是我们的方法变为
function bindReactive (target, key, value) { reactive(value) Object.defineProperty(target, key, { get () { return value }, set (newVal) { if (newVal !== value) { reactive(newVal) value = newVal // 触发视图更新 renderView() } } }) } // val是对象key对应的value function reactive (val) { // 首先,不是对象直接返回 if (typeof target !== 'object' || val === null) { return target } // 遍历对象,对每个key进行响应式监听 for (let key in target) { bindReactive(target, key, target[key]) } } const reactiveData = reactive(data) 复制代码
目前,我们以及实现了对于原始类型和对象的响应式监听,当数据变化时,会在数据更新后,调用renderView方法(这个方法可以做任何事情)进行视图更新。
2.2.2 对于数组
很明显,虽然Object.defineProperty()
很好的完成了对于原始类型和普通对象的响应式监听,但是这个方法对数组是无能为力的。那么,vue是如何实现数组的响应式监听的呢?
我们首先再次回到vue的官方文档
可以看到,vue在执行数组的push, pop, shift, unshift
等方法的时候,是可以响应式监听到数组的变化,从而触发更新视图的。
但是我们都知道,数组原生的这些方法,是不具有响应式更新视图能力的,所以,我们可以知道,vue
一定是改写了数组的这些方法,于是,现在问题就从数组如何实现响应式变成了,如何改写数组的api。
这里要用到的核心方法就是Object.create(prototype)
,这个方法就是创建一个对象,他的原型指向参数prototype
,于是,我们也可以实现对这些数组方法的改写了:
// 数组的原型 const prototype = Array.prototype // 创建一个新的原型对象,他的原型是数组的原型(于是newPrototype上具有所有数组的api) const newPrototype = Object.create(prototype) const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] methods.forEach(method => { newPrototype[method] = () => { prototype[method].call(this, ...args) // 视图更新 renderView() } }) 复制代码
实现了数组的响应式,我们完善入口方法reactive
function bindReactive (target, key, value) { reactive(value) Object.defineProperty(target, key, { get () { return value }, set (newVal) { if (newVal !== value) { reactive(newVal) value = newVal // 触发视图更新 renderView() } } }) } // val是对象key对应的value function reactive (val) { // 首先,不是对象直接返回 if (typeof target !== 'object' || val === null) { return target } // 对于数组,原型修改 if (Array.isArray(val)) { value.__proto__ = newPrototype } // 遍历对象,对每个key进行响应式监听 for (let key in target) { bindReactive(target, key, target[key]) } } const reactiveData = reactive(data) 复制代码
到目前为止,我们已经讲述清楚了vue2.x版本的响应式原理
2.3 vue2.x版本响应式实现方案的弊端
通过我们的分析,也就看到了vue2.x版本响应式实现的弊端:
Object.defineProperty()
这个api无法原生的对数组进行响应式监听- 实现过程中对于深度嵌套的数据,递归消耗大量性能
- 我们注意到,
Object.defineProperty()
这种实现,以及数组的实现,都存在一个问题,那就是没办法监听到后续的手动新增删除属性元素,比如数组,直接通过索引去设置和改变值是不会触发视图更新的,当然vue为我们提供了vue.set
和vue.delete
这样的api
,但终究是不方便的
3. vue3.0
实现响应式
前不久vue3.0
也正式发布了,虽然还没有正式的推广,不过里面的一些变化是值得我们去关注和学习的
3.1 Proxy
和Reflect
因为vue2.x版本响应式的实现存在的那些问题,vue
官方在3.0版本中完全重写了响应式的实现,改用Proxy
和Reflect
代替Object.defineProperty()
。
3.1.1 Proxy
首先来看MDN对Proxy的定义:
The Proxy object is used to define custom behavior for fundamental operations(e.g. property lookup, assignment, enumeration, function invocation, etc). 复制代码
翻译为中文大概就是:Proxy对象用来给一些基本操作定义自定义行为(比如查找,赋值,枚举,函数调用等等) 基本用法:
let proxy = new Proxy(target, handler) 复制代码
上面的参数意义:(注意target
可以是原生数组)
target
: 用Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组
,函数,甚至另一个代理)。handler
: 一个对象,其属性是当执行一个操作时定义代理的行为的函数。
举个栗子:
let handler = { get: function(target, name){ return name in target ? target[name] : 'sorry, not found'; } }; let 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, 'sorry, not found' 复制代码
3.1.2 Reflect
首先来看MDN对Reflect的定义:
Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of proxy handlers. Reflect is not a function object, so it's not constructible. 复制代码
大概意思就是说:Reflect 是一个内置的对象,提供拦截 JavaScript 操作的方法。这些方法与proxy的 handlers相同。Reflect不是一个函数对象,因此它是不可构造的。
Refelct对象提供很多方法,这里只介绍实现响应式会用到的几个常用方法:
Reflect.get()
: 获取对象身上某个属性的值,类似于target[name]
。Reflect.set()
: 将值分配给属性的函数。返回一个Boolean
,如果更新成功,则返回true
。Reflect.has()
: 判断一个对象是否存在某个属性,和in
运算符 的功能完全相同。Reflect.deleteProperty()
: 作为函数的delete操作符,相当于执行 delete target[name]。
于是,我们可以联合Proxy
和Reflect
完成响应式监听
3.2 Proxy
和Reflect
实现响应式
下面直接贴出代码,对之前我们实现的方法进行改造:
function bindReactive (target) { if (typeof target !== 'object' || target == null) { // 不是对象或数组,则直接返回 return target } // 因为Proxy原生支持数组,所以这里不需要自己实现 // if (Array.isArray(value)) { // value.__proto__ = newPrototype // } // 传给Proxy的handler const handler = { get(target, key) { const reflect = Reflect.get(target, key) // 当我们获取对象属性时,Proxy只会递归到获取的层级,不会继续递归子层级 return bindReactive(reflect) }, set(target, key, val) { // 重复的数据,不处理 if (val === target[key]) { return true } // 这里可以更具是否是已有的key,做不同的操作 if (Reflect.has(key)) { } else { } const succuss = Reflect.set(target, key, val) // 设置成功与否 return success }, deleteProperty(target, key) { const success = Reflect.deleteProperty(target, key) // 删除成功与否 return success } } // 生成proxy对象 const proxy = new Proxy(target, handler) return proxy } // 实现数据响应式监听 const reactiveData = bindReactive(data) 复制代码
上述代码我们可以看到,对于vue2.x
响应式存在的问题,都得到了很好的解决:
Proxy
支持监听原生数组Proxy
的获取数据,只会递归到需要获取的层级,不会继续递归Proxy
可以监听数据的手动新增和删除
那是不是vue3.0
的响应式方案就是完美的呢,答案是否定的,主要原因在于Proxy
和Reflect
的浏览器兼容问题,且无法被polyfill
。
4. 总结
本文详细深入的剖析了vue
响应式原理,对于2.x
和3.0
版本的实现差异,各有利弊,没有什么方案是完美的,相信未来,当浏览器兼容问题越来越少的时候,生活会更美好!
这篇关于深入vue响应式原理(包含vue3.0)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南