熟悉vue
的小伙伴应该都知道,谈到vue
的原理,最重要的莫过于:响应式,虚拟dom
及diff
算法,模版编译,今天,咱们一块儿来深刻vue
的响应式,探讨vue2.x
响应式的实现原理与不足,以及vue3.0
版本如何重写响应式实现方案。javascript
vue
是一个MVVM
框架,所谓MVVM
,最核心的就是数据驱动视图,通俗一点讲就是,用户不直接操做dom
,而是经过操做数据,当数据改变时,vue
内部监听数据变化而后更新视图。一样,用户在视图上的操做(事件)也会反过来改变数据。而响应式,则是实现数据驱动视图的第一步,即监听数据的变化,使得用户在设置数据时,能够通知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
java
vue2.x
实现响应式我想绝大多数人有了解过vue,都应该或多或少的知道一些,vue响应式的核心就是Object.defineProperty()
, 这里简单作一个回顾react
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两个内部实现,让咱们能够获取或者设置这个属性(方法)算法
首先,咱们定义一个初始数据以下api
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) {
}
复制代码
最后咱们定义实现响应式的入口方法bash
function reactive () {
// ...
}
复制代码
咱们最终调用就是框架
const reactiveData = reactive(data)
复制代码
上面的数据,咱们模拟了一我的的简单信息介绍,能够看到对象的字断值有字符串,数字,布尔值,对象,数组。对于字符串,数字,布尔值这样的原始类型,咱们直接返回就行了
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方法(这个方法能够作任何事情)进行视图更新。
很明显,虽然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版本的响应式原理
经过咱们的分析,也就看到了vue2.x版本响应式实现的弊端:
Object.defineProperty()
这个api没法原生的对数组进行响应式监听Object.defineProperty()
这种实现,以及数组的实现,都存在一个问题,那就是没办法监听到后续的手动新增删除属性元素,好比数组,直接经过索引去设置和改变值是不会触发视图更新的,固然vue为咱们提供了vue.set
和vue.delete
这样的api
,但终究是不方便的vue3.0
实现响应式前不久vue3.0
也正式发布了,虽然尚未正式的推广,不过里面的一些变化是值得咱们去关注和学习的
Proxy
和Reflect
由于vue2.x版本响应式的实现存在的那些问题,vue
官方在3.0版本中彻底重写了响应式的实现,改用Proxy
和Reflect
代替Object.defineProperty()
。
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'
复制代码
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
完成响应式监听
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
。
本文详细深刻的剖析了vue
响应式原理,对于2.x
和3.0
版本的实现差别,各有利弊,没有什么方案是完美的,相信将来,当浏览器兼容问题愈来愈少的时候,生活会更美好!