前端攻城狮该了解的 Vue.2x 响应式原理

原创 2017年11月13日 00:00:00

640.gif?wxfrom=5&wx_lazy=1

本文来自作者 大师兄 在 GitChat 上分享「Vue.2x 源码分析之响应式原理」,阅读原文」查看交流实录

文末高能

编辑 | 黑石

原本文章的标题叫《源码解析》,不过后来一想还是以学习的态度写合适一点。在没有彻底掌握源码中每一个代码之前,说“解析”有点标题党了。

我们都知道 Vue 是一个非常典型的 MVVM 框架,它的核心功能:

  1. 双向数据绑定系统

  2. 组件化开发系统

本文我们就聊聊双向数据绑定,不管你是学过或者没学过,我相信看完本文你都会对 vue 有一个比较简单明确的了解。不过如果哪块有错误,还望指出。

很多朋友说自己读不懂,索性就“不敢”去读。要我说凡事均要去尝试,不尝试永远没有读懂的机会,如果你试着读了,并且坚持了,那你就真的能读懂。

读源码也是有技巧的,我的技巧就是: 抓住主线,从宏观到微观。

我们不能一开始就要求自己读懂所有的细节,基本不现实;最好是能找到一条主线,先把大体流程结构摸清楚,再深入到细节,逐项击破,形成对源码整体的认识。

比如,我们都知道 Vue 中更新数据后会采用 virtual DOM(虚拟dom)的方式更新 dom。

这个时候,如果你不了解 virtual DOM,那么听我一句“暂且不要去研究内部具体实现,因为这会使你丧失主线”,而你仅仅需要知道 virtual DOM 分为三个步骤:

  1. createElement( ): 用 JavaScript 对象(虚拟树) 描述 真实 DOM 对象(真实树)

  2. diff(oldNode, newNode) : 对比新旧两个虚拟树的区别,收集差异

  3. patch( ) : 将差异应用到真实 DOM 树

回过头我们再去研究这个分支,仅此而已。

0?wx_fmt=png

上图对于学习过 Vue 的朋友来说应该不陌生吧,来自 Vue 官网深入响应式原理,建议先看图一分钟。

为了说明原理,我们会把虚拟 dom 这块用 fragment 来代替(这个是1.x版本的实现)。

并且只考虑数据为对象的情况。记住今天的主线:搞清楚响应式原理,实现一个简单的 MVVM 框架。

由一个例子开始:

template:

0?wx_fmt=png

javascript:

0?wx_fmt=png

我们要解决的问题有:

如何将 data 中的数据渲染到真实的宿主环境中?

template 是如何被编译成真实环境中可用的 HTML 的?

如何通过“响应式”修改数据?

计算属性 getWeChatblog 如何和 data 中的数据绑定的?

带着这些问题开始我们 Vues 的开发。尽量和 Vue 代码保持一致。下面是目录结构:

0?wx_fmt=png

入口文件:

0?wx_fmt=png

export 构造函数 Vues,不清楚 ES6 中 module 可以可以点这里

初始化

进入到 ./instance/index.js 就可以看到 Vues 构造函数

0?wx_fmt=png

其实就是调用 this._init(options),我们先不看这个函数是做什么的,这里有一个疑惑点,我们并没有在 Vues 构造函数内部申明 _init() 函数呀,那是因为我们调用了一个 initMixin 函数,我们来看看此函数:

0?wx_fmt=png

ok,原来 _init() 是在这里定义的,在 Vues 的原型上扩展了此方法。Vue也用了这种形式。

在 _init() 首先调用了 initState(this) :

0?wx_fmt=png

但是这里有个问题,从代码中可看出监听的数据对象是 $options.data,每次需要更新视图,则必须通过 vm._data.dsx= '前端开发大师兄';这样的方式来改变数据。

这显然不符合 Vue 中的赋值方式,我们所期望的调用方式应该是这样的:  vm.dsx = '前端开发大师兄';

所以这里需要给 Vues 实例添加一个属性代理的方法 _proxyData(),使访问 vm 的属性代理为访问 vm._data 的属性,方法代码如下:

0?wx_fmt=png

我们初始化计算属性 computed,具体就是调用了函数 _initComputed(vm),来看看代码:

0?wx_fmt=png

我们可以看出我们已经两次用到同一个方法——Object.defineProperty(),这就是 Vue 实现响应式数据的利器之一。举个栗子来说明。

0?wx_fmt=png

我们为对象a 通过该方法定义了一个b属性,然后定义了 set 和 get 属性,这样我们给b 赋值就会触发它的 set 属性,我们获取值就会触发它的 get 属性。

这样我们就可以劫持数据,然后执行我们的操作。详细点可以看这里。

observe

这是我们第一个重点,observe 很明显我们会用到观察者模式,事实上Vue也是这么干的。我们看一下 observe() 做了什么?

0?wx_fmt=png

就是做了类型判断,之后就直接实例化 Oberver ,参数为 data 对象。

官网的 Reactivity in Depth 上有这么句话:

When you pass a plain JavaScript object to a Vue instance as its data
option, Vue.js will walk through all of its properties and convert
them to getter/setters

The getter/setters are invisible to the user, but under the hood they
enable Vue.js to perform dependency-tracking and change-notification
when properties are accessed or modified

observe 使 data 变成“发布者”,watcher 是订阅者,订阅 data 的变化。那如何使 data 变为“发布者”呢?

当然是我们的利器—-Object.defineProperty() ,将数据对象 data 的属性转换为访问器属性。看看我们的代码:

0?wx_fmt=png

我们遍历 data 对象的所有可配置属性,最终调用了 defineReactive() 函数。

将需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 set 和 get 。

先看看 defineReactive() 的我们是怎么实现的:

0?wx_fmt=png

这个地方有一个值得思考的点,如果修改一个数组的成员,该成员是一个对象,那只需要递归对数组的成员进行双向绑定即可。

但这时候出现了一个问题,如果我们进行 pop、push 等操作的时候,push 进去的对象根本没有进行过双向绑定,更别说 pop 了,那么我们如何监听数组的这些变化呢?

Vue.js 提供的方法是重写 push、pop、shift、unshift、splice、sort、reverse 这七个数组方法。

修改数组原型方法的代码可以参考observer/array.js 以及 observer/index.js。

还有就是利用 vue.set() ,借用官方的 API

0?wx_fmt=png

get 的方法主要用来进行依赖收集,就是添加订阅者。

所以我们只要在最开始进行一次 render,那么所有被渲染所依赖的 data 中的数据就会被 getter 收集到 Dep 的 subs 中去。

set 方法会在对象被修改的时候触发(不存在添加属性的情况,添加属性请用Vue.set),这时候 set 会通知闭包中的 Dep,Dep 中有一些订阅了这个对象改变的 Watcher 观察者对象,Dep 会通知 Watcher 对象更新视图。

我们用一个简单的栗子来说明观察者模式:

0?wx_fmt=png

那应用到我们这里就是:每个 data 属性值在 defineReactive 函数监听处理的时候,添加一个主题对象,当 data 属性发生改变,通过 set 函数去通知所有的观察者们。

 那么如何添加观察者们呢,就是在 complie 函数编译 template 时,通过初始化 value 值,触发 set 函数,在 set 函数中为主题对象添加观察者。有点难理解?直接看代码就明白了。

ok,我们继续。看看我们的 Dep :

0?wx_fmt=png

那么你可能有疑问了。。谁是订阅者。。对,没错就是 Watcher。。一旦 dep.notify()
就遍历订阅者,也就是 Watcher,并调用他的 update() 方法。

Watcher

如何实现一个 Watcher,通过上面的分析我可以确定得要一个 update() 方法。见下图:

0?wx_fmt=png

很关键的一个地方就是 this.value = this.get() ,这个就是我们之前说的最开始要进行一次 render,我们看 get() 实现:

0?wx_fmt=png

这个最关键了,主要做了以下几件事:

  1. 把当前 watcher 赋值给 Dep.target

  2. 获取 value 值就会触发 Oberver 中定义的 get

  3. 执行 dep.append() 添加订阅者

  4. 重新将 Dep.target 赋值为 null

  5. 返回 vaule 值

关于 notify

