阅读《深入浅出vue.js》 - 笔记
第一章:简介
vue的发展
- 视图层渲染
- 组件机制
- 路由机制
- 状态管理器
- 构建打包工具

vue.js是一个渐进式的javascript框架
第二章:Object的变化侦测
变化侦测?什么变化了?怎么侦测?得到的效果是什么?
先来理解一下什么叫渲染:渲染是从状态生成dom,再显示到用户界面的一整套流程叫做渲染;系统或者应用在运行的过程中因为状态不断发生变化,其视图也需要重新渲染,其中最重要的是变化侦测。
这里的状态主要是指数据,数据发生变化了,会通知试图做相应的更新,效果就是用户可以看到新界面。
变化侦测
变化侦测分为两种类型推push和拉pull
angular和react属于pull:这个是当状态发生变化了,但是不知道具体是哪个状态变了,只知道状态有可能变了,然后发送一个信号告诉框架,当框架内部收到信号之后,进行一个暴力比对来找出哪些dom需要重新渲染。
vue属于push:当状态发生改变时,vue马上就知道了,并且在一定程度上知道是哪些状态发生了变化,可以有效地进行更新。
对于单个属性的变化侦测
目前vue变化侦测的方式有两种方式,vue2的Object.defineProperty和vue3的Proxy,具体可参考我之前的链接:
vue3的数据劫持跟vue2的有什么不一样
总结:在Object.defineProperty中,有getter和setter函数,当vue中的组件或者节点使用了数据,那么肯定就会触发getter,当数据发生变化时要更改数据的值,那么肯定会触发setter函数。在getter中收集依赖,在setter中触发依赖。
1.1如何收集依赖?
答案就在上面这句话,依赖就是使用数据的地方,获取数据要执行getter函数,那么在执行getter函数的过程时,把依赖收集起来。
1.2依赖放到哪里去?
假如每个key每个数据都有一个数组dep,这个数组dep用来存储这个key的依赖,也就是收集这个数据所被使用的地方。假设依赖是一个函数,保存在window.target上。当新增一个依赖的时候,就在执行getter时往dep里push一个依赖window.target。当数据key发生变化时,在setter函数中遍历dep重新渲染。
如果单纯这样设置dep比较耦合不模块化,我们将dep收集依赖的过程封装成一个Dep类,用于vue管理依赖。这个类具有收集依赖、删除依赖、更新依赖等功能。也是在getter和setter函数中调用对应的功能来实现依赖收集和派发更新的过程。
1.3依赖收集好了通知谁去做更新?
当我们知道这个Dep类保存了依赖window.target,但是这些依赖可不仅仅是模版{{name}},也有可能是用户写的一个监听watch,或者是computed等情况,那么多情况我们需要集中处理,所以我们新定义了一个处理各种情况的类,给这个类起一个名字叫Watcher,类似一个中介的角色。当数据发生变化时,就让它作为一个消息派发的起点去通知需要其他需要更新的地方。那么在使用数据的时候dep在getter中就会push一个Watcher。比如这个例子vm.$watch('a.b.c', (value, oldValue) => {}):
export default class Watcher{
//expOrFn需要监听的值,cb是回调函数callback
constructor (vm, expOrFn, cb) {
this.vm = vm;
this.getter = parsePath(expOrFn)
this.cb = cb;
this.value = this.get()
}
get () {
window.target = this;
//执行getter获取数据,添加watcher进Dep
let value = this.getter.call(this.vm, this.vm);
window.target = undefined;
return value;
}
update () {
const oldValue = this.value;
this.value = this.get();
//执行回调函数cb,更新依赖
this.cb.call(this.vm, this.value, oldValue);
}
}
对于多个属性及其子属性的变化侦测
前面已实现单个属性的变化侦测,那data中的所有属性属性都被侦测到,那就需要将这些属性都转化为getter/setter的形式,所以我们来封装一个Observer类来实现这样的功能。
export class Observer {
constructor (value) {
this.value = value
// 只有是非数组的才可以执行walk函数
if(!Array.isArray(value)) {
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj);
for (let i = 0; i< keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
function defineReactive(data, key, val) {
//如果该属性是一个object,就继续递归执行
if (typeof val === 'object') {
new Observer(val)
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
// 在这里会将watcher依赖收集起来
dep.depend();
return val;
},
set: function(newVal) {
if(val === newVal) {
return;
}
val = newVal;
//将依赖进行更新
dep.notify();
}
})
}
}
这样的写可以涵盖大部分的变化监测,但是也总有别的漏网之鱼,因为只有通过getter/setter才能将一个属性变成响应式,一个对象变成响应式的对象;如果直接给一个对象添加或删除一个属性,这是vue检测不到的,但是提供了vm.$set/vm.$delete方法,我上面对比的vue2和vue3对比数据劫持的链接里也有说明。
总体思路图

