深入浅出Vue.js--变化侦测

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入浅出Vue.js--变化侦测相关的知识,希望对你有一定的参考价值。

参考技术A

侦测状态变化,重新渲染页面。

拉(通知状态改变,然后暴力比对哪些节点需要重新渲染): Angular脏检查、React虚拟dom

推(明确知道哪些状态改变,细粒度,通知绑定这个状态的依赖节点更新): Vue

但,粒度越细,每个状态绑定的依赖越多,追踪开销就越大。从Vue2.0开始引入虚拟dom,绑定依赖到组件层面,而不是节点层面。 状态改变,通知到组件,组件内部再使用虚拟dom进行比对。

追踪变化 Object.defineProperty 和 Proxy

收集依赖
当数据发生变化的时候,需要通知使用了该数据的地方。所以在gettter中收集依赖,在setter中触发依赖。

为了减少耦合,封装Dep类,专门管理依赖

收集的依赖window.target,到底是啥?依赖是用到数据的地方,可能是模板,可能是用户写的一个watch,需要抽象出一个类集中处理多种情况,收集依赖阶段只收集这个类的实例,通知也只通知它,它再负责通知其他地方 -- Watcher。

递归侦测所有key

封装一个Observer类用于将data中的所有属性(包括子属性)都转化成getter/setter的形式。

getter/setter只能追踪一个属性是否被修改,但无法追踪新增和删除属性,所以另外提供了vm. delete两个api。ES6之前。

侦测Object变化是通过getter/setter实现的,但是如果用Array原型上的方法改变数组,就无法侦测了。同setter追踪,如果可以在用户使用Array原型上的方法改变数组时,得到通知,就可以侦测变化。

我们可以用一个拦截器arrayMethods去覆盖Array.prototype,在拦截器中发送变化通知, 再执行原本的功能。改变数组自身内容的7个方法: [\'push\', \'pop\', \'shift\', \'unshift\', \'splice\', \'sort\', \'reverse\']

拦截器arrayMethods不能直接覆盖Array.prototype,会污染全局的Array。我们的拦截操作只需要针对那些被侦测了变化的数据生效,也就是说拦截器只覆盖那些响应式数组的原型。将一个数据转化成响应式,需要用到Observer。

ES6用Object.getPropertyOf和Object.setPropertyOf替代了 proto
每次访问数组的值,就会触发getter。所以Array在getter里收集依赖,在拦截器中触发依赖。
依赖列表dep存储在Observer中,因为getter和拦截器中都可以访问到Observer实例。
getter中访问:

拦截器中访问:

这样,就可以通过数组值的 ob 属性访问到Observer实例上的dep,调用改变数组内容的方法时,通知依赖。同时,收集依赖中的observe函数中通过 ob 来判断,数据是否已经被Observer转换成了响应式。

侦测数组中元素变化

侦测新增元素变化
可以新增数组元素的方法为:push、unshift 和splice,可以取出新增元素,使用observeArray方法使其变成响应式的。

Array的变化侦测是通过拦截原型上方法实现的,所以对直接给数组某一项赋值,或者通过设置length改变数组,是侦测不到的。所以可以用api或方法代替。

expOrFn: a.b.c or 函数
options: deep, immediate
用于观察一个表达式或computed函数在Vue实例上的变化。回调函数调用时,会从参数得到newValue和oldValue。返回一个取消观察函数,用来停止触发回调。

deep: watch对象内部值的变化,都会触发回调
immediate: 立即以表达式的当前值触发回调

所有vm.$开头的属性,都是写在Vue.prototype上的。

原理

teardown 首先需要先在Watcher中记录自己被收录进了哪些Dep中,当unwatch时,遍历自己的记录列表,从dep依赖列表中把自己删除。

deep实现原理:除了要触发当前这个被监听数据的收集依赖之外,需要把其所有子值都触发一遍收集依赖。当子数据发生变化时,可以通知当前Watcher。

在taget上设置一个属性,如果target是响应式的,被创建的属性也是响应式的,并触发视图更新。主要用来避免vue侦测不到新增加属性的限制。

用于删除target对象上的key属性。如果对象是响应式的,需要确保删除能触发更新试图。主要为了避免直接使用delete无法被侦测到变化的限制。

《深入浅出Vue.js》读书笔记1-Object的变化侦测

Object变化侦测

1.什么是变化侦测

vue.js会自动通过状态生成DOM,并将其输出在页面上显示出来,这个过程叫做​渲染​。

在运行时,应用内部的状态会不断发生变化,此时需要不停的重新渲染,如何确定状态中发生了什么变化? ​变化侦测​就是来解决这个问题的。

vue的变化侦测​:状态发生变化时,vue.js立刻就知道哪些状态发生了变化,就可以进行更细粒度的更新。(ps:Angular和React的变化侦测都是在状态发生变化时,不知道哪个状态变了,只知道状态可能变了,然会会发送一个信号告诉框架,框架内部收到信号后,会进行一个暴力比对来找出哪些DOM节点需要更新。)但是粒度越细,每个状态绑定的依赖就越多,依赖追踪在内存上开销越大。因此vue.js2.0引入了虚拟DOM,状态绑定的依赖不再是具体的DOM节点,而是一个组件,这样状态变化后,通知到组件组件内部再去使用虚拟dom去比对。

2.如何追踪变化

vue.js2.0使用的是Object.defineProperty实现,vue3.0使用的是proxy。

function defineReactive(data,key,val)
Object.defineProperty(data,key,
enumerable:true,
configurable:true,
get:function()
return val;
,
ser:function(newVal)
if(val === newVal)
return;

val = newVal;

)

这个函数defineReactive用来对Object.defineProperty封装,并定义了一个响应式的数据。封装好后,每当从data的key读取数据,get函数被触发,每当往data的key中设置数据时,set函数被触发。

3.如何收集依赖

我们观察数据,目的是当数据属性发生变化时,可以通知使用的地方。

<template>
<p>hancao</p>
</template>

对于上面的问题,要先收集依赖,把用到gepingli的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循坏触发一遍就好。

故,总结:在getter中收集依赖,在setter中触发依赖。

4.依赖收集在哪

首先,可以想到,每一个key都对应一个数组,用来存储当前key的依赖,假设依赖是一个函数,我们将其保存在window.target上(window.xxx都无所谓,只要是确定的位置就好),于是对defineReactive进行改造。

//vue2.0
function defineReactive(data,key,val)
let dep = [];
Object.defineProperty(data,key,
enumerable:true,
configurable:true,
get:function()
dep.push(window.target);//将依赖保存在dep数组中
return val;
,
ser:function(newVal)
if(val === newVal)
return;

// 触发dep数组中的依赖
for(let i =0;i<dep.length;i++)
dep[i](newVal,val);// watch

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++)
subs[i].update()



function remove(arr,item)
if(arr.length)
const index = arr.indexOf(item);
if(index > -1)
return 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;
,
ser:function(newVal)
if(val === newVal)
return;

val = newVal;
dep.notify();

)

