面试的时候经常被问到 响应式 相关的内容,而vue3.0 更新后,面试官又有了新的武器;
面试官: 为什么 Vue3.0 要重写响应式系统?
懵逼树上懵逼果,懵逼树下你和我,面试官在问什么,我该怎么回答,完全不知道怎么回事;
有些经验的小伙伴可能会从解释 Proxy 的好处开始简单聊一下,比如: Proxy 是直接代理对象,而不是劫持对象的属性;更好的数组监控;
这样的回答,勉强算是合格吧
那到底应该怎么答呢?
面试官背后的出题逻辑
别急,咱们先整理一下思路,孙子兵法有云:“知己知彼,百战不殆”;面试就像打仗,你来我往,所以我们需要换位思考,想一想,为什么面试官会问这样一个问题?面试官想从这个问题里得到什么回答?这个问题可以考察哪些技术点? 想清楚这个问题,再回到自己身上,这些技术点,你都掌握了吗?
说得直白一点,面试就像考试,你需要先 读题、审题才能答好这道题;
为什么很多人认为 “面试造火箭,工作拧螺丝”?因为没有换位思考,没有想清楚面试题背后的逻辑;
那我们想清楚这个逻辑之后,需要我们做的就是提取技术点,整理思路,做出对应解答;当然, 前提是你需要具备这些技术能力
那么接下来,我就尝试拆解一下这个面试题了,提取其中的知识点。
对于你来说,就是要看看这些知识点,你都掌握了多少?
为什么 Vue3.0 要重写响应式系统 ?
为什么重写?如果之前好好的,重写就没有意义,那之前存在什么问题,现在是怎么解决的?就是关键点了;
不知道你对 Vue2.x 的响应式掌握多少,是不是欠下了技术的债呢?没关系,我来帮你还债,先梳理 Vue2.x 的响应式;
其实基于这个面试题,背后还有很多技术点,上面这些,是与当前题目有直接关系的,实际面试中,很有可能基于这些技术点,在进行深入交流,这里就不扩展了,你能把现在这些问题理清楚,就算赚到了;
Vue2.x 响应式
其实关于这一点,在Vue 的官方文档中,早已经有过说明了,而且说得非常详细;官方文档: https:// cn.vuejs.org/v2/guide/r eactivity.html
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
我们使用官方给的一张图示,来梳理整个流程;
我们先来看一段代码
响应式原理
data 中的 obj 就是一个普通的 JavaScript 对象,通过点击 Click 按钮,将获取到的随机数赋值给 this.message ,而 this.message 指向的就是 data 中 obj 对象的 message 属性;当message 发生数据改变时,页面中 H1 标签的内容会随之改变,这个过程就是就是响应式的;那么Vue 是如何实现的呢?
首先,Vue 内部使用 Object.defineProperty() 将 Data 中的每一个成员都转换为 getter / setter 的形式;getter 用来依赖收集,setter 用来派发更新;而模板内容,最终会被编译为 render 函数,在 render 函数中,我们能发现 _v(_s(message)) message 被访问了,就会触发 getter 来进行依赖收集,而在代码中的点击事件中,一旦事件处理程序被触发执行,那么 message 则会被修改,就会触发 setter来进行派发更新;
虽然流程理清楚了,但是总感觉少点什么,怎么才能更通透呢?
我们用代码来模拟整个的实现过程;
defineProperty 模拟代码
defineProperty 的基本用法,直接看手册就行了: https:// developer.mozilla.org/z h-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
我们来看看代码:
<div id="app">
hello
</div>
<script>
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello'
}
// 模拟 Vue 的实例
let vm = {}
// 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作
Object.defineProperty(vm, 'msg', {
// 可枚举(可遍历)
enumerable: true,
// 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义)
configurable: true,
// 当获取值的时候执行
get () {
console.log('get: ', data.msg)
return data.msg
},
// 当设置值的时候执行
set (newValue) {
console.log('set: ', newValue)
if (newValue === data.msg) {
return
}
data.msg = newValue
// 数据更改,更新 dom 的值
document.querySelector('#app').textContent = data.msg
}
})
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
</script>
你没有看错,加上注释,一共 36行代码,这就是 Vue2.x 对响应式实现的整个流程;
继续实现多个数据的响应式
<body>
<div id="app">
hello
</div>
<script>
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 10
}
// 模拟 Vue 的实例
let vm = {}
proxyData(data)
function proxyData(data) {
// 遍历 data 对象的所有属性
Object.keys(data).forEach(key => {
// 把 data 中的属性,转换成 vm 的 setter/setter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('get: ', key, data[key])
return data[key]
},
set (newValue) {
console.log('set: ', key, newValue)
if (newValue === data[key]) {
return
}
data[key] = newValue
// 数据更改,更新 DOM 的值
document.querySelector('#app').textContent = data[key]
}
})
})
}
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
</script>
</body>
上面的代码只是模拟了 响应式 的原理,但Vue在实现中,肯定不会那么简单,接下来,我们看一下源码呀……
Vue2 源码解读
首先找到响应式代码的处理位置:
关键位置 | 作用 | 源码位置 |
---|---|---|
function Vue () {} | Vue 构造函数 | core/instance/index.js:8 |
Vue.prototype._init | 初始化组件实例对象 | core/instance/init.js:16 |
initState | 初始化组件状态相关成员 | core/instance/state.js:48 |
initData | 初始化用户传入的 data 数据 | core/instance/state.js:112 |
observe | 观察 data | core/observer/index.js:110 |
关键位置 | 作用 | 源码位置 |
---|---|---|
observe | 观察 data | core/observer/index.js:110 |
class Observer | Observer 逻辑 | core/observer/index.js:37 |
walk | 遍历对象成员分别处理 | core/observer/index.js:64 |
definereactive | 为组件实例定义响应式数据 | core/observer/index.js:135 |
Object.defineProperty | 拦截数据的访问和修改 | core/observer/index.js:157 |
看完Vue2.x 响应式的代码,我们再回过头来思考最开始的问题, 为什么 Vue3.0 要重写响应式系统 ?
为什么重写?如果之前好好的,重写就没有意义,那之前存在什么问题,换句话问就是 defineProperty 有什么问题?
Object.defineProperty 的问题
其实, defineProperty 的问题,在Vue2.x 的手册中,已经说过了;“哎,很多人就是不看文档啊”
https:// cn.vuejs.org/v2/guide/r eactivity.html#%E5%AF%B9%E4%BA%8E%E6%95%B0%E7%BB%84
下面分别使用 Vue2 和 Vue3 实现了一个小功能,代码一模一样,功能当然也一样,但是,在 Vue2 中就会有Bug,而运行在vue3中的,则没有任何问题;
Vue2:
<template>
<div class="about">
<h1>This is an about page</h1>
<p v-for="(v, k) in users">
{{ v.names }}
</p>
<button @click="changes">更新</button>
</div>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, names: "路飞-v2" },
{ id: 2, names: "鸣人-v2" },
],
};
},
methods: {
changes() {
// this.users[0] = {id:'0',names:'liuneng'}
// this.users[1].names = 'lnsdsdfg'
this.users[1] = { id: "1", names: "刘能-v2" };
},
},
};
</script>
<style lang="stylus" scoped></style>
Vue3:
<template>
<div class="about">
<h1>This is an about page</h1>
<p v-for="(v, k) in users">
{{ v.names }}
</p>
<button @click="changes">更新</button>
</div>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, names: "路飞-v3" },
{ id: 2, names: "鸣人-v3" },
],
};
},
methods: {
changes() {
// this.users[0] = {id:'0',names:'liuneng'}
// this.users[1].names = 'lnsdsdfg'
this.users[1] = { id: "1", names: "刘能-v3" };
},
},
};
</script>
其核心点在于 defineProperty 不能很好的实现对数组下标的监控,而在 Vue2 的实现代码中,没有更好的方案对此进行改善,尤大索性直接放弃了实现;关于这个问题,尤大也在 github 做过回应,截个图给大家看看;
那么,Vue 目前还没有解决的问题,Vue3中显然是已经解决的了,问题是,Vue3 是如何解决的呢?前面在 Vue3 的代码中我们使用的是传统的 Options api 来实现的数据响应式, 而在 Vue3 中全新的 Composition Api 也实现了响应式系统,我们先来感受一下 Composition Api 的基础用法
Composition API 的响应式系统
ref 响应式
<template>
<!-- 不需要.value -->
<button @click="addNu"> Composition API: {{nu}}</button>
</template>
<script>
// 引入 ref
import {ref} from "vue"
export default {
setup() {
// 定义 ref 响应式数据
const nu = ref(1);
// 定义函数
function addNu(){
nu.value++;
}
// 将数据和方法返回,即可在模板中直接使用
return {
nu,
addNu
};
},
};
</script>
reactive 响应式
<template>
<!-- 不需要.value -->
<button @click="addNu"> Composition API: {{nu}}</button>
<!-- reactive 响应式数据 -->
<h2>{{revData.name}}</h2>
<h3 @click="ageAdd">年龄:{{revData.age}}</h3>
<p v-for="(v,k) in revData.skill"> {{v}} </p>
</template>
<script>
// 引入 ref
import {reactive, ref} from "vue"
export default {
setup() {
// 定义 ref 响应式数据
const nu = ref(1);
// reactive 响应式数据
const revData = reactive({
name: '路飞',
age: 22,
skill:['橡胶机关枪','吃鸡腿'],
})
function ageAdd(){
revData.age++
}
// 定义函数
function addNu(){
nu.value++;
}
// 将数据和方法返回,即可在模板中直接使用
return {
nu,
addNu,
revData,
ageAdd
};
},
};
</script>
Vue3 中的响应式是如何实现的呢?关键点在于Proxy 函数;
Proxy 实现原理
使用 Proxy 实现的响应式代码,要比使用 defineProperty 的代码简单得多,因为 Proxy 天然的能够对整个对象做监听,而不需要对数据行遍历后做监听,同时也就解决了数组下标的问题;
我们来一段模拟代码看一下:
<div id="app">
hello
</div>
<script>
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 0
}
// 模拟 Vue 实例
const vm = new Proxy(data, {
// 执行代理行为的函数
// 当访问 vm 的成员会执行
get (target, key) {
console.log('get, key: ', key, target[key])
return target[key]
},
// 当设置 vm 的成员会执行
set (target, key, newValue) {
console.log('set, key: ', key, newValue)
if (target[key] === newValue) {
return
}
target[key] = newValue
document.querySelector('#app').textContent = target[key]
}
})
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
</script>
来自:https://zhuanlan.zhihu.com/p/346241358
安装 vue-cli3,在使用任何 @vue/composition-api 提供的能力前,必须先通过 Vue.use() 进行安装,安装插件后,您就可以使用新的 Composition API 来开发组件了。
Vue3 就是基于 Proxy 对其数据响应系统进行了重写,现在这部分可以作为独立的模块配合其他框架使用。数据响应可分为三个阶段: 初始化阶段 --> 依赖收集阶段 --> 数据响应阶段
在2019.10.5日发布了Vue3.0预览版源码,但是预计最早需要等到 2020 年第一季度才有可能发布 3.0 正式版。新版Vue 3.0计划并已实现的主要架构改进和新功能:
有关即将发布的 Vue.js 的第 3 个主要版本的信息越来越多。通过下面的讨论,虽然还不能完全确定其所有内容,但是我们可以放心地认为,它将是对当前版本(已经非常出色)的巨大改进。 Vue 团队在改进框架 API 方面做得非常出色
用新的 Vue 3 编写的程序效果会很好,但性能并不是最重要的部分。对开发人员而言,最重要的是新版本将会怎样影响我们编写代码的方式。如你所料,Vue 3 带来了许多令人兴奋的新功能。值得庆幸的是
emmm 用半天时间捋顺了 vue3 的源码,再用半天时间写了个 mini 版……我觉得我也是没谁了,vue3 的源码未来一定会烂大街的,我们越早的去复现它,就……emm可以越早的装逼hhh
10 月 5 日,尤雨溪在 GitHub 开放了 Vue 3.0 处于 pre-alpha 状态的源码,这次 Vue 3.0 Updates 版本的更新,将带来五项重大改进:速度体积、可维护性、面向原生、易用性
从一开始使用 Vue 时,对于之前的 jq 开发而言,一个很大的区别就是基本不用手动操作 dom,data 中声明的数据状态改变后会自动重新渲染相关的 dom。换句话说就是 Vue 自己知道哪个数据状态发生了变化及哪里有用到这个数据需要随之修改。
Vue 开发团队终于在今天发布了 3.0-beta.1 版本,也就是测试版。通常来说,从测试版到正式版,只会修复 bug,不会引入新功能,或者删改老功能。所以,如果你对新版本非常感兴趣,或者有新项目即将上马,不妨尝试一下新版本
对于大多数单页应用程序而言,管理路由是一项必不可少的功能。随着新版本的Vue Router处于Alpha阶段,我们已经可以开始查看下一个版本的Vue中它是如何工作的。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!