我们都知道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}}
//一个依赖 //另一个依赖之后我们还需要改造一下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. delete。后续会慢慢讲解的。