5.依赖是谁

上面收集的window.target是什么,我们要收集的是谁?

收集谁,就是当属性发生变化时,通知谁。

我们通知用到数据的地方,有可能是模板,也有可能是用户写的watch。所以需要抽象出一个能集中处理这些的类。

于是,给它起了一个好听的名字,就是hancao,不对,是Watcher。

6.什么是watcher

watcher是一个中介,数据发生变化时通知它,然后它再通知其他地方。

就像房东房子要出租,告诉中介,中介再告诉你这有个房子要出租,房租正好是你一个月工资。

Watch使用方式:

vm.$watch(g.p.l,function (newVal,oldVal) 
// 搞点事情
)

上面代码表示,当data.g.p.l发生变化的时候,触发第二个参数里面的方法。

于是我们只需要把watch儿实例添加到data.g.p.l属性的Dep里面就好了,然后当这个值发生变化,通知Watcher,接着Watcher在执行参数中的这个回调函数。

于是乎,下面这段代码便产生了:

export default class Watcher 
constructor (vm,exp,cb)
this.vm = vm
this.getter = parsePath(exp) // 比如 exp是 g.p.l。parsePath就是通过这个路径去获取data.g.p.l的内容
this.cb = cb
this.value = this.get()

get ()
window.target = this
let value = this.getter.call(this.vm,this.vm)
window.target = undefined
return value

update ()
const oldVal = this.value
this.value = this.get()
this.cb.call(this.vm,this.value,oldVal)

上面的代码可能第一次看到会有些懵,

那从头捋一次。
《深入浅出Vue.js》读书笔记1-Object的变化侦测_封装《深入浅出Vue.js》读书笔记1-Object的变化侦测_数据_02
以上。

parsePath根据路径读取data中的数据,简单的循环不在此多描述了。

7.递归侦测所有key

前面完成了Object的变化侦测功能(data的key),但是我们希望侦测对象中的所有属性(包括子属性)。

export class Observer 
constructor (value)
this.value = value
if(!Array.isArray(value))
this.walk(value)


walk(obj)
const keys = Object.keys(obj)
for(let i =0 ;i<keys.length;i++)
defineReactive(ob

以上是关于深入浅出Vue.js--变化侦测的主要内容,如果未能解决你的问题,请参考以下文章

《深入浅出Vue.js》读书笔记2-Proxy的自我尝试

活动五一劳动节,五本深入浅出Vue.js等你来

vue.js源码学习-双向绑定之Array

5分钟掌握Vuex,深入浅出

5分钟掌握Vuex,深入浅出

Vue.js之深入浅出