阅读《深入浅出vue.js》 - 1-4章笔记
阅读《深入浅出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是回调函数callbackconstructor (vm, expOrFn, cb) {this.vm = vm;this.getter = parsePath(expOrFn)this.cb = cb;this.value = this.get()}get () {window.target = this;//执行getter获取数据,添加watcher进Deplet 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();
}
主要也是要对数组的处理。具体去看书吧。
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!