熟悉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 () { // ... } 复制代码
咱们最终调用就是markdown
const reactiveData = reactive(data) 复制代码
上面的数据,咱们模拟了一我的的简单信息介绍,能够看到对象的字断值有字符串,数字,布尔值,对象,数组。对于字符串,数字,布尔值这样的原始类型,咱们直接返回就行了
function reactive (target) { // 首先,不是对象直接返回 if (typeof target !== 'object' || target === 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() } }) } function reactive (target) { // 首先,不是对象直接返回 if (typeof target !== 'object' || target === 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() } }) } function reactive (target) { // 首先,不是对象直接返回 if (typeof target !== 'object' || target === 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() } } }) } function reactive (target) { // 首先,不是对象直接返回 if (typeof target !== 'object' || target === 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() } } }) } function reactive (target) { // 首先,不是对象直接返回 if (typeof target !== 'object' || target === null) { return target } // 对于数组,原型修改 if (Array.isArray(target)) { target.__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(target)) { // target.__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 success = 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
版本的实现差别,各有利弊,没有什么方案是完美的,相信将来,当浏览器兼容问题愈来愈少的时候,生活会更美好!