第三章:Array的变化侦测
Array和Object监测的不同
由于我们可以通过Array.prototype即数组原型的方式来改变数组内容,所以并不会触发getter/setter,所以监听数组需要另辟蹊径。
如何检测数组变化
在es6之前,javascript并没有提供元编程的能力(不懂这个元编程是啥意思,找个时间去查查),也就是说没有可以拦截原型的方法,但是我们可以重写数组的原型方法,也就是使用拦截器。
首先我们要知道,数组原型的哪些方法可以改变数组本身,分别是push、pop、shift、unshift、splice、sort、reverse,在操作这些方法的时
实现拦截器:监测到数组变化
拦截器是和Array.prototype一样的object,有所不同的是这里面可以改变数组自身的方法是我们经过处理的。
要明白一点,拦截器是要覆盖原型上的这几个方法。
const arrayProto = Array.prototype;
//定义拦截器
export const arrayMethods = Object.create(arrayProto);
const copyMenthodsArr = ['push','pop','shift','unshift','splice','sort','reverse'];
copyMenthodsArr.forEach(function(method){
//缓存原始方法
const original = arrayProto[method];
//重新定义拦截器中的这些方法
Object.defineProperty(arrayMethods, method, {
value: function mutator(...args){
//使用apply改变this指向
return original.apply(this, args)
},
enumerbale: false,
writable: true,
configurable: true
})
})
在上述代码中,创建了arrayMethods,继承Array.prototype,遍历会使数组自身改变的方法,重新定义arrayMethods这个对象中的数组方法,也就是说在执行copyMenthodsArr中的方法时,比如push,执行的是arrayMethods.push也就是mutator函数。那我们就可以在mutator中干点什么事情了。
使用拦截器:覆盖到响应性数组原型上
我们有了这个arrayMethods之后,要怎么让他生效呢?要覆盖在Array.prototype上,但是又不能直接覆盖Array.prototype,因为这样会造成全局污染。我们只需要将这个拦截器用在响应式数组的原型上。
那就在Observer中调用,还记得上一章的检测对象变化吗?我们只需要改写一下,将arrayMethods覆盖数组的key的原型,如下所示:
export class Observer{
constructor(value){
this.value = value;
if (Array.isArray(value)) {
//覆盖value即当前监听数组上的__proto__
value.__proto__ = arrayMethods;
} else {
this.walk(value);
}
}
}
如下图所示:
小问题:如果浏览器不支持__proto__属性呢?
作者说vue处理的方法比较暴力,如果有那就直接覆盖protoAugment,没有的话那就直接设置这些方法给被侦测数组copyAugment。改写一下上述的Observer:
const hasProto = '__proto__' in {};
const arrayKeys = Object.hasOwnPropertyNames(arrayMethods);
export class Observer{
constructor(value){
this.value = value;
if (Array.isArray(value)) {
const augment = hasProto ? protoAument : copyAugment;
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value);
}
}
}
收集数组变化的依赖
可能你也发现了,如果只有一个拦截器,其实还是什么事都做不了。为什么会这样呢?因为我们之所以创建拦截器,本质上是为了得到一种能力,一种当数组的内容发生变化时得到通知的能力。
前一章我们讲到的object是在defineReactive函数中收集存储到Dep中的,那么数组呢?
数组要被访问,肯定也会经过getter函数啊,所以我们可以在get中收集数组的依赖。Array在getter中收集依赖,在拦截器中触发依赖。
小问题:依赖收集存放在哪里呢?
在Obverser的constructor中使用了拦截器,在getter方法中和对象一样收集依赖,由于拦截器和get方法中都需要访问依赖,所以依赖的保存位置至关重要,与对象不同的是,数组的dep(依赖)要保存在Observer实例上。
function defineReactive(data, key, val){
//创建一个Observer实例,若已存在则直接返回,避免重复侦测
let childOb = observe(val);
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
//对象收集依赖
dep.depend();
if(childOb) {
//数组收集依赖
childOb.dep.depend();
}
return val;
},
set: function(newVal){
if(val === newVal){
return;
}
dep.notify();
val = newVal;
}
})
}
export function observe(value, asRootData){
//如果不是一个对象,那就直接返回
if(!isObject(value)){
return;
}
let ob;
if(hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob;
}
在拦截器中获取Observer实例
将Observer实例定义在拦截器的属性__ob__上,如下所示:
function def(obj, key, val, enumerable){
Objetc.defineProerty(obj, key, {
value: val,
enumerbale: !!enumerable,
writable: true,
configurable: true
})
}
export class Observer {
constructor(value){
this.value = value;
this.dep = new Dep();
def(value, '__ob__', this) //新增
if(Array.isArray(value)){
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
} else {
this.walk(value);
}
}
}
为当前属性添加__ob__属性,即Observer实例,作用有两点:
- 为拦截器提供依赖访问,因为可以通过
__ob__拿到Observer实例,那就是也可以拿到Observer实例上的dep。 - 用来标记当前属性是否被转为响应式数据,因为每个被侦测变化的数据身上都有一个
__ob__属性来说明它是响应式的。
当value身上被标记了 ob 之后,就可以通过value.ob 来访问Observer实例。如果是Array拦截器,因为拦截器是原型方法,所以可以直接通过this.ob 来访问Observer实例。
通知依赖做更新
从前面我们可以知道,数组发生变化时,会在拦截器中通知依赖派发更新。
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function(method){
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args){
const result = original.apply(this, args);
const ob = this.__ob__;
ob.dep.notify();//向依赖发送消息
return result;
})
})
上述代码,我们调用了ob.dep.notify()去通知依赖Watcher数据发生了变化。
侦测数组中元素的变化
除了检测数组本身的变化,数组中也存在其他的元素需要被侦测的,比如数组中存在对象类型的属性,侦测也很简单,像处理对象一样递归就就行了,如下所示:
export class Observer {
constructor(value) {
this.value = value;
def(value, '__ob__', this);
//新增
if(Array.isArray(value)) {
this.observerArray(value)
} else {
this.walk(value)
}
}
observerArray(items){
for (let i = 0; i<items.length; i++) {
observer(items[i])
}
}
...
}
侦测新增元素的变化
首先我们需要获取新增的元素,然后利用前面Observer实例的observerArray去侦测就好了
一些问题
1、this.list[0] = 1;
2、this.list.length = 0;
这两个问题还是没有被拦截到的。
总结
Array追踪变化的方式和Object不一样。Array是通过创建拦截器去覆盖数组原型的方式来追踪变化的。
为了不污染全局的Array.prototype,在Observer中,只针对那些需要侦测变化的数据使用__proto__来覆盖原型方法,但又因为__proto__并不是所有的浏览器都支持,所以我们还是要判断一下,如果浏览器支持那就直接覆盖原型的方法,如果不支持那就是循环这个数组方法直接设置在当前被侦测的数组上。
Array和Object收集依赖的方式一样,都在getter中。但由于使用以来的位置不同,数组要在拦截器中向依赖发送通知,所以不能像Object一样存储在defineReactive中,而是保存在了Observer实例上。
在Observer中,每个被侦测变化的数据都具有一个__ob__属性,这个属性有两个作用,一是标记数据是否一倍侦测,二是方便通过数据取到__ob__,从而拿到Observer上的依赖dep,以便在拦截器中通知依赖数据发生了变化。
除了数组自身的变化需要被侦测,数组中元素发生变化也需要被侦测,这个就要使用observer(items[i])递归调用,类似对象的变化侦测;还有新增的元素也需要被侦测,获取新增的元素,然后也是调用observer实例中的observeArray方法。
第四章:变化侦测相关API
vm.$watch
vm.$watch是对Watcher的一个封装,但多了deep和immediate两个属性,可以监听函数及对象及对象子属性。
deep:会递归调用直到属性所有的子属性都被侦测,依赖都被收集到,任何一个发生变化,Watcher都会得到通知。不可用于数组。immediate:在发生变化之后,立即执行一次回调函数。unwatch:解除监听。
实现:
export default class Wtacher{
constructor(vm, expOrFn, cb, options){
this.vm = vm;
//deep属性处理
if(options) {
this.deep = !!options.deep;
} else {
this.deep = false;
}
this.deps = [];//新增
this.depIds = [];//新增
this.cb = cb;
if (typeof expOrFn === 'function') {
//直接赋值给getter,expOrFn函数中所读取的数据也会被Watcher监听
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
}
this.value = this.get();
}
get() {
window.target = this;
let value = this.getter.call(vm, vm)
if (this.deep) {
traverse(value)
}
window.target = undifined;
return value;
}
...
addDep(dep) {
const id = dep.id;
//在收集之前要判断是否已经存在了,不重复收集
if(!this.depIds.has(id)){
this.depIds.add(id);
this.deps.push(dep);
dep.addSub(this)
}
}
...
removeDep(sub) {
const index = this.sub.indexOf(sub);
if(index > -1) {
return this.subs.splice(index, 1);
}
}
...
}
const seenObjetcs = new Set();
//当属性deep为true时,会递归调用子属性的依赖收集
export function traverse(val){
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse(val, seen){
let i, keys;
const isA = Array.isArray(val)
//如果不是数组或对象,或者是被冻结的对象,什么都不处理
if((!isA && !isObject(val)) || Object.isFroZen(val)){
return
}
//如果已经是响应式的
if(val.__ob__){
const depId = val.__ob__.dep.id;
//如果是已经收集了的,也不处理
if(seen.has(depId)) {
return
}
//否则会加入
seen.add(depId);
}
//递归操作,监听子属性的变化
if(isA){
i = val.length;
while(i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length;
while(i--) _traverse(val[keys[i]], seen)
}
}
Watcher和Dep的关系
是多对多的关系,比如expOrFn是函数,那就要Watcher收集多个Dep
vm.$set
这个方法其实是在observer中抛出的set方法。讨论的主要是对数组的处理。
vm.$delete
代码:
export function del(target, key) {
const ob = target.__ob__;
delete target[key];
ob.dep.notify();
}
主要也是要对数组的处理。具体去看书吧。