图解 Vue 响应式原理
- Vue 初始化
- 模板渲染
- 组件渲染
为了便于理解,本文将从以下两个方面进行探索:
从 Vue 初始化,到首次渲染生成 DOM 的流程。
从 Vue 数据修改,到页面更新 DOM 的流程。
Vue 初始化
先从最简单的一段 Vue 代码开始:
"""
<template>
<div>
{{ message }}
</div>
</template>
<script>
new Vue({
data() {
return {
message: "hello world",
};
},
});
</script>
"""
这段代码很简单,最终会在页面上打印一个 hello world,它是如何实现的呢?
我们从源头:new Vue 的地方开始分析。
"""
// 执行 new Vue 时会依次执行以下方法
// 1. Vue.prototype._init(option)
// 2. initState(vm)
// 3. observe(vm._data)
// 4. new Observer(data)
// 5. 调用 walk 方法,遍历 data 中的每一个属性,监听数据的变化。
function walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
// 6. 执行 defineProperty 监听数据读取和设置。
function defineReactive(obj, key, val) {
// 为每个属性创建 Dep(依赖搜集的容器,后文会讲)
const dep = new Dep();
// 绑定 get、set
Object.defineProperty(obj, key, {
get() {
const value = val;
// 如果有 target 标识,则进行依赖搜集
if (Dep.target) {
dep.depend();
}
return value;
},
set(newVal) {
val = newVal;
// 修改数据时,通知页面重新渲染
dep.notify();
},
});
}
"""
数据描述符绑定完成后,我们就能得到以下的流程图:

图中我们可以看到,Vue 初始化时,进行了数据的 get、set 绑定,并创建了一个 Dep 对象。
对于数据的 get、set 绑定我们并不陌生,但是 Dep 对象什么呢?
Dep 对象用于依赖收集,它实现了一个发布订阅模式,完成了数据 Data 和渲染视图 Watcher 的订阅,我们一起来剖析一下。
"""
class Dep {
// 根据 ts 类型提示,我们可以得出 Dep.target 是一个 Watcher 类型。
static target: ?Watcher;
// subs 存放搜集到的 Watcher 对象集合
subs: Array<Watcher>;
constructor() {
this.subs = [];
}
addSub(sub: Watcher) {
// 搜集所有使用到这个 data 的 Watcher 对象。
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 搜集依赖,最终会调用上面的 addSub 方法
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 调用对应的 Watcher,更新视图
subs[i].update();
}
}
}
"""
根据对 Dep 的源码分析,我们得到了下面这张逻辑图:

了解 Data 和 Dep 之后,我们来继续揭开 Watcher 的面纱。
"""
class Watcher {
constructor(vm: Component, expOrFn: string | Function) {
// 将 vm._render 方法赋值给 getter。
// 这里的 expOrFn 其实就是 vm._render,后文会讲到。
this.getter = expOrFn;
this.value = this.get();
}
get() {
// 给 Dep.target 赋值为当前 Watcher 对象
Dep.target = this;
// this.getter 其实就是 vm._render
// vm._render 用来生成虚拟 dom、执行 dom-diff、更新真实 dom。
const value = this.getter.call(this.vm, this.vm);
return value;
}
addDep(dep: Dep) {
// 将当前的 Watcher 添加到 Dep 收集池中
dep.addSub(this);
}
update() {
// 开启异步队列,批量更新 Watcher
queueWatcher(this);
}
run() {
// 和初始化一样,会调用 get 方法,更新视图
const value = this.get();
}
}
"""
源码中我们看到,Watcher 实现了渲染方法 _render
和 Dep 的关联, 初始化 Watcher 的时候,打上
Dep.target 标识,然后调用 get 方法进行页面渲
染。加上上文的 Data,目前 Data、Dep、Watcher
三者的关系如下:

我们再拉通串一下整个流程:
Vue 通过defineProperty 完成了 Data 中所有数据
的代理,当数据触发 get 查询时,会将当前的
Watcher 对象加入到依赖收集池 Dep 中,当数据
Data 变化时,会触发 set 通知所有使用到这个 Data
的 Watcher 对象去 update 视图。
目前的整体流程如下:

上图的流程中 Data 和 Dep 都是 Vue 初始化时创建的,但现在我们并不知道 Wacher 是从哪里创建的,带着这个问题,我们接着往下探索。
模板渲染
上文中,我们分析了初始化 Vue 过程中处理数据的
部分,接下来,我们分析一下数据渲染的部分。
其实 new Vue 执行到最后,会调用 mount 方法,
将 Vue 实例渲染成 dom 。
"""
// new Vue 执行流程。
// 1. Vue.prototype._init(option)
// 2. vm.$mount(vm.$options.el)
// 3. render = compileToFunctions(template) ,编译 Vue 中的 template 模板,生成 render 方法。
// 4. Vue.prototype.$mount 调用上面的 render 方法挂载 dom。
// 5. mountComponent
// 6. 创建 Watcher 实例
const updateComponent = () => {
vm._update(vm._render());
};
// 结合上文,我们就能得出,updateComponent 就是传入 Watcher 内部的 getter 方法。
new Watcher(vm, updateComponent);
// 7. new Watcher 会执行 Watcher.get 方法
// 8. Watcher.get 会执行 this.getter.call(vm, vm) ,也就是执行 updateComponent 方法
// 9. updateComponent 会执行 vm._update(vm._render())
// 10. 调用 vm._render 生成虚拟 dom
Vue.prototype._render = function (): VNode {
const vm: Component = this;
const { render } = vm.$options;
let vnode = render.call(vm._renderProxy, vm.$createElement);
return vnode;
};
// 11. 调用 vm._update(vnode) 渲染虚拟 dom
Vue.prototype._update = function (vnode: VNode) {
const vm: Component = this;
if (!prevVnode) {
// 初次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
} else {
// 更新
vm.$el = vm.__patch__(prevVnode, vnode);
}
};
// 12. vm.__patch__ 方法就是做的 dom diff 比较,然后更新 dom,这里就不展开了。
"""
看完 Vue 模板渲染的过程,我们可以得到如下的流程图:

到这里,我们就知道了 Watcher 其实是在 Vue 初始
化的阶段创建的,属于生命周期中 beforeMount 的
位置创建的,创建 Watcher 时会执行 render 方法,
最终将 Vue 代码渲染成真实的 DOM。
我们再将之前的流程整合一下,就能得到以下的流程:

上图分析了 Vue 初始化到渲染 DOM 的整个过程,最后我们再分析一
下,当数据变化时,Vue 又是怎么进行更新的?
其实,在上图也能看出,在 Data 变化时,会调用 Dep.notify 方法,随
即调用 Watcher 内部的 update 方法,此方法会将所有使用到这个 Data
的 Watcher 加入一个队列,并开启一个异步队列进行更新,最终执行
_render 方法完成页面更新。
整体的流程如下:

好了,探索到这里,Vue 的响应式原理,已经被我们分析透彻了,如果你
还没有明白,不妨再细品一下上图。
组件渲染
本来探索到上面的流程图就结束了,但好奇的我又想到了一个问题
Vue 组件又是怎么渲染的呢?
带着这个问题,我继续查阅了源码。
"""
// 从模板编译开始,当发现一个自定义组件时,会执行以下函数
// 1. compileToFunctions(template)
// 2. compile(template, options);
// 3. const ast = parse(template.trim(), options)
// 4. const code = generate(ast, options)
// 5. createElement
// 6. createComponent
export function createComponent(
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
// $options._base 其实就是全局 Vue 构造函数,在初始化时 initGlobalAPI 中定义的:Vue.options._base = Vue
const baseCtor = context.$options._base;
// Ctor 就是 Vue 组件中 <script> 标签下 export 出的对象
if (isObject(Ctor)) {
// 将组件中 export 出的对象,继承自 Vue,得到一个构造函数
// 相当于 Vue.extend(YourComponent)
Ctor = baseCtor.extend(Ctor);
}
const vnode = new VNode(`vue-component-${Ctor.cid}xxx`);
return vnode;
}
// 7. 实现组件继承 Vue,并调用 Vue._init 方法,进行初始化
Vue.extend = function (extendOptions: Object): Function {
const Super = this;
const Sub = function VueComponent(options) {
// 调用 Vue.prototype._init,之后的流程就和首次加载保持一致
this._init(options);
};
// 原型继承,相当于:Component extends Vue
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
return Sub;
};
"""
看完组件渲染的源码后,结合上文,重新整理了一张流程图,图中的蓝色
部分就是渲染组件的过程。

好了,现在是真的结束了,最终的流程图就是上面的这一张图。
问个问题,现在你理解 Vue 响应式原理了吗?
如果仍觉得不好理解,我这里还准备了一张带标注的简图

