Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

关于vue状态更新原理的理解 #15

Open
zp1112 opened this issue May 15, 2018 · 0 comments
Open

关于vue状态更新原理的理解 #15

zp1112 opened this issue May 15, 2018 · 0 comments
Labels

Comments

@zp1112
Copy link
Owner

zp1112 commented May 15, 2018

vue状态更新 vs jQuery操作DOM

今天被问到一个问题,假设页面只有一个按钮(仅仅只有一个,很简单的元素),有两种方式改变这个按钮上面的文字,一个是使用vue的数据绑定,另一个是使用jquery操作dom,问哪种方法性能更好。

首先,jquery和vue的差别在于,vue是先监听绑定数据,然后通知watcher,将变化放映到虚拟dom,然后操作真实dom,jquery是直接操作dom。

很多时候手工优化dom确实会比virtual dom效率高,对于比较简单的dom结构用手工优化没有问题,但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。至此,virtual dom的解决方案应运而生,virtual dom很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。解析vue2.0的diff算法

所以,对于dom操作频繁,不需要动画效果,就使用vue.js,对于dom操作不频繁,但又需要复杂的动画效果,就使用jquery。

vue状态更新的原理

大头来了,接下来我会一步一步实现简单意义上理解的vue状态更新原理,主要从数据,监听器,订阅者三个方面入手,目的不在于实现复杂可靠的mvvm框架,只是用于方便理解vue的双向数据绑定原理。

数据data

首先,定义一个数据源,该数据后续会被Obsever劫持,拦截它的get和set操作,并在get的时候添加订阅者,set的时候通知订阅者。定义一个全局的target,用于表示当前将要推入的订阅者,在get的时候推入调度器数组deps。注意这个data将作为全局共享的数据源。

新建一个文件mvvm.js

const data = {
  key: 1,
  user: {
    name: 'candy'
  }
}
// 定义一个全局的订阅者,表示当前将要推入的订阅者。将来就是那个watcher的实例
let target = null;

监听器Obsever

定义一个Observer监听器,将data劫持,拦截get和set。

function Observer(data) {
  this.data = data;
  this.walk(data); // 遍历第一层属性
}
Observer.prototype = {
  walk: function(data) {
    for (const key of Object.keys(data)) {
      this.convert(key, data[key]); // 劫持转化第一层属性
    }
  },
  convert: function(key, val) {
    this.defineReactive(this.data, key, val); 
  },
  defineReactive: function(data, key, val) {
    let childObj = observe(val); // 监听子属性
    Object.defineProperty(data, key, {
      configurable: false,
      enumerable: true,
      get: function() {
        console.log('哈哈,拦截到了get')
        return val;
      },
      set: function(newVal) {
        if (newVal === val) return;
        val = newVal;
        console.log('哈哈,拦截到了set')
      }
    })
  }
}

尝试给data读取和改变属性值,

data.key; //哈哈,拦截到了get
data.key = 111; //哈哈,拦截到了set

由于数据可能是多层嵌套的,所以需要遍历里面的属性和子属性,改造如下。

// 添加一个辅助函数
function observe(value) {
    if (!value || typeof value !== 'object') {
        return;
    }
    return new Observer(value);
};
Observer.prototype = {
  // .....省略
  defineReactive: function(data, key, val) {
    let childObj = observe(val); // 监听子属性
    Object.defineProperty(data, key, {
      configurable: false,
      enumerable: true,
      get: function() {
        console.log('哈哈,拦截到了get')
        return val;
      },
      set: function(newVal) {
        if (newVal === val) return;
        val = newVal;
        console.log('哈哈,拦截到了set')
        childObj = observe(newVal); // 监听新的子属性
      }
    })
  }
}

尝试给data读取和改变属性值,

data.user.name; //哈哈,拦截到了get
data.user.name = 'candy'; //哈哈,拦截到了set

拦截功能做到了,再改造一下使之具有订阅和发布功能。

// 定义一个全局的调度器deps
const deps = [];
Observer.prototype = {
  // ...省略
  defineReactive: function(data, key, val) {
    let childObj = observe(val); // 监听子属性
    Object.defineProperty(data, key, {
      configurable: false,
      enumerable: true,
      get: function() {
        if (target) {
          deps.push(target); // 若当前注册了target订阅者,将其推入到deps调度器中
        }
        return val;
      },
      set: function(newVal) {
        if (newVal === val) return;
        val = newVal;
        childObj = observe(newVal); // 监听新的子属性
        deps.forEach(target => target.update()); // 该属性改变时,遍历调度器,将变化发布给订阅者。
      }
    })
  }
}

上述代码实现了监听器,监听了数据属性的getter和setter,便于订阅和通知发布。

订阅者Watcher

上述代码中的target即是订阅者watcher实例,定义一个Watcher

function Watcher(vm, exp, cb) {
  this.cb = cb; // 订阅者监听到发布者发布的变化后,调用的回调,也是我们最终的目的,这个回调可以用于dom更新等后续操作。
  this.exp = exp; // 表达式,比如要获取data.user.name,就传入'user.name',获取data.key就传入'key'
  this.vm = vm; // 传入的数据源
  this.value = this.get(); // 获取数据源的数据,触发get事件,将this推入deps,实现订阅
}
watcher.prototype = {
  update: function() {
    this.run(); // update方法用于接收发布,执行run方法,并调用回调
  },
  run: function() {
      const value = this.get(); // 获取最新值
      const oldVal = this.value; // 取出旧值
      if (value !== oldVal) {
          this.value = value; // 保存新值
          this.cb.call(this, value, oldVal); // 执行回调
      }
  },
  get: function() {
    target = this; // 准备把自身推入deps
    const val = this.parseGetter(this.exp).call(this, this.vm); // parseGetter用于解析表达式
    target = null;
    return val;
  },
  parseGetter: function(exp) {
    if (/[^\w.$]/.test(exp)) return; 
    const exps = exp.split('.'); // 点操作符获取属性值
    return function(obj) {
       // obj是传入被改造过的数据源
        for (const item of exps) {
            if (!obj) return;
            obj = obj[item]; // 真正执行了被改造过的数据源的get
        }
        return obj;
    }
  }
}

见证奇迹的时刻到了,初始化几个Wathcer,定义callback函数

function callback(val, oldVal) {
  // 回调函数中获取到新值
  console.log(val, oldVal);
}
const watch = new watcher(data, 'key', callback);
data.key = 111;
const watch2 = new watcher(data, 'value', callback);
data.user= {
  sex: 2
};

boom !!! 可以看到控制台打出新值和旧值,成功监听了data的属性变化,并利用这个监听执行了绑定的回调。

虽然在此次的理解中没有涉及到dom上的双向绑定,但是已经到回调那一步了,接下来就是那个回调咋写的问题了,vue中定义了compile编译器,解析类似这种{{data.key}}和其他指令的约定格式,使之成为一个个订阅者。每种dom结构对应有自己各自的回调函数,当监听到数据变化时,要做的就是在回调函数中把新值反映到dom中。

加油,fighting!!

@zp1112 zp1112 added the vue label May 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant