Skip to content

Latest commit

 

History

History
232 lines (190 loc) · 8.08 KB

Vue方式.md

File metadata and controls

232 lines (190 loc) · 8.08 KB

Vue方式

本文介绍Vue.js响应式的实现原理。在vue官方文档中关于其响应式原理有一个简单的介绍: https://cn.vuejs.org/v2/guide/reactivity.html,其中还给出一张图来解释:

https://cn.vuejs.org/images/data.png

可以看到Vue的思想也是类似MVVM,在应用程序员看来,model层和view层解耦,开发者改变model数据,UI会自动响应数据的变化。我们将从内部原理细节去立即它的响应式原理。

我们要实现:

let vm = new MVVM({
  el: '#app',
  data: {
    name: "luoxia",
    info: {
      year: 21,
      addr: "China"
    }
  }
});

vm.$watch("name",newName=>{
  console.log("name变化了哦:",newName)
});
<div id="app">
    hello,{{name}}
    <input type="text" l-model="name" />
</div>

原理中主要涉及到ObserveWatcherDep几个概念,用一张图解释它们的关系:

https://github.com/DMQ/mvvm/raw/master/img/2.png

Observe的过程

在创建vm实例的步骤中,有一个很重要的过程是对data中的属性进行getter/setter化,对data中的每个属性进行监听,处理目标对属性的绑定,以及属性值变化后通知订阅者。Vue使用Object.defineProperty()来实现。

class MVVM {
  constructor(options={}){
    this.$options = options;
    this._data = options.data;

    observe(this._data);
    // ...
  }
}

class Observer {
  constructor(data){
    const keys = Object.keys(data);
    for(let key of keys){
      let value = data[key];
      let dep = this.dep = new Dep(); // dep理解为Pub/Sub中心,也是data中属性的观察者
      observe(value); //递归observe

      Object.defineProperty(data,key,{
        configurable: true,
        get(){
          if(Dep.target){
            console.log("初始绑定:",key,value);
            dep.addSub(Dep.target); // 通知观察者dep,让dep加入watcher到dep维护的订阅者队列
          }
          return value;
        },
        set(newVal){
          if(newVal === value){
            return;
          }
          value = newVal;
          observe(newVal);
          dep.notify(); // 通知观察者dep属性变化
        }
      })
    }
  }
}

function observe(data){
  if(!data || typeof data !== 'object'){
    return;
  }
  return new Observer(data);
}

Watcher

在模板编译过程中,每遇到一个数据绑定,就会生成一个订阅该属性变化的watcher,它具有update()方法,pub/sub中心接到来自于setter的属性变化通知,调用所有订阅者的update()方法,在watcherupdate()方法中,回去调用更新UI的方法:

class Watcher {
  constructor(vm,exp,cb){
    this.cb = cb; // 将用于更新UI
    this.vm = vm; // vm实例
    this.exp = exp; // 正则匹配出来的属性名
    let arr = exp.split('.');
    let val = vm;
    Dep.target = this; // 用于识别初次访问该属性的getter,用于加入观察者到订阅者队列
    arr.forEach(key => {
      val = val[key]; //会访问该属性的getter
    });
    this.oldValue = val; // 记录第一次绑定时的属性值
    Dep.target = null; // 防止后续访问该属性的getter重复加入订阅者队列
  }
  update(){
    // 获取属性的最新值
    let arr = this.exp.split('.');
    let val = this.vm;
    arr.forEach(key => {
      val = val[key];
    });
    
    if(this.oldValue !== val){
      this.oldValue = val;
      this.cb(val); // 更新UI的回调
    }
  }
}

Compiler

Compiler用于编译模板,在模板中会有对vm实例中data相应属性的绑定,在编译过程中主要做的事情:

  • 匹配出绑定属性的地方
  • 为该次绑定添加相应的watcherwatcher回调中更新UI
  • 初始绑定值的替换
  • 嵌入到DOM结构中
compile(){
    let vm = this;

    vm.$el = document.querySelector(this.$options.el);
    let fragment = document.createDocumentFragment();

    var child;
    while (child = vm.$el.firstChild) {
        fragment.appendChild(child);    // 此时将el中的内容放入内存中
    }
    // 对el里面的内容进行替换
    function replace(frag) {
        Array.from(frag.childNodes).forEach(node => {
            let txt = node.textContent;
            let reg = /\{\{(.*?)\}\}/g;   // 正则匹配{{}}

            if (node.nodeType === 3 && reg.test(txt)) { // 即是文本节点又有大括号的情况{{}}

                    // 初次数据的替换
                    node.textContent = txt.replace(reg,vm.getExpVal(RegExp.$1)).trim();

                    // 添加观察者
                    new Watcher(vm,RegExp.$1,newVal=>{
                        // 更新UI,用trim方法去除一下首尾空格
                    node.textContent = txt.replace(reg, newVal).trim();
                    });
                
            }

            if (node.nodeType === 1) {  // 元素节点
                let nodeAttr = node.attributes; // 获取dom上的所有属性,是个类数组
                Array.from(nodeAttr).forEach(attr => {
                    let name = attr.name;   // l-model  type
                    let exp = attr.value;   // c        text
                    if (name.includes('l-')){
                        node.value = vm[exp];   // this.c 为 2
                        // 数据双向绑定
                        node.addEventListener('input', e => {
                          let newVal = e.target.value;
                          // 相当于给this.c赋了一个新值
                          // 而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
                          vm[exp] = newVal;   
                        });
                        // 监听变化
                        new Watcher(vm, exp, function(newVal) {
                            node.value = newVal;   // 当watcher触发时会自动将内容放进输入框中
                        });
                    }
                    
                    
                    
                });
            }
            // 如果还有子节点,继续递归replace
            if (node.childNodes && node.childNodes.length) {
                replace(node);
            }
        });
        }

    replace(fragment);
    vm.$el.appendChild(fragment);
}

Dep

在我的理解当中,Dep充当两种角色:

  • Observer的联系:在创建Observer时,会实例化一个Dep对象,这个dep实例充当属性的观察者,属性变化时,setter内部会调用dep.notify()用于将属性变化通知给这个dep,而有模板对该属性进行绑定时,会调用dep.addsub()用于将观察者加入到dep维护的watcher队列当中。此处体现观察者模式。
  • 充当Pub/Sub的调度中心,内部维护watcher队列,每次属性变化,setter调用dep.notify()dep接受到通知后,会调用每个watcherupdate()方法。此处体现发布订阅者模式。
class Dep {
  constructor(){
    this.subs = [];
  }
  addSub(sub){
    this.subs.push(sub);
    return this;
  }
  notify(){
    this.subs.map(sub=>sub.update());
  }
}

总的来说,完整的思路:

  • 使用Object.defineProperty()为属性添加观察者,而这里的观察者是Dep,它又充当Pub/Sub调度中心的角色
  • 编译模版,每次匹配到属性的绑定,实例化一个watcher,实例化的时候会去访问属性的值,所以会访问属性的getter,该getter再通知Dep实例将这个watcher加入到Dep维护的watcher队列中
  • 当属性变化时,首先会调用属性的settersetter会通知dep实例,dep实例接到通知后再广播给所有watcher
  • 所有watcher接到通知后,调用自己的update()方法,如果该watcher监听的属性发生了变化,就要调用相应的方法更新UI

完整代码见:https://github.com/laoqiren/Reactive/blob/master/vue.js

存在的问题