这一章就着重讲两个点:
响应式零碎如何收集依赖
响应式零碎如何更新视图
咱们晓得通过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.idif (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') } }}
参考vue实战视频解说:进入学习
咱们的callback
调用updated
钩子
讲到这里就有点超纲
了,咱们初始化渲染
会调用一个initRender
函数创立dom
,还有下面所述的nextTick
,前期都会讲,那么理解了更新机制
,下一章咱们就来实现一个让面试官都惊呆了
的双向绑定
咱们对Vue
的响应式零碎
有肯定的理解,并且晓得它是如何实现数据更新视图
,视图扭转数据的
,那么有这样的根底,咱们来手写一个MVVM
,以便面试的时候,吊打面试官(此为笑谈
,有余论,嘿嘿)。
那么先抛出一张在座的各位再也相熟不过的图:
1、当咱们new MVVM
之后有两步操作,Observer
,Compile
,咱们晓得Observer
是做数据劫持,Compile
是解析指令,那么问题来了:
Observer
为什么要做数据劫持?Compile
为什么要做解析指令?
带着这两个问题,咱们回顾一下往期内容:- 什么是
数据响应式
? 数据响应式原理
是什么?- 数据响应式是如何
实现
的?
数据响应式
就是数据双向绑定,就是把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
框数据关联,说白了也就是雷同的数据源,那咱们的数据源在哪呢?
//数据源dataconst 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的另一个外围概念组件零碎
了