学习 vue 源码 -- 响应式原理
概述
由于刚开始学习 vue 源码,而且水平有限,有理解或表述的不对的地方,还请不吝指教。
vue 主要通过 Watcher、Dep 和 Observer 三个类来实现响应式视图。另外还有一个 scheduler 来进行调度,本次暂时不做讨论。
Watcher 和 Dep 是订阅者和发布者的关系,每个 Watcher 可以订阅多个 Dep,而每个 Dep 也可以被多个 Watcher 订阅。当 Observer 监听的数据发生改变时,相应的 Dep 就会触发其订阅者 Watcher 更新视图。
下面通过一个简单的例子来说明其实现流程和原理:
<div id="app">
{{someVar}}
</div>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
someVar: 'init'
},
mounted(){
setTimeout(() => this.someVar = 'changed', 3000)
}
})
</script>
页面初始会显示 "init" 字符串,3秒钟之后,会更新为 "changed" 字符串。
为了便于理解,将整个流程分为三个阶段:
- 初始化data,这个阶段 vue 通过 Observer 监听 data 对象,并将普通的 someVar 属性代理为 get\set 属性
- 初次挂载el,这个阶段 vue 使用默认的 someVar 数据渲染视图,并将 watcher 添加到 dep 的订阅者列表
- someVar 更改触发视图更新,这个阶段 someVar 被赋予了新值,vue 根据 watcher 和 dep 的订阅关系触发视图的更新
下面我们来逐步分析这三个阶段的流程。
第一阶段
主要流程
new Vue(options) => vm._init(options) => initState(vm) => initData(vm) => observe(data) => new Observer(data) => defineReactive(data, key, value)
说明
初始化操作会监听 data 对象,对其每一个属性调用 defineReactive() 方法,将其改造为响应式属性,代码如下(去掉了不影响表述主流程的代码,以便能更清晰的抓住重点):
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any
) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
dep.depend()
return val
},
set: function reactiveSetter (newVal) {
if (newVal === val || (newVal !== newVal && val !== val)) {
return
}
val = newVal
dep.notify()
}
})
}
可以看到,当 get() 方法执行的时候,会调用 dep.depend() ;当 set() 方法执行时,会调用 dep.notify()。后面我们会看到这两个方法的作用。
第二阶段
主要流程
vm.$mount(el) => mountComponent(vm, el) => new Watcher(vm, updateComponent) => watcher.get() => updateComponent() => vm._update(vm._render())
说明
vm._render() 调用 vm.$options.render() 方法生成 vnode,vm._update() 方法根据 vnode 对视图做更新。
vm.$options.render() 方法是在 $mount() 方法中生成的,生成后的代码如下:
(function() {
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v("\n " + _s(someVar) + "\n ")])
}
})
可以看到,代码中会使用到 vm.someVar 属性,而该属性最终会代理到之前定义的响应式属性上,从而调用其 get() 方法,进而调用 dep.depend() 方法将 watcher 添加到订阅者列表。
dep.depend() => Dep.target.addDep(dep) => dep.addSub(watcher) => dep.subs.push(watcher)
dep.subs 就是 dep 的订阅者列表,通过这个流程,就建立起了 dep 和 watcher 之间的订阅关系。
其中,Dep.target 就是当前的 watcher,因为在 watcher.get() 方法执行时,有如下流程:
watcher.get() => pushTarget(watcher) => Dep.target = watcher
第三阶段
3秒之后,vm.someVar 被赋予了新的值,从而最终会调用到响应式属性的 set() 方法,进而调用 dep.notify(),触发 watcher 更新视图。
主要流程
dep.notify() => watcher.update() => watcher.run() => watcher.get() => watcher.getter.call(vm, vm) == updateComponent() => vm._update(vm._render())
说明
watcher 的 getter() 方法就是第二阶段 new Watcher(vm, updateComponent) 中的 updateComponent 方法,可以看到,通过这个流程,视图得到了更新。
以上就构成了一个响应式视图模型,其核心是利用 defineProperty() 方法将普通属性转换为带有钩子的 set\get 属性,从而实现了数据监听。
学习 vue 源码 -- 响应式原理的更多相关文章
- 手牵手,从零学习Vue源码 系列一(前言-目录篇)
系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 手牵手,从零学习Vue源码 系列三(虚拟DOM篇) 陆续更新中... 预计八月中旬更新 ...
- 手牵手,从零学习Vue源码 系列二(变化侦测篇)
系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...
- 一起学习vue源码 - Object的变化侦测
作者:小土豆biubiubiu 博客园:www.cnblogs.com/HouJiao/ 掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d 简书:h ...
- Vue数据绑定和响应式原理
Vue数据绑定和响应式原理 当实例化一个Vue构造函数,会执行 Vue 的 init 方法,在 init 方法中主要执行三部分内容,一是初始化环境变量,而是处理 Vue 组件数据,三是解析挂载组件.以 ...
- Vue 2.0 与 Vue 3.0 响应式原理比较
Vue 2.0 的响应式是基于Object.defineProperty实现的 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 prop ...
- 一起学习vue源码 - Vue2.x的生命周期(初始化阶段)
作者:小土豆biubiubiu 博客园:https://www.cnblogs.com/HouJiao/ 掘金:https://juejin.im/user/58c61b4361ff4b005d9e8 ...
- vue 数据劫持 响应式原理 Observer Dep Watcher
1.vue响应式原理流程图概览 2.具体流程 (1)vue示例初始化(源码位于instance/index.js) import { initMixin } from './init' import ...
- 手写实现vue的MVVM响应式原理
文中应用到的数据名词: MVVM ------------------ 视图-----模型----视图模型 三者与 Vue 的对应:view 对应 te ...
- 学习Vue源码前的几项必要储备(二)
7项重要储备 Flow 基本语法 发布/订阅模式 ES6+ 语法 原型链.闭包 函数柯里化 event loop 接上讲 聊到了ES6的几个重要语法,加下来到第四点继续开始. 4.原型链.闭包 原型链 ...
随机推荐
- 每天一个Linux命令(03):du命令
du命令 今天找开发定位问题,看到他使用了这个命令,查看文件,之前知道df,所以今天的每天系列把这命令 du命令也是查看使用空间的,但是与df命令不同的是Linux du命令是对文件和目录磁盘使用的空 ...
- Ubuntu16.04下的NetCore环境搭建(附录含Ubuntu 18.04 安装 NetCore2.1)
跨平台系列汇总:http://www.cnblogs.com/dunitian/p/4822808.html#linux VSCode安装:http://www.cnblogs.com/dunitia ...
- MySQL使用普通用户访问返回ERROR 1698 (28000): Access denied for user 'root'@'localhost'
这个问题最开始查资料都说要改密码,密码不对.其实不是这个样子都. 解决方法 修改/etc/mysql/my.cnf,添加以下内容 [mysqld] skip-grant-tables 重启mysql服 ...
- poj2259 Team Queue
吼哇,又是水题. 我本来准备开1010个queue的,但是STL容器里好像只有vector滋磁开组,于是只好数组模拟... 然后模拟过了...... #include <cstdio> # ...
- JS验证身份证
话不多说,直接看代码 JS部分 /** * 身份证15位编码规则:dddddd yymmdd xx p * dddddd:地区码 * yymmdd: 出生年月日 * xx: 顺序类编码,无法确定 * ...
- Day10--Python--动态传参,作用域
python的三目运算a = 10b = 20c = a if a > b else b #先判断中间的条件a > b是否成立,成立返回if前面的值,不成立返回else后面的值,也可以 c ...
- Mock3 moco框架的http协议post方法Mock的实现
新建一个 startupPost.json [ { "description":"模拟一个post请求", "request":{ &quo ...
- tensor flow中summary用法总结
对于用法的总结详细的参见博文https://www.cnblogs.com/lyc-seu/p/8647792.html
- poj 2385 Apple Catching(记录结果再利用的动态规划)
传送门 https://www.cnblogs.com/violet-acmer/p/9852294.html 题意: 有两颗苹果树,在每一时刻只有其中一棵苹果树会掉苹果,而Bessie可以在很短的时 ...
- php项目核心业务(增、删、改、查)(第三篇)
对增删改查数据库的封装 //php对数据库的封装 //Mysql_fetach($sql)函数查询所有的 function Mysql_fetach($sql){ $conn=mysqli_conne ...