什么是变化侦测
运行时,内部状态可能会发生变化,相应页面要重新渲染,变化侦测就是弄清楚是哪里发生了变化。
从实现的方案上分为两种:react(虚拟DOM)和angular(脏检查)使用的是“拉”的方案,vue使用的是“推”的方案。
拉的方案实际上不知道是哪里发生的变化,要通过暴力对比的方法来查找。推的方案就不同了,它实际是知道哪里发生了变化,可以进行更“细”的更新,缺点就是随着粒度的变细,每个状态所绑定的依赖就越多,依赖追踪的内存开销就越大。
Vue2.0开始引入了虚拟DOM,变成了一个折中的方案,状态绑定的不再是DOM节点而是组件。当状态发生变化通知到组件一级,组件内再进行虚拟DOM对比。
如何追踪变化
function defineReactive(data, key, val){
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
return val;
},
set: function(newVal){
if(val === newVal){
return;
}
val = newVal;
}
})
}
用Object.defineProperty就可以解决这个问题,每当data中的状态被读取,get函数就会触发,每当状态被修改就触发set函数。
由于浏览器对ES6的支持不理想,暂时选择了这样的方案。下一个版本会使用Proxy来重写这里。
如何收集依赖
<template>
<h1>{{name}}</h1>
</template>
对于这样一个对data.name的引用当如何处理呢?
Vue2.0中,模板使用数据等同组件使用数据,所以当数据发生变化只会通知到组件一级。
看看上面Object.defineProperty的部分,在getter收集依赖,在setter触发依赖就好了。
依赖收集在哪里
我们给每一个key分配一个数组存放依赖就好了,假设依赖是一个函数,就存在window.target上,要怎么丰满我们的defineReactive呢?
function defineReactive(data, key, val){
let dep = [];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
dep.push(window.target) // 新增
return val;
},
set: function(newVal){
if(val === newVal){
return;
}
// 新增
for(let i = 0;i < dep.length;i++){
dep[8i](newVal, val);
}
val = newVal;
}
})
}
但是这样写有点耦合,我们可以把收集依赖的部分封装成一个Dep类,为我们管理依赖。
export.default = class Dep{
constructor(){
this.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; i<subs.length ;i++){
sub[i].update();
}
}
}
function remove(arr, item){
if(arr.length){
const index = arr.indexOf(item);
if(index > -1){
arr.splice(index, 1);
}
}
}
改造之前写的defineReactive
function defineReactive(data, key, val){
let dep = new Dep(); // 修改
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
dep.depend(); // 修改
return val;
},
set: function(newVal){
if(val === newVal){
return;
}
dep.notify(); // 修改
val = newVal;
}
})
}
依赖是谁
依赖就是window.target,window.target是啥?核心问题就是当状态发生变化的时候通知谁?
要通知用到这个状态的地方,可能是模板也可能是一个用户写的watch。所以可以抽象出一个类,在状态改变的时候通知到收集到的类实例,由它们再通知到确实的地方。给这个类起一个名字,Watcher。
什么是Watcher
先看一个经典的使用场景。
// keypath
vm.$watch('a.b.c', function(newVal, oldVal){
// 做点什么
})
像上面这样写了之后,我们希望data.a.b.c发生变化触发回调函数,如何实现这个功能呢?
将这个依赖实例Watcher添加到data.a.b.c的Dep中,当状态变化时,通知这个实例,它再触发回调函数。
开始实现Watcher吧。
export default class Watcher{
constructor(vm, expOrFn, cb){
this.vm = vm;
// 执行this.getter(),就可以获取到data.a.b.c的内容
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = value;
}
get(){
window.target = this;
let value = this.getter.call(this.vm, this.vm);
window.target = undefined;
return value;
}
update(){
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
实例化一个这样的类,它就把自身主动添加到data.a.b.c
的Dep中,神奇吧!关注一下get方法的过程,先将实例赋值给window.target,然后用getter方法读取一次data.a.b.c
,就把实例主动添加到data.a.b.c
的Dep中了,最后window.target赋undefined。
parsePath是怎样生成读取指定状态的函数的呢?
const bailRE = /[^\w.$]/
export function parsePath(path){
if(bailRE.test(path)){
return;
}
const segments = path.split('.');
return function(obj){
if(!obj)return;
for(let i = 0;i < segments.length;i++){
obj = obj[segments[i]];
}
return obj;
}
}
就是以.
分割返回的函数会从实例里一层层取值。
递归侦测所有的key
前面实现了一个属性的变化侦测,我们希望用一个类,将所有属性(包括子属性)都转换成getter/setter形式,然后侦测变化,叫它Observer好奇查了一下英文,惊喜,观察者。。。。。(模式两个字没脱口)
export class Observer{
constructor(value){
this.value = value;
if(!Array.isArray(value)){
this.walk(value);
}
}
walk(obj){
const keys = obj.keys(obj);
for(let i = 0;i < keys.length;i++){
defineReactive(obj, keys[i], obj.keys[i])
}
}
}
function defineReactive(data, key, val){
// 新增,递归子属性
if(typeof val = 'object'){
new Observer(val);
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
dep.depend();
return val;
},
set: function(newVal){
if(val === newVal){
return;
}
dep.notify();
val = newVal;
}
})
}
在原有的defineReactive加入了递归生成Observer实例的过程。这样给Observer传入一个obj,obj就变成响应式的了。
关于Object的问题
对于对象(不讲数组)添加属性和用delete
字段删除属性,将成为漏网之鱼,并不会添加和移除这些属性的侦测。也就是说多数情况下,响应式的内容开始就定下来了,除非调用特定方法(vm.delete)。
这种局面也是没有办法的事,引入proxy可能会改观吧。
总结
本文是参照《深入浅出Vue.js》,老实说本白能力有限,拜读第二遍才清楚作者要表达的意思,第一遍模模糊糊。
关于看源码方面,引用左少的话‘收起你野马般的思绪’,忍不住会像如果参数是那样的就不对了,不公开的方法设计的人当然按设计意图小心使用,而且会报错的工具就不是好工具了吗?