Vue原理
Vue原理
kilito一、Vue原理
- 响应式系统
- 学习
Vue
中如何实现数据的响应式系统,从而达到数据驱动视图。
- 学习
- vue中选项方法
- 学习watch选项 $watch方法 computed选项 $set方法 $nextTick $mount方法的封装
- template 编译过程
- 学习
Vue
内部是怎么把template
模板编译成虚拟DOM
,从而渲染出真实DOM
- 学习
- 虚拟 dom 生成与更新
- 学习什么是虚拟 DOM,以及
Vue
中的DOM-Diff
原理
- 学习什么是虚拟 DOM,以及
二、Vue2 学习路线图
下面这张流程图中表示了vue的关键部分的执行过程,和核心函数。我们可以根据这样一个过程来自己实现一个vue框架。
通过梳理Vue初始化的过程,我们发现实现一个类似于Vue的框架主要需要实现这几部分 响应式系统框架、虚拟dom编译渲染机制 MVVM更新机制,接下来我们先从最基本的响应式系统开始,自己动手写一个Vue的简单框架
【思考】Vue在初始化的过程中主要经历的哪些步骤
【回答】
1、初始化Vue构造函数,挂载属性 方法
2、模板编译成render函数
3、通过Watcher收集依赖
4、diff更新dom
5、渲染dom
【补充】vue是一个标准的MVVM框架么?
Vue 并不完全是一个MVVM框架MVVM只能数据驱动视图,视图更改数据,而不能通过其他方式操作数据。在vue中我们也可以自己手动修改数据,所以vue并不是一个完全意义上的MVVM框架。
三、Vue2 响应式原理
从这一小节开始我们带着大家实现一个Vue框架
我们先来看看面试宝典中的关于Vue响应式的八股文 (P143-4)
相信绝大多数的同学看到这个八股文都会感觉头大。学完今天的内容,我们都会对怎么回答vue的响应式原理有了自己的理解。
下面我们一起来揭秘vue的响应式原理到底是怎么实现的!
1、章节概述
我们首先实现学习路线中第一条分支,从状态初始化到数据响应式的过程
所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制。MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理。
Vue 的响应式原理依赖于Object.defineProperty,Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
所以在vue中的数据响应式原理主要是给data绑定一个观察着 observe 让数据变成可观察的,我们首先来看源码然后自己尝试手写一个observe
2、环境准备
在这一小节中我们开始自己实现一个Vue框架,通过Vue源码我们了解到Vue使用的rollup构建工具进行打包
/package.json
这里我们也使用Rollup实现项目打包,我们之前有学习过脚手架工具webpack,Rollup和webpack的区别在于项目类代码中有大量的代码拆分,构建项目类型的应用显然webpack更为合适,如果想要构建js类库将多个模块打包成一个大的文件rollpu更加合适,同时rollup中提供的tree-shake可以帮助我们自动删除冗余代码
Webpack | Rollup |
---|---|
vue-cli, create-react-app 各类应用脚手架 | react,vue,three.js,D3,moment |
1、源码工程的初始化
1、新建项目文件夹,在文件夹下初始化工程
1 | npm init -y |
获得package.json
2、安装Rollup打包依赖
1 | // 1,安装 rollup:用于 Vue 源码的打包构建 |
3、创建Vue.js文件
创建打包入口:src/index.js
1 | // src/index.js Vue 构造函数 |
4、创建 Rollup 配置文件
rollup 默认配置文件:项目根目录下rollup.config.js
文件
创建 rollup.config.js,完成 rollup、babel 相关配置:
1 | // src/rollup.config.js |
5、创建 rollup 构建脚本
执行 Rollup 打包构建 Vue,创建 rollup-script 构建脚本:
1 | // package.json |
dev 脚本解释:
- rollup 命令:默认会去找 node_module/bin/rollup;
- -c:config 选项,使用配置文件,默认找 rollup.config.js;
- -w:watch 选项,监听文件变化;当文件发生变化时重新打包;
6、打包构建 Vue
执行构建脚本 npm run dev
将 src/index.js
输出至 dist/vue.js
其中,vue.js.map
为 sourcemap 源码映射文件
7、创建 Html 引入 Vue
创建 dist/index.html
引入 dist/vue.js
,打印输出 Vue:
1 |
|
在浏览器中打开index.html
,查看控制台输出,此时一个Vue
的构建环境就搭建完成了
3、Vue函数的封装
【目标】封装一个 Vue 函数并且在 index.html 中引入
【前置知识】
在js中函数和class都可以new,如下:有啥区别呢 ?
class 类是用于创建对象的模板。
我们使用 class 关键字来创建一个类,类体在一对大括号 {} 中,我们可以在大括号 {} 中定义类成员的位置,如方法或构造函数。
每个类中包含了一个特殊的方法 **constructor()**,它是类的构造函数,这种方法用于创建和初始化一个由 class 创建的对象。
创建一个类的语法格式如下:
1 | class ClassName { constructor() { ... } } |
在js中除了class函数也可以new,函数本身就是对象,在js中每定义一个函数都会同时生成一个以这个函数体为构造函数的对象,不信你试试
可以通过对象来new出一个新的对象。定义 function Vue(options){} 时, 实际上生成了一个Function类型(预定义类型)的对象,对象名叫Vue,对象的构造函数就是这个函数的体。如下
我们在初始化Vue
项目的时候使用到new
关键字,这里的vue是使用函数定义的。目的是提升vue的灵活性。
1 | function Vue (options) { |
【思考】为什么vue使用函数定义而其他的watcher observer使用class定义?
【回答】核心目的:提升Vue的灵活性:
1、class的所有方法都是不可枚举的,而function声明的函数是可以枚举的。用户可以根据需要定制重写(重载)vue提供的成员方法
2、function 既能当常规函数来用,又能当做函数的属性来用,又能当类来用。相对class更加灵活。
3、对于内部定义的不希望修改的方法,通过class来定义,另外class声明的函数会有变量提升。
下面我们实例化一个Vue
1 | let vm = new Vue({ |
此时在 vm 实例上就具有了 name 和 data 属性
接下来我们在src/index.js
中定义这个类并导出。在构造函数中获取传入的options
并挂载到vue
实例上。
1 | // src/index.js |
并且在 dist/index.html中实例化一个Vue对象。
1 | <!-- 引入 vue.js,将会绑定到 window--> |
此时我们访问Vue
对象中data里面定义的数据不能直接访问,必须通过vue.data.xxx
访问,实际在Vue
项目中data
里面定义的数据是可以直击访问的,所以我们需要给data
中的数据添加一个代理实现数据的直接访问。
4、核心函数 Object.defineProperty 的介绍和简单响应式的实现
【目标】能够了解Object.defineProperty的用法,并且实现一个简单的响应式
为了实现vue中的数据代理,我们需要首先了解一下vue中的响应式核心方法Object.defineProperty
Object.defineProperty 在 vue2 中起到了非常重要的作用,通过Object.defineProperty实现了数据的代理,数据响应式原理,以及vue中的一些重要成员方法。下面我们学习Object.defineProperty的基本概念和用法。
语法:**Object.defineProperty(obj, prop, descriptor)**
其中:obj要在其上定义属性的对象。prop要定义或修改的属性的名称。descriptor将被定义或修改的属性描述符。
参数: 1、obj : 第一个参数就是要在哪个对象身上添加或者修改属性
2、prop : 第二个参数就是添加或修改的属性名
3、desc : 配置项,一般是一个对象
1 | desc 的详细配置 |
1 | <script> |
【思考】什么是响应式?
【回答】对外界的变化做出反应
【思考】数据响应式的核心思想是什么?
【回答】将数据变成可观察的
下面我们来实现一个简单的数据响应式过程,在 dist 下新建一个 **defineproperty.html **
1 |
|
【思考】数据获取的时候触发的哪个方法,数据修改的时候触发的哪个方法?
【回答】获取触发get 修改触发set
【思考】上面这段代码实现了什么呢?
【回答】实现了数据的双向绑定,数据改变视图改变,视图改变数据也改变
【思考】定义defineReactive函数的时候,里面第三个参数value的作用?
【回答】在函数体内部形成闭包结构,用开来拓展函数内部变量的作用域
5、通过data代理,实现数据的访问
【目标】实现data的代理可以直接通过vm实例获取data中定义的数据
【思考】在 vue 中的数据是存放在 data 中为什么可以通过 vm.XXX 直接访问数据呢?
【回答】通过数据代理实现数据的访问
此时我们想获取数据 a 需要通过 Vue.$options.data.a ,但是在 vue 中只需要 this.a 就可以获取到 a 的值,这是怎么实现的呢?
在 Vue 中,可以在外部直接通过vm实例进行数据访问和操作:
1 | let vm = new Vue({ |
当前代码中,外部通过vue实例只能拿到 vue.$options
,想要拿到data
需要 vue.$options.data
,要想实现vue.message
和vue.$options.data.message
等效,就需要想办法将vue
实例操作“代理”到$options.data
上;这样,就实现了 Vue 的数据代理我们来观察一下vue的实例,在实例上有一个_data属性 还有我们定义的变量。
首先,先做一次代理,将data
挂载到 vue._data下(因为Object.defineProperty的第一个参数必须为一的对象,我们,第一层代理更加方便我们在实现属性的追加),这样 vue 实例就能够在外部通过vue._data.message
获取到data.message
;
之后,再做一次代理,将 vue 实例操作 vue.message 代理到 vue.data 上,这样,外部就可以直接通过vue.message 获取到 data.message;
Vue 状态初始化阶段,通过 observe() 实现数据响应式之后,通过 Object.defineProperty 对 _data 中的数据操作进行劫持;将 vue.xxx 在 vue 实例上的取值操作,代理到 vue._data.xxx 上,这样可以简化书写。
下面我们开始实现数据的代理
data
挂载到 vue._data下
1 | function Vue (options) { |
将vue实例操作vue.message代理到vue._data上
1 | // 数据代理 实现非侵入的数据修改 |
然后在Vue的构造器中使用proxy方法代理数据
1 | function Vue (options) { |
此时我们再访问vue对象中的数据,就不需要.data了,观察打印结果:当从vue实例取值时,就会被代理到vm._data取值;
【总结】vue中实现数据直接访问的实现步骤
1、将 data 暴露在 vue._ data 实例属性上
2、利用 Object.defineProperty 将 vue.xxx 操作代理到 vue. _data 上