这一章就着重讲两个点:
响应式系统如何收集依赖
响应式系统如何更新视图
我们知道通过Object.defineProperty
做了数据劫持,当数据改变的时候,get
方法收集依赖,进而set
方法调用dep.notify
方法去通知Watcher
调用本身update
方法去更新视图。那么我们抛开其他问题,就讨论get
,notify
,update
等方法,直接上代码:
get( )
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
我们知道Dep.target
在创建Watcher
的时候是null
,并且它只是起到一个标记的作用,当我们创建Watcher
实例的时候,我们的Dep.target
就会被赋值到Watcher
实例,进而放入target
栈中,我们这里调用的是pushTarget
函数:
// 将watcher实例赋值给Dep.target,用于依赖收集。同时将该实例存入target栈中
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
那我们继续执行到if (Dep.target)
语句的时候就会调用Dep.depend
函数:
// 将自身加入到全局的watcher中
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
那下面的childOb
是啥东西呢?
let childOb = !shallow && observe(val)
我们通过这个变量判断当前属性下面是否还有ob
属性,如果有的话继续调用Dep.depend
函数,没有的话则不处理。
我们还需要处理当前传入的value
类型,是数组属性的话则会调用dependArray
收集数组依赖
// 收集数组依赖
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}
那么收集依赖部分
到这里就完了现在进行下一步触发更新
了
set( )
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 判断NaN的情况
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
我们看到了下面的 set
函数触发了dep.notify()
方法
notify( )
// 通知所有订阅者
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
在notify
里面我们就做了一件事情,遍历subs
数组里面的所有Watcher
,逐一调用update
方法,也就是我们说的通知所有的订阅者Watcher
调用自身update
方法 update( )
update () {
if (this.lazy) {
// 计算属性会进来这段代码块
// 这里将dirty赋值为true
// 也不会马上去读取值
// 当render-watcher的update被触发时
// 重新渲染页面,计算属性会重新读值
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
那么update方法实现了什么呢?lazy
,dirty
,sync
又是啥?
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
// 这里将lazy的值赋值给了dirty
// 就是说实例化的时候dirty = lazy = true
this.dirty = this.lazy // for lazy watchers
那是控制计算属性的,当render—watcher
的方法update
被调用的时候,this.dirty
会变为true
会重新计算computed
值,渲染视图,我们这里不叙述。
那么我们直接看queueWatcher()
函数:
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
我们可以看到一个更新队列,更新队列指向:
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
我们的callback
调用updated
钩子
讲到这里就有点超纲
了,咱们初始化渲染
会调用一个initRender
函数创建dom
,还有上面所述的nextTick
,后期都会讲,那么了解了更新机制
,下一章我们就来实现一个让面试官都惊呆了
的双向绑定
我们对Vue
的响应式系统
有一定的了解,并且知道它是如何实现数据更新视图
,视图改变数据的
,那么有这样的基础,我们来手写一个MVVM
,以便面试的时候,吊打面试官(此为笑谈
,不足论,嘿嘿)。
那么先抛出一张在座的各位再也熟悉不过的图:
1、当我们new MVVM
之后有两步操作,Observer
,Compile
,我们知道Observer
是做数据劫持,Compile
是解析指令,那么问题来了:
Observer
为什么要做数据劫持?Compile
为什么要做解析指令?
带着这两个问题,我们回顾一下往期内容:- 什么是
数据响应式
? 数据响应式原理
是什么?- 数据响应式是如何
实现
的?
参考 Vue面试题详细解答
数据响应式
就是数据双向绑定,就是把Model
绑定到View
,当我们用JavaScript
代码更新Model
时,View
就会自动更新;如果用户更新了View
,那么Model
数据也被自动更新了,这种情况就是双向绑定。
数据响应式原理
Vue
实现数据响应式原理就是通过Object.defineProperty()
这个方法重新定义了对象获取属性值get
设置属性值set
的操作来实现的Vue3.0
中是通过ECMAScript6
中的proxy
对象代理来实现的。
那么本章节就是来实现数据响应式
的。
那么回答前面的两个问题,为什么要劫持数据
?为什么要解析指令
?
- 只有劫持到数据,才能对数据做到监听,以便于数据更改能够及时做到更新视图。
Vue
中自定义了N
多指令,只有解析它,我们JavaScript
才能认识它,并运行它。
诸如此类问题我们不再复述,下面开始实现数据响应式。
写一个demo
之前,我们应当整理好思路:
1. 首先实现整体的一个架构(包括MVVM类或者VUE类、Watcher类), /这里用到一个订阅发布者设计模式。
2. 然后实现MVVM中的由M到V,把模型里面的数据绑定到视图。
3. 最后实现V-M, 当文本框输入文本的时候,由文本事件触发更新模型中的数据
4. 同时也更新相对应的视图。
//html代码
<div id="app">
<h1>MVVM双向绑定</h1>
<div>
<div v-text="myText"></div>
<div v-text="myBox"></div>
<input type="text" v-model="myText" />
<input type="text" v-model="myBox" />
</div>
</div>
我们创建了两个div
与input
实现input
框数据关联,说白了也就是相同的数据源,那我们的数据源在哪呢?
//数据源data
const app = new Vue({
el: "#app",
data: {
myText: "大吉大利!今晚吃鸡!",
myBox: "我是一个盒子!",
},
});
可见我们需要一个Vue
类,也就是一个发布者,那么直接上代码:
//Vue类(发布者)
class Vue{
}
发布者有了,我们还需要有订阅者:
//Watcher类(订阅者)
class Watcher{
}
可见两者都有了,那么我们该怎么实现呢?
- 获取data数据
- 获取元素对象
- 构造一个存放订阅者的对象
class Vue {
constructor(optios) {
this.$data = optios.data; //获取数据
this.$el = document.querySelector(optios.el); //获取元素对象
this._directive = {}; // 存放订阅者
}
}
那么我们说了,我们需要劫持数据
,解析指令
,那么我们得构造两个方法。
class Vue {
constructor(optios) {
this.$data = optios.data; //获取数据
this.$el = document.querySelector(optios.el); //获取元素对象
this._directive = {}; // 存放订阅者
this.Observer(this.$data);
this.Compile(this.$el);
}
//劫持数据
Observer(data) {
Object.defineProperty(this.$data, key, {
get: function(){},
set: function(){}
},
});
}
//解析指令 //视图 --- >对象 -- >指令
Compile(el) {
}
}
一个是劫持数据
,一个是解析元素指令
,劫持到的属性要根据属性分配容器,当当前容器不存在该属性的时候,我们便需要把他添加到订阅器对象里面,等待通知更新。
for (let key in data) {
this._directive[key] = [];
let val =data[key];
let watch = this._directive[key];
}
那么解析指令,首先必须要递归
当前节点,是否还有子节点,是否有v-text
指令,v-model
指令。
let nodes = el.children;
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
//递归 查询所有当前对象子类是否再含有子类
if (node.children.length) {
this.Compile(nodes[i]);
}
//判断是否含有V-text指令
if (node.hasAttribute("v-text")) {
let attrVal = node.getAttribute("v-text");
this._directive[attrVal].push(
new Watcher(node, this, "innerHTML", attrVal)
);
}
//判断是否含有V-model指令
if (node.hasAttribute("v-model")) {
let attrVal = node.getAttribute("v-model");
this._directive[attrVal].push(
new Watcher(node, this, "value", attrVal)
);
node.addEventListener("input", () => {
//赋值到模型
this.$data[attrVal] = node.value;
// console.log(this.$data);
});
}
}
那么我们触发更新时候需要收集依赖,我们直接吧收集到的依赖return
出去
Object.defineProperty(this.$data, key, {
get: function(){
return val;
}
}
那么我们订阅者长什么样呢?我们订阅者,接收当前元素信息,MVVM对象,标识,属性。并且需要构造一个更新方法update
class Watcher {
constructor(el, vm, exp, attr) {
this.el = el;
this.vm = vm;
this.exp = exp;
this.attr = attr;
this.update();
}
//更新视图
update() {
this.el[this.exp] = this.vm.$data[this.attr];
//div.innerHTML/value = this.Vue.$data["myText/myBox"]
}
}
到这里已经快完成了,那么我们收集了依赖就要去,通知watcher去更新视图啊,那么来了:
Object.defineProperty(this.$data, key, {
get: function(){
return val;
},
set: function(newVal){
if(newVal !== val){
val = newVal;
watch.forEach(element => {
element.update();
});
}
},
});
做到这里,你就可以实现一个数据响应式了。
我们已经掌握了响应式原理,那我们开始着手Vue的另一个核心概念组件系统
了