当监听的数据赋值就会被 set 拦截,然后执行 dep.notify() ,遍历订阅者(watcher)执行其 update() 方法,update 调用的其实是 this.run() 自己的 run 方法。我们看看:

0?wx_fmt=png

有一个值得注意的地方,this.cb.call(this.vm, value, oldVal); 这个 cb 是什么?没错就是我们在编译 template 的时候为每一个指令绑定的更新 dom 的函数 。

最后对 watcher 做一个总结:

  1. 每次调用 run() 的时候会触发相应属性的 get

  2. get 里面会触发 dep.depend(),继而触发这里的 addDep

  3. 假如相应属性的 dep.id 已经在当前 watcher 的 depIds 里,说明不是一个新的属性,仅仅是改变了其值而已。

    则不需要将当前 watcher 添加到该属性的 dep 里

  4. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里,如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性。

    则需要将当前watcher(child.name)加入到新的 child.name 的 dep 里,因为此时 child.name 是个新值,之前的 set,dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中。

    通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了。

  5. 每个子属性的 watcher 在添加到子属性的 dep 的同时,也会添加到父属性的 dep。

  6. 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的 watcher 也能收到通知进行 update。

    这一步是在 this.get() --> this.getVMVal() 里面完成,forEach 时会从父级开始取值,间接调用了它的 get,触发 addDep(),在整个 forEach 过程,当前 wacher 都会加入到每个父级过程属性的 dep。

    例如:当前 watcher 的是 'child.child.name', 那么 child, child.child, child.child.name 这三个属性的 dep 都会加入当前watcher。

到时候看看 Compile 了。

Compile

还记得在 _init() 函数最后那行代码吗?

0?wx_fmt=png

new Compile ,看看做了什么?

0?wx_fmt=png

注释说的很明白,就不做解释。看看如何转化 fragment :

0?wx_fmt=png

就是遍历子节点添加到 fragment 。

0?wx_fmt=png

接下来我们就会对 fragment 节点包括子节点遍历,判断其节点类型,然后调用对应的解析函数解析其中的指令。

我们只看一个 compile:

0?wx_fmt=png

内置的指令处理方法:

0?wx_fmt=png

最终都调用同一个方法bind() ,我们先看看它是做了什么?

0?wx_fmt=png

  1. 获取对应指令的更新方法,并执行

  2. new Watcher,在回调函数执行 updaterFn

对于第一条,在执行 updaterFn 的时候会调用 this._getVMVal(vm, exp) :

0?wx_fmt=png

很简单,就是获取对应的数据返回。我们看一个text类型的更新函数:

0?wx_fmt=png

这个也是没毛病吧?ok。

第二条,new Watcher,在回调函数执行 updaterFn。还记得之前讲Watcher时提过一句:

0?wx_fmt=png

还没记住,我就再贴一次图了

0?wx_fmt=png

哈哈哈,明白了吧。那这个cb啥时执行呢?

当我们修改了数据就会触发对应的set ,然后就会调用 dep.notify();,通知订阅者,再调用订阅者的 update() 方法,update() 方法就会调用 this.run( ) ,run() 就会执行下面这一句:

0?wx_fmt=png

最后补充,input 的 v-model 双向数据绑定,其实就是监听了 input 事件,还是贴代码:

0?wx_fmt=png

在 input 事件回调执行 _setVMVal() 方法重新设置一次值。最后再贴一次代码,感觉我贴了好多,不过都是为了把事情说清楚。

0?wx_fmt=png

到这我们就完成了一个缩减版的 Vue,当然 Vue 功能远远不止这些。我们这只是凤毛麟角。

学会这个对于你看真正的 Vue 源码帮助绝对很大,因为逻辑都是相似的。最后再看看下图,回味一下整个过程。

0?wx_fmt=png

篇幅比较长,有时还很罗嗦,当然我只是想更清楚的讲解,真怕漏掉那个难点。

此次分享的所有源码均上传 git https://github.com/GGwujun/Vues.git

Over. Thanks.

近期热文

前端工程师“应试”指南

如何用 Node.js 爬虫?

两款敏捷工具,治好你碎片化交付硬伤

改做人工智能之前,90%的人都没能给自己定位

想入行 AI,别让那些技术培训坑了你...


