深入浅出Vue变化侦测

我们都知道vue是个很优秀的框架,官网上也说明了是一个渐进式框架。那么什么是渐进式框架呢?
所谓渐进式,就是把框架分层。如图所示:

最核心的部分就是视图层渲染,然后往外就是组件机制,在这个基础上再加入路由机制,状态管理,构建工具。
所谓分层,就是既可以只用最核心的视图渲染功能快速开发一些需求,也可以使用全家桶开发大型应用。Vue足够灵活,根据自己的需求,选择不同的层级
视图层渲染作为最核心部分,其特性之一就是响应式系统,视图会随着状态的变化而变化。这也是我最喜欢Vue的地方,视图里任何一个地方,都可以用一种状态(变量)来表示。
从状态生成DOM,在输出到用户界面显示一整套过程叫做渲染。vue在运行时不断地重新渲染。而响应式系统赋予了框架重新渲染的能力,其重要组成部分就是变化侦测。学会了变化侦测,更有利于接下来对api的原理学习,接下来我们便开始从0到1实现一个变化侦测逻辑。
2.目录
3.1 什么是变化侦测
3.2 如何追踪变化
3.3 什么是依赖,如何收集依赖
3.4 依赖收集在哪里
3.5 依赖是谁,什么是watcher?
3.6递归侦测所有key
3.7 object的问题
3.1什么是变化侦测
上面我们说过,渲染就是Vue会自动通过状态生成DOM,并输出到页面上。Vue的渲染过程是声明式,我们可以通过模板来描述状态与DOM之间的映射关系。
在网页运行时,通过各种用户交互,Vue内部的数据状态会不断改变,此时页面也会不断渲染。但是,我们又怎么知道哪些状态发生了怎么样的改变?这就是变化侦测,只要是状态一改变,我们的vue就能知道,通过跟新的状态去渲染视图。
3.2如何追踪变化
关于追踪,在JavaScript中,我们如何知道一个对象改变了呢?

Object.defineProperty
ES6的proxy
在Vue3之前,我们还是使用Object.defineProperty

我们知道,Object.defineProperty用来侦测变化会有很多缺陷,并且在Vue3之后都用Proxy重写这部分代码了,那么我们还有必要学习这部分吗?其实我觉得很有必要,我们毕竟是学习原理和思想的,通过对原理的探索,我们更能领会牛人解决问题的思想,在以后的编程路上,还是很有必要的。
知道了如何追踪对象的变化,那么我们就可以写出以下代码:
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,key,val就行了。那么如何追踪变化?每当我们从data中的key读取数据时,get函数触发了,在设置data的key数据时,set就被触发了。
3.3什么是依赖,如何收集呢?
上面只是对Object.defineProperty进行封装了一下,但实际上并没什么作用,真正有用的是收集依赖。现在我们就有两个问题了:

什么是依赖?
如何收集依赖?

我们先回头思考一下,什么是响应式。就是数据改变了,视图自动更新。所以我们要去观察数据,当数据的属性发生变化时,我们就可以通知曾经使用了该数据的地方,这些地方,就被称作为依赖,举个例子:

{{name}}

//一个依赖

//另一个依赖
复制代码模板中,有两个地方是用了数据name,所以就有两个依赖。当数据改变时,我们就要向这两个依赖发送通知。 通过变化侦测中,在读取数据的时候,会触发getter,所以就通过在getter函数中去收集依赖,数据发生变化时,就需要在setter中触发依赖,所以我们可以把defineReactive函数改造一下: function defineReactive (data, key, val) { let dep=[] //新增,依赖收集器 Object.defineProperty(data, key, { enumerable: true, configurable: true, get: function () { dep.push(window.target) //新增,收集依赖,假设window.target就是一个依赖 return val }, set: function (newval) { if (val === newval) { return } val = newval dep.notify() //新增,通知依赖数据改变,关于dep后面会讲,这里只是抽象的表示需要做的事情 } }) } 复制代码3.4依赖收集在哪里 我们可以封装一个类Dep,专门帮助我们管理依赖。在这个类中,我们可以收集依赖,删除依赖,通知依赖等等,其代码如下: export default class dep { constructor () { this.subs = [] } addSub (sub) { this.subs.push(sub) } removeSub (sub) { remove(this.sub, sub) } depend () { if (window.target) { this.addSub(window.target) } } notify () { const subs = this.subs.slice() for (let i=0,l = subs.length;i<1;i++) { subs[i].update() } } } function remove (arr, item) { if (arr.length) { const index = arr.indexOf(item) if (index > -1) { return arr.splice(index, 1) } } } 复制代码 subs:用数组作为一个容器,收集依赖 addSub:添加依赖 removeSub:移除依赖 depend:假设window.target是一个依赖,判断是否有这个依赖,进而执行添加依赖操作 notify:通知每个依赖,数据变化了,要执行每个依赖的更新视图方法。

之后我们还需要改造一下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
}
val = newval
dep.notify() //通知每个依赖
}
})
}
复制代码3.5依赖是谁,什么是watcher?
在上面演示里,我们将winodw.target代表依赖,作用就是数据改变了,依赖就接受到了通知,然后再去通知其他地方。所以我们需要封装一个类,就叫watcher把。watcher实例就是一个一个的依赖。代码如下:
export default class watcher{
constructor(vm,exp,cb){
this.vm=vm
this.getter=parsePath(exp)
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 oldValue=this.value
this.value=this.get()
this.cb.call(this.vm,this.value,oldValue)
}
}
复制代码watcher接受三个参数:

vm:vue实例
exp:{{}}这里面的表达式,还有v-text和v-html中的表达式
cb:真正的更新DOM的函数(知道作用就行,后面模板解析会详细讲解)
其它参数:
getter:通过parsePath函数解析表达式,获取表达式的值
value:get方法返回值
get:通过getter获取表达式的值
update:更新视图

现在关于对象变化侦测基本原理都已近说完了,可能你现在还是感觉很懵,接下来我将整个过程从头来顺一下:
初始化过程:

响应式过程:

3.6递归侦测所有key
上面,整个变化侦测功能都已近实现,但是,只能侦测数据中某一个属性,我们希望能够把数据中所有属性都要侦测到,于是我们就要封装一个observer类.通过递归的形式,把data数据中所有属性都变成响应式。代码如下:
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++) {
definedReactive(obj, keys[i], obj[keys[i]])
}
}
}
function definedReactive(data, key, value) {
if(typeof val === ‘object’) {
new Observer(value)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumberable: true,
configurable: true,
get: function () {
dep.depend()
return value
},
set: function (newVal) {
if(value === newVal) {
return
}
value = newVal
dep.notify()
}
})
}
复制代码简单理解:

3.6Object的问题
由于Object类型数据是通过setter/getter来追踪的,所以在有些语法中,即使数据改变,vue也追踪不到。
什么情况无法侦测:

新增属性
删除属性

解决办法通过vue提供的两个API——vm. s e t v m . set和vm. delete。后续会慢慢讲解的。

发布了68 篇原创文章 · 获赞 1 · 访问量 945

猜你喜欢

转载自blog.csdn.net/A669MM/article/details/104929061