从模仿开始理解vue2的数据响应式
Posted 一腔诗意醉了酒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从模仿开始理解vue2的数据响应式相关的知识,希望对你有一定的参考价值。
文章目录
- 学习目标
- 学习过程
- 1. 让一个数据会变
- 2. 让一个对象会变
- 3. 源码阅读
- 测试代码
- 跟踪调用栈
- 自己画的一个流程图
- talk is cheap, show me the code
- `src\\core\\instance\\index.js`
- `src\\core\\instance\\init.js`
- `src\\core\\instance\\state.js\\initState`
- `src\\core\\instance\\state.js\\initData`
- ``src\\core\\instance\\state.js\\proxy`
- `src\\core\\observer\\index.js\\observe`
- `src\\core\\observer\\index.js\\Observer类`
- `src\\core\\observer\\dep.js\\Dep类`
- `src\\core\\observer\\watcher.js\\Watcher类`
- 学习成果
学习目标
- 搞清楚什么是响应式
Vue
怎么知道我们数据更新了- 模拟数据响应式
- 通过阅读
Vue2
源码,理解Vue的双向数据绑定原理,可以跟面试官拉扯- 什么是
Watcher
- vue里面有多少种Watcher
- 什么是
Dep
学习过程
所谓数据响应式,不过是当依赖发生变化的时候,目标(视图)自动更新。
所以,要想理解数据响应式,我们先来尝试一下怎么让数据自动发生变化。
1. 让一个数据会变
模拟数据响应式,即当数据的依赖发生变化的时候,
target
也发生变化。如:let c = a+b;
这里我们把
c
称为target
,a,b
是c
的依赖,因为c
是根据a
和b
得出来的。
那么我们怎么让c在a或者b发生变化得时候,c也跟着发生变化呢?
1.1 我首先想到的是,将c放在一个函数里面,当a或者b发生变化的时候,我们调用一下那个函数,让c回炉重造不就可以变化了吗?
让我们来测试一下我的想法。
"use strict"; let a = 1 let b = 2 let c = null; function fn() c =a+b; console.log('1-',a,b,c) // 1- 1 2 null // 明显这里c 为null // 调用一下函数,让c得到初始化 fn() console.log('2-',a,b,c) // 2- 1 2 3 // 到了这里c=3 // 依赖发生变化 a = 9 console.log('3-',a,b,c) // 3- 9 2 3 // 这里a发生变化, 但是c并没有发生变化 fn() console.log('4-',a,b,c) // 4- 9 2 11 // 调用fn之后c=11,发生了变化
很明显到了这里,我们手动调用函数target确实会更新,但是老是手动的话,感觉怪怪的,那有没有什么办法,可以让他自动调用呢?
到了这里我们要解决的问题有两个:
1. 怎么知道自动数据发生变化 2. 怎么自动调用一个特定函数
1.2 我想到的办法是红宝书里面看到的Object.defineProperty()
,利用Object.defineProperty()
的getter
以及setter
的拦截特性, 让我i们来测试一下。
"use strict"; function say() console.log('hello') function defineReactive(obj, key, val) return Object.defineProperty(obj, key, get() console.log('get->', key) return val , set(newVal) if (newVal === val) return; console.log(`set $key from $val to $newVal`) // 数据发生变化,我们就调用函数 say() val = newVal ) let source = defineReactive(source, 'a', 1) console.log(source.a) source.a = 99 console.log(source.a)
结果:
可以看到我们对a的get以及set 都被识别到了,而且say函数也被成功调用了。
那我们怎么复现
c = a + b
这个例子呢?如下:"use strict"; function defineReactive(obj, key, val) return Object.defineProperty(obj, key, get() console.log('get->', key) return val , set(newVal) if (newVal === val) return; console.log(`set $key from $val to $newVal`) // 数据发生变化,我们就调用函数 fn() val = newVal ) let source = defineReactive(source, 'a', 1) defineReactive(source, 'b', 2) let c; function fn() c = source.a + source.b; console.log('c自动变化成为',c) // 初始化一下c fn() source.a = 99
我们发现fn虽然被自动调用了,但是c的值依然是3,那应该怎么解决呢?
经过查看发现是set里面的问题(先调用了fn导致val还没来得及发生变化。)
set(newVal) if (newVal === val) return; console.log(`set $key from $val to $newVal`) // 数据发生变化,我们就调用函数 val = newVal fn()
上面的问题就解决了。
2. 让一个对象会变
因为对象操作比较复杂,所以我们先实现对对象操作的拦截,比如对象的获取与设置我都知道。
2.1 拦截到对对象属性的获取与设置
新加observe对一个对象的属性遍历进行重新定义(类似于定义一个数据可变)
"use strict";
/**
* 这里的defineReactive实际上是一个闭包,
* 外面的对面引用着函数内的变量,导致这些临时变量一直存在
*/
function defineReactive(obj, key, val)
// 2. observe 避免key的val是一个对象,对象里面的值没有响应式
observe(val)
// 利用getter setter 去拦截数据
Object.defineProperty(obj,key,
get()
console.log('get', key)
return val
,
set(newVal)
if( newVal !== val)
console.log(`set $val -> $newVal`)
val = newVal
)
// 2. 观察一个对象,让这个对象的属性变成响应式
function observe(obj)
// 希望传入的是一个Object
if( typeof obj !== 'object' || typeof(obj) == null)
return ;
Object.keys(obj).forEach(key=>
defineReactive(obj, key, obj[key])
)
let o = a: 1, b: 'hello', c:age:9
observe(o)
o.a
o.a = 2
o.b
o.b = 'world'
2.2 让对象属性变化
为了简化程序,我们只看一层的对象
"use strict";
const log = console;
let target = null;
let data = a: 1, b:2
let c, d;
// 依赖收集,每个Object的key都有一个Dep实例
class Dep
constructor()
this.deps = []
depend()
target && !this.deps.includes(target) && this.deps.push(target)
notify()
this.deps.forEach(dep=>dep() )
Object.keys(data).forEach(key=>
let v = data[key]
const dep = new Dep()
Object.defineProperty(data, key,
get()
dep.depend()
return v;
,
set(nv)
v = nv
dep.notify()
)
)
function watcher(fn)
target = fn
target()
target = null
watcher(()=>
c = data.a + data.b
)
watcher(()=>
d = data.a - data.b
)
log('c=',c)
log('d=',d)
data.a = 99
log('c=',c)
log('d=',d)
/**
c= 3
d= -1
c= 101
d= 97
*/
- 简述一下这个过程:
对data对象里面的每一个key利用defineProperty进行数据拦截,在get里面进行Dep依赖收集,在set里面通知数据更新。
依赖收集实则是将watcher实例加入deps队列,当接到通知更新的时候,对队列里面的函数遍历执行,达到数据自动更新的效果。
3. 源码阅读
在阅读源码的时候,为了我们方便寻找入口,我们先来看看官网对数据响应式的阐述。
看完官方给的图,我们可以明确知道,
Watcher
的粒度是组件,也就是说,每一个组件对应一个Watcher
。
那么Watcher
究竟是什么呢?Dep
又是什么? Observer
又是做什么用的?下面让我们到源码中去寻找答案吧。
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src='../dist/vue.js'></script>
</head>
<body>
<div id="app">
a
</div>
<script>
const app = new Vue(
el:'#app',
data:
a: 1
,
mounted()
setInterval(()=>
this.a ++
, 3000)
)
</script>
</body>
</html>
跟踪调用栈
自己画的一个流程图
talk is cheap, show me the code
特别声明,为简化流程,所有的源码展示,均经过删减
src\\core\\instance\\index.js
一个入口文件
function Vue (options)
/**
* 初始化
*/
this._init(options)
/**
* 以下通过给Vue.prototype挂载的方法,混入其他方法
*/
initMixin(Vue)
/**
* initMixin
通过该方法,给Vue提供__init方法, 初始化生命周期
initLifecycle -> initEvents -> initRender
-> callHook(vm, 'beforeCreate') -> initInJections
-> initState -> initProvide
-> callHook(vm, 'created')
-> if (vm.$options.el)
vm.$mount(vm.$options.el)
*/
stateMixin(Vue)
/**
stateMixin
$data -> $props -> $set -> $delete -> $watch
*/
eventsMixin(Vue)
/** eventsMixin
$on $once $off $emit
*/
lifecycleMixin(Vue)
/** lifecycleMixin
* _update(), $forceUpdate, $destroy
*
*/
renderMixin(Vue)
/**
* $nextTick, _render, $vnode
*
* */
export default Vue
src\\core\\instance\\init.js
/**
* initMixin
通过该方法,给Vue提供__init方法, 初始化生命周期
initLifecycle -> initEvents -> initRender
-> callHook(vm, ‘beforeCreate’) -> initInJections
-> initState -> initProvide
-> callHook(vm, ‘created’)
-> if (vm.$options.el)
vm. m o u n t ( v m . mount(vm. mount(vm.options.el)
*/
export function initMixin (Vue: Class<Component>)
/**
* @重要 数据初始化,响应式
* $set $delete $watch
* 在reject之后,初始化数据,达到去重的效果
*/
initState(vm)
/**
* 调用created钩子函数
*/
callHook(vm, 'created')
if (vm.$options.el)
vm.$mount(vm.$options.el)
src\\core\\instance\\state.js\\initState
对data进行预处理
export function initState (vm: Component)
vm._watchers = []
const opts = vm.$options
/**
* state的初始化顺序
* props -> methods -> data -> computed -> watch
*/
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) initData(vm)
if (opts.computed) initComputed(vm, opts.computed)
src\\core\\instance\\state.js\\initData
对data进行observe,对数据的getter,setter拦截
function initData (vm: Component)
let data = vm.$options.data
// proxy data on instance
/**
* 数据代理
*/
const keys = Object.keys(data)
let i = keys.length
while (i--)
const key = keys[i]
/**
* @代理
*/
proxy(vm, `_data`, key)
/**
* @响应式操作
*/
observe(data, true /* asRootData */)
``src\\core\\instance\\state.js\\proxy`
const sharedPropertyDefinition =
enumerable: true,
configurable: true,
get: noop,
set: noop
/**
*
proxy(vm, `_data`, key) 168行
*/
export function proxy (target: Object, sourceKey: string, key: string)
sharedPropertyDefinition.get = function proxyGetter ()
return this[sourceKey][key]
sharedPropertyDefinition.set = function proxySetter (val)
this[sourceKey][key] = val
Object.defineProperty(target, key, sharedPropertyDefinition)
src\\core\\observer\\index.js\\observe
创建观察者实例
export function observe (value: any, asRootData: ? boolean): Observer | void
if (!isObject(value) || value instanceof VNode)
return
/**
* @观察者
*/
let ob: Observer | void
ob = new Observer(value)
if (asRootData && ob)
ob.vmCount++
return ob
src\\core\\observer\\index.js\\Observer类
export class Observer
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any)
this.value = value
/**
* @思考为什么在Observer里面声明dep
* 创建Dep实例
* Object 里面新增或者删除属性
* array 中有变更方法
*/
this.dep = new Dep()
this.vmCount = 0
/**
* 设置一个—个 __ob__ 属性,引用当前Observer实例
*/
/**
*
export function def (obj: Object, key: string, val: any, enumerable?: boolean)
Object.defineProperty(obj, key,
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
)
*/
def(value, '__ob__', this)
/**
* 类型判断
*/
// 数组
if (Array.isArray(value))
if (hasProto)
protoAugment(value, arrayMethods)
else
copyAugment(value, arrayMethods, arrayKeys)
/**
* 如果数组里面的元素还是对象,还需要进行响应式处理
*/
this.observeArray(value)
以上是关于从模仿开始理解vue2的数据响应式的主要内容,如果未能解决你的问题,请参考以下文章