请给一个不学 Vue 的理由

如果非要让我说一个不学 Vue 的理由,可能是它的写法太方便了……

0?wx_fmt=jpeg

「阅读原文」看交流实录,你想知道的都在这里

版权声明:本文为GitChat作者的原创文章,未经 GitChat 允许不得转载。

慕课网Web前端工程师成长第一阶段(基础篇)的四步走

慕课网Web前端工程师成长第一阶段(基础篇) 第一步 了解HTML和CSS HTML/CSS做为前端工程师第一门必修课,从简单标签知识到布局基础的学习。你可以将UI设计稿转化成静态...
  • qq443068902
  • qq443068902
  • 2015年01月04日 15:47
  • 1218

IT公司是动物园——程序猿、攻城狮、射鸡师、产品锦鲤、西衣鸥!

IT公司是动物园——程序猿、攻城狮、射鸡师、产品锦鲤、西衣鸥!
  • xinxin19881112
  • xinxin19881112
  • 2016年04月15日 00:14
  • 5076

测试“攻城狮”的生活(搞笑版)

版本上线前, 小明加班到深夜,精神疲惫的他为了发泄心中的苦闷,冲到空无一人的楼梯间高唱了一句:“在那山的那边海的那边有一群蓝精灵! ” 忽然,楼下传来一个哀怨的声音:"他们苦B又聪明,他们加班到天明!...
  • sogouqa
  • sogouqa
  • 2015年04月23日 03:20
  • 566

【备忘】安卓入门之Android攻城狮[慕课网内部课程] 下载

Android攻城狮的第一门课\1、搭建Android开发环境.zip Android攻城狮的第一门课\2、Android项目结构介绍.zip Android攻城狮的第一门课\3、显示以及输入文本...
  • javashenzhi
  • javashenzhi
  • 2016年12月23日 14:40
  • 34

JAVA攻城狮学习路线

JAVA攻城狮学习路线指导
  • Code_Road
  • Code_Road
  • 2017年02月24日 16:18
  • 403

关于web前端攻城狮的职业规划(小白看了都惊呆了...)

关于一个WEB前端的职业规划,其实是有各种的答案,没有哪种答案是完全正确的,全凭自己的选择,只要是自己选定了,坚持去认真走就好。但是,任何规划和目标的实现都依赖于知识的积累,而知识的积累来源于学习及学...
  • u014326381
  • u014326381
  • 2015年08月19日 21:38
  • 6680

攻城狮与程序猿的区别

攻城狮与程序猿的形象代言 程序猿的“官方”解释是:是一种近几十年来出现的新物种,是工业革命的产物。英文(ProgrammerMonkey)是一种非常特殊的、可以从事程序开发、维护的动物。一般分为程序...
  • yutian130
  • yutian130
  • 2012年08月09日 11:03
  • 868

JAVA 攻城狮 第十一天

今天是学习java的第十一天 是军训结束的日子 说真心话 我有点舍不得 意料之中 评选上了 优秀标兵 然后今天的情况说一下 早上在教室坐了一上午 没有训练 下午在教室坐了一会然后就解散了 正式...
  • PolarAurora
  • PolarAurora
  • 2017年07月09日 23:48
  • 113

女程序员再谈中年危机:攻城狮不可承受之重

点击上方“程序人生”,选择“置顶公众号”第一时间关注程序猿(媛)身边的故事总有报道称,各个职业的风险系数里,程序员和快递员都是数一数二的高。前段时间又出了中兴员工自杀事件,再次为程序员这个职业添上了“...
  • csdnsevenn
  • csdnsevenn
  • 2018年01月04日 00:00
  • 4620

Android优秀资源整理合集(论菜鸟到高级攻城狮)

时间一长,发现在平时逛论坛,订阅号或者其他人推荐的优秀干货,浏览器随机的收藏已经太乱了。抽空整理下,顺便真心推荐大家看看。至少对于我来说,从菜鸟到现在的进步全靠它们!! Android基础相关 1.A...
  • u011176685
  • u011176685
  • 2016年05月17日 11:24
  • 1213
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:前端攻城狮该了解的 Vue.2x 响应式原理
举报原因:
原因补充:

(最多只允许输入30个字)