思考与总结
本文从源码的角度,介绍了 Vue 响应式原理,来简单回顾一下吧。
从 new Vue 开始,首先通过 get、set 监听 Data 中的数据变化,同时创建 Dep 用来搜集使用该 Data 的 Watcher。
编译模板,创建 Watcher,并将 Dep.target 标识为当前 Watcher。
编译模板时,如果使用到了 Data 中的数据,就会触发 Data 的 get 方法,然后调用 Dep.addSub 将 Watcher 搜集起来。
数据更新时,会触发 Data 的 set 方法,然后调用 Dep.notify 通知所有使用到该 Data 的 Watcher 去更新 DOM。
图解 Vue 响应式原理的更多相关文章
- 深度解析 Vue 响应式原理
深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还是进 ...
- Vue源码--解读vue响应式原理
原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...
- 详解Vue响应式原理
摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...
- vue响应式原理,去掉优化,只看核心
Vue响应式原理 作为写业务的码农,几乎不必知道原理.但是当你去找工作的时候,可是需要造原子弹的,什么都得知道一些才行.所以找工作之前可以先复习下,只要是关于vue的,必定会问响应式原理. 核心: / ...
- 深入Vue响应式原理
深入Vue.js响应式原理 一.创建一个Vue应用 new Vue({ data() { return { name: 'yjh', }; }, router, store, render: h =& ...
- vue响应式原理解析
# Vue响应式原理解析 首先定义了四个核心的js文件 - 1. observer.js 观察者函数,用来设置data的get和set函数,并且把watcher存放在dep中 - 2. watcher ...
- 浅析Vue响应式原理(三)
Vue响应式原理之defineReactive defineReactive 不论如何,最终响应式数据都要通过defineReactive来实现,实际要借助ES5新增的Object.definePro ...
- 深入解析vue响应式原理
摘要:本文主要通过结合vue官方文档及源码,对vue响应式原理进行深入分析. 1.定义 作为vue最独特的特性,响应式可以说是vue的灵魂了,表面上看就是数据发生变化后,对应的界面会重新渲染,那么响应 ...
- 浅谈vue响应式原理及发布订阅模式和观察者模式
一.Vue响应式原理 首先要了解几个概念: 数据响应式:数据模型仅仅是普通的Javascript对象,而我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率. 双向绑定:数据改变,视图 ...
- Vue 响应式原理模拟以及最小版本的 Vue的模拟
在模拟最小的vue之前,先复习一下,发布订阅模式和观察者模式 对两种模式有了了解之后,对Vue2.0和Vue3.0的数据响应式核心原理 1.Vue2.0和Vue3.0的数据响应式核心原理 (1). ...
随机推荐
- MDC轻量化日志链路跟踪的若干种应用场景
"If debugging is the process of removing software bugs, then programming must be the process of ...
- 一文搞懂JavaScript数组的特性
前言 数组是几乎所有编程语言的基础语法,JavaScript因为语法特性,之前缺少一些集合类对象,对数组的使用就会更多一些,因此我们更需要理解数组知识. 然而大部分人对数组都已经非常熟悉了,所以本文将 ...
- 使用Ref还是Reactive?
我喜欢Vue 3的Composition API,它提供了两种方法来为Vue组件添加响应式状态:ref和reactive.当你使用ref时到处使用.value是很麻烦的,但当你用reactive创建的 ...
- Android 通过scheme跳转支付宝实现支付
原文地址: Android 通过scheme跳转支付宝实现支付 - Stars-One的杂货小窝 需求的来源是支付功能,由于支付宝不允许个人开通具有webhook的支付服务,所以是对接了一个支付系统( ...
- 前端 引用svg图片,支持动态切换颜色
当我们添加一张svg图片显示时,react提示找不到文件. 我们可以在全局文件global.d.ts内,添加图片类型的声明: 详见<TypeScript 引用资源文件后提示找不到的错误处理方案& ...
- [Pytorch框架] 4.1 Fine tuning 模型微调
文章目录 4.1 Fine tuning 模型微调 4.1.1 什么是微调 为什么要微调 迁移学习 Transfer Learning 二者关系 4.1.2 如何微调 4.1.3 注意事项 4.1.3 ...
- Node + Express 后台开发 —— 登录标识
登录标识 系统通常只有登录成功后才能访问,而 http 是无状态的.倘若直接请求需要登录才可访问的接口,假如后端反复查询数据库,而且每个请求还得带上用户名和密码,这都是不很好. 作为前端,我们听过 c ...
- CTFShow 反序列化 Web 255-266
CTFShow 反序列化 255-266 漏洞原理 未队用户输入的序列化字符串进行检测,导致攻击者可以控制反序列化过程,从而导致代码执行,SQL注入,目录遍历等后果. 触发条件 unserialize ...
- 2020-12-02:mysql中,一张表里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 Mysql 重启,再 insert 一条记录,这条记
2020-12-02:mysql中,一张表里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 Mysql 重启,再 insert 一条记录,这条记 ...
- 2021-03-28:定义一种数:可以表示成若干(数量>1)连续正数和的数 。比如:5 = 2+3,5就是这样的数 ;12 = 3+4+5,12就是这样的数 。1不是这样的数,因为要求数量大于1个、连续正数和 。2 = 1 + 1,2也不是,因为等号右边不是连续正数 。给定一个参数N,返回是不是可以表示成若干连续正数和的数 。
2021-03-28:定义一种数:可以表示成若干(数量>1)连续正数和的数 .比如:5 = 2+3,5就是这样的数 :12 = 3+4+5,12就是这样的数 .1不是这样的数,因为要求数量大于1 ...