数据驱动视图
- 把数据理解为状态,视图就是用户可直观看到的页面
- UI = render(state)
- 变化侦测就是追踪状态即数据的变化,变化侦测是响应式系统的核心
变化侦测(observer)
起初JavaScript没有元编程能力proxy,所以vue2采用object.defineProperty方法,现在vue3借助Proxy实现变化侦测,简洁了许多。
本文主要围绕vue2 object.defineProperty方法展开
侦测数据的变化,分为两种类型:推(vue), 拉(React、Angular)
- Angular脏检查
- React虚拟DOM
- vue推,粒度细,开销大
- vue2引入虚拟DOM且粒度调整为中等粒度(组件),即一个组件共用一个watcher
├─dist # 项目构建后的文件,会找到不同的vue.js构建版本
├─scripts # 与项目构建相关的脚本和配置文件
├─flow # flow的类型声明文件
├─packages # vue-server-render和vue-template-compiler,作为单独的NPM包发布
└─test # 项目测试代码
├─src # 项目源代码
│ ├─complier # 与模板编译相关的代码
│ ├─core # 通用的、与运行平台无关的运行时代码
│ │ ├─observe # 实现变化侦测的代码
│ │ ├─vdom # 实现virtual dom的代码
│ │ ├─instance # Vue.js实例的构造函数和原型方法
│ │ ├─global-api # 全局api的代码
│ │ └─components # 内置组件的代码
│ ├─server # 与服务端渲染相关的代码
│ ├─platforms # 特定运行平台的代码,如weex、web
│ ├─sfc # 单文件组件的解析代码
│ └─shared # 项目公用的工具代码
├─types # TypeScript类型定义
│ ├─test # 类型定义测试
实例初始化中,调用 initState 处理部分选项数据,initData 用于处理选项 data
// initData 函数的最后一句代码:
// observe data
observe(data, true /* asRootData */)
observer类位于源码的src/core/observer/index.js
// 源码位置:src/core/observer/index.js
/**
* Observer类
*/
export class Observer {
constructor (value) {
this.value = value;
// 相当于为value打上标记,表示它已经被转化成响应式了,避免重复操作
def(value,'__ob__',this); // 给value新增一个__ob__属性,值为该value的Observer实例
if (Array.isArray(value)) {
// 当value为数组时的逻辑
} else {//对象
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])//通过递归的方式把一个对象的所有属性都转化成可观测对象
}
}
}
- 如果数据有__ob__属性,表示它已经被转化成响应式的 。
- 从上源码可见,vue中变化侦测分别有针对数组和对象的变化侦测。
对象的变化侦测
在JavaScript中,侦测一个对象的变化有两种方法:Object.defineProperty、Proxy代理
- Data初始化时,通过observer调用Object.defineProperty,定义了getter/setter的形式来追踪变化
- 当外界通过Watcher读取数据时,会触发getter,两者通过(全局window.target)将Watcher添加到数据的依赖管理dep中。(计算属性等需要响应式依赖的地方生成watcher)
- 当数据发生了变化时,会触发setter,setter中调用 dep.notify(),向Dep中的每一个依赖(即Watcher)发送通知。
- Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等
- 不足:仅仅只能观测到object数据的取值及设置值,无法观测到向object添加、删除一对key/value。
- 为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete
object.defineProperty
- Object.defineProperty() 方法会直接在一个对象上定义一个新属性或者修改现有属性,并返回此对象。
- 应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。而vue 3 代理实现数据响应式就不允区分
- vue 2 只有当实例被创建时就已经存在于 data 中的 property 才是响应式的,所有响应式的property需要在一开始列出。
function defineReactive (obj,key,val) {
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依赖
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}
依赖管理器Dep类
依赖保存在Observer实例上,因为observer在getter、setter、拦截器中都可以访问到。
// 源码位置:src/core/observer/dep.js
export default class Dep {
constructor () {
this.subs = []; // 初始化了一个subs数组,用来存放依赖
}
addSub (sub) {
this.subs.push(sub)
}
// 删除一个依赖
removeSub (sub) {
remove(this.subs, sub)
}
// 添加一个依赖
depend () {
if (window.target) {
this.addSub(window.target)
}
}
// 通知所有依赖更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
/**
* Remove an item from an array
*/
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
Watcher类
计算属性等需要响应式依赖的地方生成watcher
- 计算属性的初始化是在 src/core/instance/state.js 文件中的 initState 函数中调用initComputed 函数完成的
- 谁使用了数据,谁就是依赖,为依赖创建一个Watcher实例。
- Watcher把自己设置到全局(window.target),然后读取数据,触发这个数据的getter。
- 在getter中就会从全局(window.target)获得Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。
this.cb.call(this.vm, this.value, oldValue);实现改变视图,替换虚拟DOM中的数据
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get();//调用实例方法,重点
}
get () {//初始化即被调用
window.target = this;//把实例自身赋给了全局的一个唯一对象
const vm = this.vm
let value = this.getter.call(vm, vm);//获取一下被依赖的数据,同时触发该数据上面的getter(加入依赖)
// 在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中
//完毕最后将window.target释放掉。
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)// 实现改变视图
}
}
/**
* Parse simple path.
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
- 对后文提到的watch方法中deep=true 深度侦测的支持:在释放window.target之前,遍历子值,触发收集子值依赖
数组的变化侦测
- Object.defineProperty是对象原型上的,Array无法使用这个方法,所以我们需要对Array型数据设计一套另外的变化侦测机制。
- 数组通过get收集依赖(同上),在拦截器中触发依赖
- 对于数组变化侦测是通过拦截器实现的,也就是说只要是通过数组原型上的方法对数组进行操作就都可以侦测到,但是部分语法会被忽略,例如通过数组的下标来操作数据、采用length来清零。(对应可以使用set、和splice方法来规避)
- Vue增加了两个全局API:Vue.set、Vue.delete
思考下标给数组赋值的实质应该是新增键值对,对应上面对象中新增属性。
创建拦截器
为什么不直接覆盖Array.prototype,因为这样会污染全局的Array,而我们只希望对响应式的数组设置拦截器。
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(Array.prototype)
// 改变数组自身内容的7个方法,即变异方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* 创建拦截器
*/
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]// 缓存 original method
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)//调用原生方法
const ob = this.__ob__//数据已经是响应式
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)//数组新增数据,将数据设置为响应式
// notify change
ob.dep.notify()//通知
return result
})
})
注意:这里通过inserted拿到新增的元素并对他进行侦测
不在原数组上直接操作的数组方法 : filter、concat、slice
挂载拦截器
'__proto__' in {} 判断了浏览器是否支持__proto__
如果支持,把value.__proto__ 赋值为 拦截器对象arrayMethods
如果不支持,把拦截器中重写的7个方法循环加入到value上
// 源码位置:/src/core/observer/index.js
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
this.dep = new Dep()//依赖挂载在observer实例上
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {//对象
this.walk(value)
}
}
/**
* 挂载拦截器在__proto__ 上,不存在则新建方法属性
*/
function protoAugment (target, src: Object, keys: any) {
target.__proto__ = src
}
/**
* Augment an target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])//给target新增一个key属性,属性值是src[key]
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
深度侦测
- 不但要侦测数据自身的变化,还要侦测数据中所有子数据的变化
- 默认整个对象被替换时候触发
- 设置deep:true后可以深度监听,即使属性变化也会触发
- 是通过上面observeArray方法中的observe来实现的
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {//不支持非对象、vNode
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
举个响应式实例
- 当一个 Vue 实例被创建时,除了创建HTML,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
- Object.freeze(),这会阻止修改现有的 property,也意味着响应系统无法再追踪变化。
- 只有当实例被创建时就已经存在于 data 中的 property 才是响应式的。如果后来添加一个新的 property,不是响应式
在页面显示seconds,计时
var app = new Vue({
name: 'my-app',
el: '#root',
// Some data
data:{
seconds:0
},
created(){
setInterval(()=>{
this.seconds++;
},1000);
}
})
与数据相关的实例方法
$watch
- expOrFn string键路径 | 有返回值的Function
- options是一个对象包含boolean属性:deep、immediate
- deep: true 发现对象内部值的变化,注意监听数组的变动不需要这么做
- immediate: true 立即以表达式的当前值触发回调
vm.$watch( expOrFn, callback, [options] );
// 键路径
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
// 函数
vm.$watch(
function () {
// 表达式 `this.a + this.b` 每次得出一个不同的结果时
// 处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
return this.a + this.b
},
function (newVal, oldVal) {
// 做点什么
}
)
返回值:取消观察函数
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()
方法源码
Vue.prototype.$watch = function (expOrFn,cb,options) {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
$set和delete
- 对于object型数据,添加一对新的key/value或删除一对已有的key/value时,Vue是无法观测到的
- 对于Array型数据,当我们通过数组下标修改数组中的数据时,Vue也是无法观测到的。
- 所以提出了全局方法vm.$set,是全局 Vue.set 的别名,其用法相同。
vm.$set( target, propertyName/index, value );//返回:设置的值。
vm.$delete( target, propertyName/index );//没有返回值
watch和computed
Vue为我们提供watch和computed接口,以Vue的依赖追踪机制为基础的,在依赖数据发生改变的时候,被依赖的数据根据预先定义好的函数,发生“自动”的变化
- watch擅长处理 :一个数据影响多个数据
- computed擅长处理 :一个数据受多个数据影响
watch是监听+事件,first变化触发事件执行
watch: {
first: function (val) { this.fullName = val + this.lastName }
}
b:{//深度监听,可监听到对象、数组的变化
handler(val, oldVal){
console.log("b.c: "+val.c, oldVal.c);
},
deep:true //true 深度监听
immediate: true,//不等变化,申明后立即执行
}
computed是计算属性
- 计算属性会被缓存,多次调用一个计算属性代码只执行一次,后面会使用缓存值(计算依赖变化,重新计算)
- 也可以将计算属性由函数改为带有get、set属性的对象,这样就可以设置它的值
computed:{
full: function () { return this.firstName + lastName }
}
//this.fullName取用,类似data
computed: {
fullName: {
// getter
get: function () {
return this.firstName + ' ' + this.lastName
},
// setter
set: function (newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
computed原理
- 实质是定义在实例上的一个特殊的getter方法,方法中结合watcher实现缓存和收集依赖功能。
- 缓存是通过watcher的dirty属性实现,当计算属性中的内容变化,计算属性的watcher收到通知,将自己的dirty属性设置为true
- 有人提出提案,应该先对比计算属性的值再重新渲染。
watch监听一个对象
<html>
<head>
<title>VueJS</title>
</head>
<body>
<!-- Include the library in the page -->
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<!-- Some HTML -->
<div id="root">
<label> <input type="text" v-model="fo.name" ></label>
</div>
<!-- Some JavaScript -->
<script>
// New VueJS instance
var app = new Vue({
el: '#root',
// Some data
data:{
fo:{
name:""
}
},
watch:{
'fo.name'(cur,old){//有.的时候用引号包围
console.log('old:', old)
console.log('cur:', cur)
}
}
})
</script>
</body>
</html>
试着实现一个简单的数据响应,类似Mobx
Mobx数据管理方案,采用观察者模式,观察可变数据,持有细粒度的数据响应模式,感知到哪些组件需要被更新的时间复杂度是O(1)的
0 ObserveTarget和Model的定义
class ObserveTarget {
value: any;
// 可增加其他描述属性,制订不同的响应机制,例如computed
}
Model实现了响应机制的方法基类
- key为数据, value为监听该数据的方法,用map建立链接
class Model {
depMap = new Map<ObserveTarget, Set<Function>>();
private trackedProperties = new Set<ObserveTarget>(); // 响应式data集
initTrack() {
this.trackedProperties = new Set();
}
// 收集:响应式data集
onTrack(data: ObserveTarget) {
this.trackedProperties.add(data);
}
// 遍历响应式data集,注册 监听该数据的方法
// 目前的逻辑是把一个方法给所有数据
recordDep(func: Function) {
this.trackedProperties.forEach(data => {
const actions = this.dependencyMap.get(data) || new Set();
actions.add(func);
this.depMap.set(data, actions);
});
}
// 当一个数据发生变化时触发相关方法调用
onChange(data: ObserveTarget) {
const actions = this.dependencyMap.get(data) || new Set();
actions.forEach(f => f()); // 依次调用监听的方法
}
}
1 实例化一个Model :单例模式
const managerInstance = new Model();
注册响应机制observe方法:getter+setter收集+通知变化
get中收集响应式data集,set中通知onchange
function observe() {
return function(target: any, key: string) {
const secureKey = 'prefix' + key;
Object.defineProperty(target, key, {
get(this: any) {
let property = target[secureKey] as ObserveTarget;
if (!property) {
target[secureKey] = new ObserveTarget();
property = target[secureKey];
}
managerInstance.onTrack(property);//
return property.value;
},
set(this: any, value: any) {
// 修改属性的值
let property = target[secureKey] as ObserveTarget;
if (!property) {
target[secureKey] = new ObserveTarget();
property = target[secureKey];
}
property.value = value;
// 通知属性改动
managerInstance.onChange(property);
},
});
}
}
2 初始化数据,使用装饰器语法,对数据调用observe()
// 数据
class Data {
@observe()
data: number;
}
const dataInstance = new Data();
dataInstance.data = 0;
collectDependency:初始化一个响应式数据集+注册监听
// 依赖收集
function collectDep(func: Function) {
managerInstance.initTrack();
func();
managerInstance.recordDependency(func);
}
3 注册监听
function test() {
console.log('test', dataInstance.data)
}
collectDep(test)
4 简单测试
function dataAdd() {
dataInstance.data ++;
}
ReactDOM.render(
<div onClick={dataAdd}>Test Modify Data</div>,
document.getElementById("app")
)