Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!
2013年7月28日,尤雨溪第一次在 GItHub 上为 Vue.js 提交代码;2015年10月26日,Vue.js 1.0.0版本发布;2016年10月1日,Vue.js 2.0发布。
最早的 Vue.js 只做视图层,没有路由, 没有状态管理,也没有官方的构建工具,只有一个库,放到网页里就可以直接用了。
后来,Vue.js 慢慢开始加入了一些官方的辅助工具,比如路由(Router)、状态管理方案(Vuex)和构建工具(Vue-cli)等。此时,Vue.js 的定位是:The Progressive Framework。翻译成中文,就是渐进式框架。
Vue.js2.0 引入了很多特性,比如虚拟 DOM,支持 JSX 和 TypeScript,支持流式服务端渲染,提供了跨平台的能力等。Vue.js 在国内的用户有阿里巴巴、百度、腾讯、新浪、网易、滴滴出行、360、美团等等。
Vue 已是一名前端工程师必备的技能,现在就让我们开始深入学习 Vue.js 内部的核心技术原理吧!
什么是渐进式框架
首先来解答一下,什么是渐进式框架。前言已经讲到,Vue2.0 引入了很多特性,比如 Router,Vuex 和 Vue-cli。所谓渐进式框架,就是把框架分层。
最核心的部分是声明式渲染,然后往外是组件系统(组件机制),客户端路由器(路由机制-Router),大规模的状态管理(Vuex),最外层是构建工具(Vue-cli),如下图所示。
也就是说,你既可以只用最核心的声明式渲染部分来开发项目,也可以在最核心的基础上逐步增加其他特性,如使用声明式的渲染和组件系统来开发项目,当然你也可以使用一整套 Vue.js 全家桶来开发项目。Vue.js 拥有足够的灵活性供用户选择。
什么是变化侦测
从状态生成DOM,再输出到用户界面展示的一整套流程叫做渲染,应用在运行时会不断地进行渲染。
对于响应式框架来说,变化侦测是其重要的组成,可以监听状态的变化,然后通知视图层进行相应的更新,从而达到响应式的效果。
对于 Angular 和 React 来说,状态发生了改变,框架并不知道哪个状态变化了,只知道有状态变了,然后框架会进行暴力比对来找出哪个状态变化,从而进行 DOM 节点重新渲染。
在 Vue.js 中,当一个状态发生了变化,Vue.js 立刻就知道了,并且还知道是哪个状态发生了变化,进而在该状态绑定的依赖中进行比对,最后找到需要渲染的 DOM 节点重新渲染。
所谓状态绑定的依赖,可以这么理解,一个状态的会引起一些 DOM 节点的重新渲染,那么这些 DOM 节点就是这个状态所绑定的依赖。
Object.defineProperty 和 Proxy 的区别
Object.defineProperty 用于监听对象的数据变化,无法监听属性的新增删除;无法监听数组变化;只能劫持对象的属性,当对象的属性值也是对象时,那么需要深度遍历,对属性值继续监听。
Proxy 可以理解为在被劫持的对象之前加了一层拦截,Proxy 返回的是一个新对象, 可以通过操作返回的新的对象达到监听目的,可以直接监听整个对象,而非属性;可以监听数组变化。有关 Proxy 的详细讲解,请移步
ES6 新特性梳理系列丨Proxy 和 Reflect
。
变化的追踪
由于 Object 和 Array 的变化侦测采用不同的处理方式,所以我们这篇文章将详细介绍Object 的变化侦测。
所谓变化的追踪,其实就是检测一个对象的某个属性的变化,在 JavaScript 中,有两种方式可以监听对象的变化,一种是 Object.defineProperty,另一种是ES6 的 Proxy。由于浏览器兼容性问题以及 ES6 的支持度不够,Vue2.0 采用的是 Object.defineProperty,后来,Vue3.0 采用的是 ES6 的 Proxy。本文旨在讲解 Vue2.0 的变化侦测。
那么 Vue2.0 是如何对状态的变化进行追踪的呢?知道了 Vue2.0 是根据Object.defineProperty 监测状态变化的,那么下面的代码相比你并不陌生。
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
return val
},
set:function(newVal){
if(newVal == val){
return
}
val = newVal
}
})
Object.definProperty(obj,prop,desc) 的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性。
-
obj:要定义属性的对象。
-
prop:要定义或修改的属性的名称 。
-
desc:要定义或修改的属性描述符。
desc 描述符有以下几种:
-
confingurable:定义当前属性的描述符能否被改变。
-
enumerable:定义当前属性是否是可枚举的,也就是能否被for…in 所遍历出来。
-
value:当前属性对应的值。
-
writable:当前属性是否可以被改变。
-
getter:当访问该属性时,会调用此函数。该函数的返回值会被作为该属性的值。
-
setter:当前属性值被修改时,会调用此函数。
那么我们就可以从上面代码看出,是在监听 data 对象中的 key 属性的改变,如果外界访问该属性,则进入 getter 方法,返回当前值;如果外界修改当前属性,则进入 setter 方法,判断修改的值是否和之前值相同,不同再修改。
依赖的收集
我们先来解释什么是依赖?所谓状态绑定的依赖,可以这么理解,一个状态的变化会引起一些 DOM 节点的重新渲染,那么这些 DOM 节点就是这个状态所绑定的依赖。
通过 Object.definProperty 可以在 setter 方法中监听对象属性的改变,也就是监听状态的改变,我们要做的就是当状态发生改变时,可以通知那些绑定的依赖进行 DOM 节点的重新渲染。
显而易见,我们是在 getter中收集依赖,在 setter 中触发依赖,进行 DOM 的重新渲染。
依赖收集到了哪里
可以这么理解,每一个 key 都有一个依赖数组,我们可以对上面的代码进行改造。
function defineReactive(data,key,val){
let arr = [] // 依赖保存的数组
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
arr.push(依赖)
return val
},
set:function(newVal){
if(newVal == val){
return
}
for(let i = 0;i<Arr.length;i++){
// 循环依赖数组,触发收集到的依赖
arr[i](newVal,val)
}
val = newVal
}
})
}
我们新增了数组 arr,用来存储被收集的依赖,然后在 set 被触发时,循环依赖数组,触发收集到的依赖。
递归监听所有的属性
我们已经知道在哪里收集依赖和怎么触发依赖了,但是现在我们还只局限于对象的单层监听,如果对象的属性还是一个对象怎么办呢,我们希望把数据中的所有属性(包括子属性)都监听到,所以我们需要递归监听。
我们封装一个 Observer 类,该类的作用是将对象中的所有属性都转换成 getter、setter 的形式,去监听他们的变化。还封装一个 YiLai 类,帮助我们管理依赖。有关 Class 类的相关知识,请移步
ES6 新特性梳理系列丨Class
// Observer 类旨在监听对象的所有属性
class Observer{
constructor(value){
this.value = value
// 判断是否是数组,不是则监听此对象的所有属性
if(!Array.isArray(value)){
this.jianting(value)
}
}
jianting(obj){
// 获取所有的key
const keys = Object.keys(obj)
// 循环键(key)的数组,监听当前对象所有属性
for(let i = 0;i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]])
}
}
}
// YiLai类旨在帮助我们管理依赖
class YiLai{
constructor(){
this.yilaiArray = []
}
addYiLai(yilai){
this.yilaiArray.push(yilai)
}
jianting(){
const yilais = this.yilaiArray.slice()
for(let i = 0;i<yilais.length;i++){
// 触发依赖,通知DOM节点重新渲染
}
}
}
function defineReactive(data,key,val){
// 判断当前属性是否是对象,是则继续监听
if(typeof val === 'object'){
new Observer(val)
}
// 创建依赖管理实例
let yilai = new YiLai()
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
// 收集依赖
yilai.addYiLai('依赖')
return val
},
set:function(newVal){
if(newVal == val){
return
}
// 触发依赖
yilai.jianting()
val = newVal
}
})
}
在上面代码中,我们定义了 Observer 类,用来将一个正常的 object 转换成被监听的 object,然后判断数据类型,只有是 object,才会调用 jianting 方法将每一个属性转换成 getter、setter 的形式。
最后,在 defineReactive 中新增 new Observer(val) 来递归子属性这样我们就可以把 data 中的所有属性都转换成了 getter、setter 的形式了。
当 data 的属性发生变化时,与这个属性对应的依赖就会被触发,这也就实现了对象的响应式。
关于Object的问题
由于 Object.defineProperty 方法自身原因,Vue2.0 无法监听到一个对象属性的新增和删除,为了解决这个问题,Vue.js 提供了两个API,vm.$set 和 vm.$delete,我们将会在后期为大家详细介绍。
Vue 进阶系列教程将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!
叶阳辉
HFun 前端攻城狮