众所周知, 这几年前端发展的非常迅速, 涌现了好几大不错的框架体系, 其中 vue.js 以入门门槛低实用性高深得广大前端喜爱. 今天我们就谈谈 vue.js 框架的核心 -- 响应式系统
说到响应式系统, 它的原理核心来自于 Object.defineProperty, 相信大多数人都认识它, 但今天也要基本的介绍介绍一下它.
我们可以看看官方的介绍:
然后我们在看看它要那些通用的属性:
知道该方法怎么用的时候, 我们就来实现就最基本的响应式系统原理:
这个图片在官方文档上就有, 仔细的观察一下, 我们不看所有的生命周期, new Vue() => init 这个阶段的时候, 数据就开始进行响应式的操作了.
我们定义一个函数, 用来表示视图更新, 调用这个函数就告诉大家视图更新啦.
- function cb (val) {
- /* 更新视图操作 */
- console.log("a=b=c=d=f=e=.....");
- }
接下来我们创建一个 defineReactive , 这个方法通过 Object.defineProperty 来实现对对象的响应式操作.
- function defineReactive (obj, key, val) {
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function getVal () {
- return val;
- },
- set: function setVal (newVal) {
- if (newVal === val) return;
- cb(newVal);
- }
- });
- }
写好这个后我们还需要定义另外一个方法 observer, 该方法我们用来遍历修改的对象, 对这些对象通过 defineReactive 函数进行相应的处理:
- function observer (value) {
- if (!value || (typeof value !== 'object')) {
- return;
- }
- Object.keys(value).forEach((key) => {
- defineReactive(value, key, value[key]);
- });
- }
这个时候基本大功告成了, 当然真理是需要实践一下, 这个时候我们封装一个 Vue:
- class Vue {
- constructor(options) {
- this._data = options.data;
- observer(this._data);
- }
- }
这里我们写一个 Vue 的构造函数, 对 options 里面的 data 进行处理, 当然, 这里面的 data 就相当于我们经常在 vue.js 模板里面的写的 data. 下面我们来 new 一个 Vue 的对象进行操作:
- let obj = new Vue({
- data: {
- test: "毁灭全人类"
- }
- });
- obj._data.test = "我的心愿是: 时间和平!";
这个时候视图就会更新了, 这就是 vue.js 的响应式基本原理.
既然是基本原理, 它实现的也是最基本的功能, 接下来我们看另外一个例子:
- new Vue({
- template:
- `<div>
- <span > 毁灭吧,{{a}}</span>
- <span > 毁灭吧,{{b}}</span>
- <div>`,
- data: {
- a: '全人类',
- b: '地球',
- c: '太阳系'
- }
- });
如果我们现在改变 c 的值: this.c = "全宇宙" 这个时候我们在调用 cb 是无用的, 但 vue 里面这时候发生什么? 我们不得而知. 我们留着疑问看下面一个例子:
假如现在我们有一个全局对象, 而且很多 vue 视图都调用了它:
- let echats = {
- options: 'pie'
- };
- let o1 = new Vue({
- template:
- `<div>
- <span>{{options}}</span>
- <div>`,
- data: echats
- });
- let o2 = new Vue({
- template:
- `<div>
- <span>{{options}}</span>
- <div>`,
- data: echats
- });
这时我们做了这样一个操作: echats.options = "line", 这时会发生什么? vue.js 又是怎么操作的呢?
根据上面的图我们可以知道, 在 getter 这个阶段会获取到相应 Watcher 实例对象, 然后 setter 被调用的时候, 会通过 Watcher 重新计算, 从而致使它关联的组件模板得以更新.
对比, 我们例 2 和例 3, 要实现这一系列的操作, 单单靠上面我们的基本原理的方法是不行的, 所以我们需要改变一下
订阅者 Dep
接下来我们来实现一个订阅者, 用来存放 Watcher 实例对象.
- class Dep {
- constructor () {
- /* 存放 Watcher 对象的数组 */
- this.subs = [];
- }
- /* 在 subs 中添加一个 Watcher 对象 */
- addSub (sub) {
- this.subs.push(sub);
- }
- /* 通知所有 Watcher 对象更新视图 */
- notify () {
- this.subs.forEach((sub) => {
- sub.update();
- })
- }
- }
这时我们在来实现一个观察者:
观察者 Watcher
- class Watcher {
- constructor () {
- Dep.target = this;
- }
- /* 视图更新 */
- update () {
- console.log("毁灭全人类");
- }
- }
- Dep.target = null;
下面我们在修改一下一开始列子的代码:
- function defineReactive (obj, key, val) {
- const dep = new Dep();
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter () {
- dep.addSub(Dep.target);
- return val;
- },
- set: function reactiveSetter (newVal) {
- if (newVal === val) return;
- dep.notify();
- }
- });
- }
- class Vue {
- constructor(options) {
- this._data = options.data;
- observer(this._data);
- new Watcher();
- console.log('render...', this._data.a);
- }
- }
通过上面的代码, 这个时候我们在回头看例 2 和例 3, 当我 data.c 改变的时候, 这里会发生什么呢? 现在我们知道每一个 data 的属性都对应一个 dep, 而每一个 dep 就对应一个或者多个 Watcher(多个视图调用的情况), 例 2 中我们改变了 data.c, 但视图上并没有对象的 Watcher, 那么它就没法调用 addSub 方法, 所以视图不会更新. 当然例 3 也从中得以理解. 最后贴一下我整理后的所有代码:
- const Observer = function(data) {
- for(let key in data) {
- defineReactive(data, key);
- }
- }
- const defineReactive = function (obj, key) {
- const dep = new Dep();
- let val = obj[key];
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get() {
- console.log('in get');
- dep.depend();
- return val;
- },
- set(newVal) {
- if(newVal === val) {
- return;
- }
- val = newVal;
- dep.notify();
- }
- });
- }
- const observe = function(data) {
- return new Observer(data);
- }
- const Vue = function(options) {
- const self = this;
- if (options && typeof options.data === 'function') {
- this._data = options.data.apply(this);
- }
- this.mount = function() {
- new Watcher(self, self.render);
- }
- this.render = () => {
- if(self) {
- return this._data.text;
- }
- }
- observe(this._data);
- }
- const Watcher = function(vm, fn) {
- const self = this;
- this.vm = vm;
- Dep.target = this;
- this.addDep = function(dep) {
- dep.addSub(self);
- }
- this.update = function() {
- console.log('is watcher update');
- }
- this.value = fn();
- Dep.target = null;
- }
- const Dep = function () {
- const self = this;
- this.target = null;
- this.subs = [];
- this.depend = function() {
- if(Dep.target) {
- Dep.target.addDep(self);
- }
- }
- this.addSub = function(watcher) {
- self.subs.push(watcher);
- }
- this.notify = function() {
- for(let i = 0; i < self.subs.lenth; i+= 1) {
- self.subs[i].update();
- }
- }
- }
- const vue = new Vue({
- data() {
- return {
- text: 'hahah'
- }
- }
- })
- vue.mount();
- vue._data.text = '123';
总结:
在 observer 的过程中会注册 get 方法, 该方法用来进行依赖收集. 在它的闭包中会有一个 Dep 对象, 这个对象用来存放 Watcher 对象的实例. 其实依赖收集的过程就是把 Watcher 实例存放到对应的 Dep 对象中去. get 方法可以让当前的 Watcher 对象 (Dep.target) 存放到它的 subs 中 (addSub) 方法, 在数据变化时, set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新.
后话: 参考了很多大牛的文章, 如果描述的有错, 请多多包涵~~
来源: http://www.qdfuns.com/article/16817/00a5f91f18fa5216135d6c14c81de3b9.html