内容简介:vue 无疑是一个非常棒的前端MVVM库,怀着好奇的心情开始看VUE源码,当然遇到了很多的疑问,也查了很多的资料看了一些文章。但是这些资料很多都忽略了很重要的部分或者是一些重要的细节,亦或是一些很重要的部分没有指出,特别是在computed的实现上。所以才打算写这篇文章,记录一下自己的学习过程,当然也希望能给其他想了解VUE源码的童鞋一点参考。如果笔者在某些地方理解有误,也欢迎批评指正出来,一起学习。为了加深理解,我按着源码的思路造了一个简易的轮子,基本核心的实现是与VUE源代码一致。测试 demo。仓库
vue 无疑是一个非常棒的前端MVVM库,怀着好奇的心情开始看VUE源码,当然遇到了很多的疑问,也查了很多的资料看了一些文章。但是这些资料很多都忽略了很重要的部分或者是一些重要的细节,亦或是一些很重要的部分没有指出,特别是在computed的实现上。所以才打算写这篇文章,记录一下自己的学习过程,当然也希望能给其他想了解VUE源码的童鞋一点参考。如果笔者在某些地方理解有误,也欢迎批评指正出来,一起学习。
为了加深理解,我按着源码的思路造了一个简易的轮子,基本核心的实现是与VUE源代码一致。测试 demo。仓库的地址: eltonchan/rollup-ts
VUE的源码采用rollup和flow至于为什么不采用typescript,主要考虑工程上成本和收益的考量, 这一点尤大在知乎也有说过。(3.0+版本确定改用typesript)
Vue 2.0 为什么选用 Flow 进行静态代码检查而不是直接使用TypeScript
不懂rollup 与typescript 也没关系,本项目已经配置好了, 只需要先执行npm i (或者cnpm i)安装相应依赖,然后 npm start 启动就可以。 npm run build 构建,默认是输出umd格式,如果需要cmd或者amd 可以在rollup.config.js配置文件修改。
output: { file: 'dist/bundle.js', format: 'umd', name: 'myBundle', sourcemap: true } 复制代码
questions ? 带着问题去了解一个事物往往能带来更好的收益,那我们就从下面几个问题开始
- 如何对http://this.xxx的访问代理到http://this._data.xxx 上 ?
- 如何实现数据劫持,监听数据的读写操作 ?
- 如何实现依赖缓存 ?
- template 改变的时候 如何清理依赖项集合? eg: v-if 、组件销毁
- 如何实现数据修改 dom更新 ?
vue实现双向绑定原理,主要是利用Object.defineProperty getter/setter(事实上,大多数响应式编程的库都是利用这个实现的,比如非常棒的mobx.js)和发布订阅模式(定义了对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将获得通知),而在vue中,watcher 就是订阅者,而一对多的依赖关系 就是指data上的属性与watcher,而data上的属性如何与watcher 关联起来, dep 就是桥梁, 所以搞懂 dep, watcher, observe三者的关系,自然就搞懂了vue实现双向绑定的原理了。
一、 Proxy 回到第一个问题, 答案其实是:对于每一个 data 上的key,都在 vm 上做一个代理,实际操作的是 this._data、 实现的代码如下:
export function proxy (target: IVue, sourceKey: string, key: string) { Object.defineProperty(target, key, { enumerable: true, configurable: true, get() { return this[sourceKey][key]; }, set(val: any) { this[sourceKey][key] = val; } }); } 复制代码
可以看出获取和修改this.xx 都是在获取或者修改this.data.xx;
二、Observer 用于把data上的属性封装成可观察的属性 用Object.defineProperty来拦截对象的读写gettet/setter操作, 在获取的时候收集依赖, 在修改的时候通知相关的依赖。
walk(data): void { if (!data || typeof data !== 'object') return; Object.keys(data).forEach(key => { this.defineReactive({ data, key, value: data[key] }); }); } defineReactive({ data, key, value }: IReactive): void { const dep = new Dep(); this.walk(value); const self = this; Object.defineProperty(data, key, { enumerable: true, configurable: true, get() { if (Dep.target) { Dep.target.addDep(dep); } return value; }, set(newVal: any): void { if (value === newVal) return; self.walk(value); value = newVal; dep.notify(); } }); } 复制代码
可以看出, 在get的时候收集依赖,而Dep.target 其实就是watcher, 等下讲到watcher的时候再回过来, 这里要关注dep 其实dep在这里是一个闭包环境,在执行get 或者set的时候 还可以访问到创建的dep. 比如 this.name当在获取this.name的值的时候 会创建一个Dep的实例, 把watcher 添加到这个dep中。
为什么对象上新增属性不会监听,而修改整个对象为什么能检测到子属性的变化 ?
由于 JavaScript 的限制,Vue 不能检测对象属性的添加或删除(当然mobx也不例外的)。所以可观察的对象属性的添加或者删除无法触发set 方法,而直接修改对象则可以,而在set 方法中则会判断新值是否是对象数组类型,如果是 则子属性封装成可观察的属性,这也是set中self.walk(value);的作用。
三、Watcher 刚才提到了watcher,从上图中也可以看到了watcher的作用 事实上,每一个computed属性和watch 都会new 一个 Watcher。接下来会讲到。先来看watcher的实现。
constructor( vm: IVue, expression: Function | string, cb: Function, ) { this.vm = vm; vm._watchers.push(this); this.cb = cb || noop; this.id = ++uid; // 处理watch 的情况 if (typeof expression === 'function') { this.getter = expression; } else { this.getter = () => vm[expression]; } this.expression = expression.toString(); this.depIds = new Set(); this.newDepIds = new Set(); this.deps = []; this.newDeps = []; this.value = this.get(); } 复制代码
这里的expression,对于初始化用来渲染视图的watcher来说,就是render方法,对于computed来说就是表达式,对于watch才是key,所以这边需要判断是字符串还是函数,而getter方法是用来取value的。这边有个depIds,deps,但是又有个newDepIds,newDeps,为什么这样设计,接下去再讲,先看this.value = this.get();可以看出在这里给watcher的value赋值,再来看get方法。
get() :void { Dep.target = this; const value = this.getter.call(this.vm); // 执行一次get 收集依赖 Dep.target = null; this.cleanupDeps(); // 清除依赖 return value; } 复制代码
可以看到getter是用来取值的,当执行这一行代码的时候,以render的那个watcher为例,会执行VNode render 当遇到{{ msg }}的表达式的时候会取值,这个时候会触发msg的get方法,而此时的Dep.target 就是这个watcher, 所以我们会把这个render的watcher和msg这个属性关联起,也就是msg的dep已经有render的这个watcher了。这个就是Watcher,Dep,Observer的关系。我们再来看Dep:
export default class Dep implements IDep { static target: any = null; subs:any = []; id; constructor () { this.id = uid++; this.subs = []; } addSub(sub: IWatcher): void { if (this.subs.find(o => o.id === sub.id)) return; this.subs.push(sub); } removeSub (sub: IWatcher) { const idx = this.subs.findIndex(o => o.id === sub.id); if (idx >= 0) this.subs.splice(idx, 1); } notify():void { this.subs.forEach((sub: Isub) => { sub.update(); }) } depend () { if (Dep.target) { Dep.target.addDep(this); } } } 复制代码
Dep的实现很简单,这边看notify的方法,我们知道在修改data上的属性的时候回触发set,然后触发notify方法,然后我们知道sub就是watcher,所以watcher.update方法就是修改属性所执行的方法,回到watcher看这个update的实现。
update() { // 推送到观察者队列中,下一个tick时调用。*/ queueWatcher(this); } run(cb) { const value = this.get(); if (value !== this.value) { const oldValue = this.value; this.value = value; cb.call(this.vm, value, oldValue); } } 复制代码
update方法并没有直接render vNode。而是把watcher推到一个队列中,事实上vue是的更新dom是异步的,为什么要异步更新队列,这边摘抄了一下官网的描述:Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。其实这是一种非常好的性能优化方案,我们设想一下如果在mounted中循环赋值,如果不采用异步更新策略,每一个赋值都更新,完全是一种浪费。
四、nextTick 关于nextTick其实很多文章写的都不错,这边就不详细介绍了。涉及到的概念可以点击下面链接查看:
JavaScript 运行机制详解:再谈Event Loop
五、Computed 计算属性是基于它们的依赖进行缓存的。只在相关依赖发生改变时它们才会重新求值 回到开始的那个问题,如何实现依赖缓存? name的更新如何让info也更新,如果name不变,info如何取值?
刚才在讲watcher的时候,提到过每个computed会实例化一个Watcher,从下面代码中也可以看出来,每一个computed属性都有一个订阅者watcher。
initComputed(computed) { if (!computed || typeof computed !== 'object') return; const keys = Object.keys(computed); const watchers = this._computedWatchers; let i = keys.length; while(i--) { const key = keys[i]; const func = computed[key]; watchers[key] = new Watcher( this, func || noop, noop, ); defineComputed(this, key); } } 复制代码
看这个例子:
computed: { info() { console.info('computed update'); return this.name + 'hello'; } }, 复制代码
watcher 的getter方法就是computed属性的表达式,而在执行this.value = this.get();这个value就会是表达式的运行结果,所以其实Vue是把info的值存储在它的watcher的value里面的,然后又知道在取name的值的时候,会触发name的get方法,此时的Dep.target 就是这个info的watcher,而dep是一个闭包,还是之前收集name的那个dep, 所以name的dep就会有两个watcher,[renderWatcher, computedWatcher], 当name更新的时候,这两个订阅者watcher都会收到通知,这也就是name的更新让info也更新。
那info的值是watcher的value, 所以这边要做一个代理,把computed属性的取值代理到对应watcher的value,实现起来也很简单。
export default function defineComputed(vm: IVue, key: string) { Object.defineProperty(vm, key, { enumerable: true, configurable: true, get() { const watcher = vm._computedWatchers && vm._computedWatchers[key]; return watcher.value; }, }); } 复制代码
六、依赖更新
<p v-if="switch">{{ name }}</p> 复制代码
假设switch由true切换成false时候,是需要把name上面的renderWatcher删除掉的,所以需要用depIds和deps的属性来记录dep。
addDep(dep: Dep) { const id = dep.id; if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } } cleanupDeps() { let i = this.deps.length; while (i--) { const dep = this.deps[i]; if (!this.depIds.has(dep.id)) { dep.removeSub(this); } } const tmp = this.depIds; this.depIds = this.newDepIds; this.newDepIds = tmp; this.newDepIds.clear(); const deps = this.deps; this.deps = this.newDeps; this.newDeps = deps; this.newDeps.length = 0; } 复制代码
这里把newDepIds赋值给了depIds, 然后newDepIds再清空,deps也是这样的操作,这是一种效率很高的操作,避免使用了深拷贝。添加依赖的时候都是用newDepIds,newDeps来记录,删除的时候会去deps里面遍历查找,等删除了再把newDepIds赋值给depIds,这样能保证在更新依赖的时候,没有使用的依赖会从这个watcher中移除。
七、watch 为什么watch 一个对象的时候 oldValue == value ?
watch的属性也是一个实例化的Watcher,只是这个时候的expression是key,value 是vm[key],而cb就是回调函数,所以这个时候对应属性的dep中自然就有这个watcher。
initWatch(watch) { if (!watch || typeof watch !== 'object') return; const keys = Object.keys(watch); let i = keys.length; while(i--) { const key = keys[i]; const cb = watch[key]; new Watcher(this, key, cb); } } 复制代码
当属性更新的时候,会执行到这个run方法, 当watch一个对象的时候,watcher的value其实是一个引用,修改这个属性的时候,this.value也同步修改了,所以也就是为什么oldValue == value了, 至于作者为什么这么设计,我想肯定是有他原因的。
run(cb) { const value = this.get(); if (value !== this.value) { const oldValue = this.value; this.value = value; cb.call(this.vm, value, oldValue); } } 复制代码
八、Compile vue 2+ 已经使用VNode了,这部分还没有细致研究过,所以我这边自己写了个简易的Compile,这部分已经和源码没有关系了。主要用到了DocumentFragment和闭包而已,有兴趣的童鞋可以到这个仓库查看。
components vnode 待补充...
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
UML基础、案例与应用
施穆勒 / 李虎、赵龙刚 / 人民邮电出版社 / 2004-7-1 / 42.00元
本书教读者循序渐进地、系统地学习UML基础知识和应用技术。和前一版相比,本书内容根据UML 2.0进行了补充和更新,随书光盘包含了建模工具Poseidon的试用版。 全书分为三部分24章。第一部分“基础知识”包括第1章到第15章,主要是介绍UML语言的基础知识以及面向对象的概念和思想,还简单介绍了UML在开发过程的应用方法。第二部分“学习案例”包括第16章到第22章,结合实例详细分析了UML的应用......一起来看看 《UML基础、案例与应用》 这本书的介绍吧!