vue源码解析pdf_MVVM (Vue)源码解析

MVVM 框架模型的核心是双向绑定,即当 viewModel 中 data 数据发生改变如何改变 view 中的数据,当 view 中数据发生改变后如何改变 viewModel 中的 data 数据:

2e8c983b68969b2276bef65afbbf2eff.png

上图中表明了通过 input 事件来监听 view 中数据的改变,再在 JS 中通过定义事件来改变 data 中对应数据,通过 Object.defineProperty API 来监听 data 数据的改变,然后在这个 API 的 setter 函数来通知 view 中的数据去作改变。

Object.difineProperty API 主要有两个主要的作用:

1、可以让一个对象访问另一个对象的属性,即数据代理,将一个对象的属性加入到另一个对象中;

2、可以改变一个对象的原有属性,当属性值被改变时会自动触发 setter 函数,我们可以在 setter 函数中作些操作,当获得到某个属性值时,会自动触发 getter 函数,我们也可以在 getter 函数中作些操作。

要想看懂 MVVM 源码,一定要将这个 API 搞明白:Object.defineProperty()

cf02de6a7c1df6b1f4b140288b1258bf.png

基本流程是这样的:

首先,我们需要为每一个 data 属性都配置一个 observer 监听器,用来监听某个属性值是否发生了改变;

然后,我们要为每一个 data 属性都匹配一个 watcher 观察者(每一个属性可以有多个观察者,但至少要有一个),当某个属性值发生变化时,让页面中的对应数据发生改变;

当 data 中某个属性发生变化后,它的 observer 会通知它的所有观察者(所有的观察者会在一个观察者列表(Dep)中来维护),让每个观察者都执行 一个 update 函数,这是一个回调函数,对页面做出更新;

还需要一个编译器,用来解析模版指令,将模版中的变量替换成对应的值。在初始化渲染页面,为每个 data 属性添加一个观察者就是在这里添加的,一旦 data 中某个属性值发生改变,就会触发 watcher 的回调函数,此回调函数与编译器初始化页面是同一个函数。

index.html

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>源码分析</title>
    <style media="screen">
      #app {
        text-align: center;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <h2>{{title}}</h2>
      <input v-model="name">
      <h1>{{name}}</h1>
      <button v-on:click="clickMe">click me!</button>
    </div>

    <script src="./observer.js"></script>
    <script src="./watcher.js"></script>
    <script src="./compile.js"></script>
    <script src="./index.js"></script>
    <script type="text/javascript">
      new Vue({
        el: '#app',
        data: {
          title: 'hello world',
          name: 'canfoo'
        },
        methods: {
          clickMe() {
            this.title = 'hello world'
          }
        },
        mounted() {
          window.setTimeout(() => {
            this.title = '你好'
          }, 1000)
        }
      });
    </script>
  </body>
</html>

index.js

//定义一个 vue 构造函数
function Vue(options) {
  //在方法中使用 this,要注意 this 的指向
  let self = this;
  this.data = options.data;
  this.methods = options.methods;
  //将 data 中每个属性拿出来放入数据代理函数中
  Object.keys(this.data).forEach((key) => {
    self.proxyKeys(key);
  });
  //给 data 中每个属性匹配一个 observer 监听器
  observe(this.data);
  //在这里初始化页面并且为每个属性匹配一个 watcher 观察者
  new Compile(options.el, this);
  //由于在 index.html 中使用了 mounted 函数,需要在实例化时,执行 mounted
  options.mounted.call(this);
}

Vue.prototype = {
  proxyKeys(key) {
    let self = this;
    //这个 API 可以让 vm 实例访问 options 对象中 data 中的所有属性,如此 this.data.name 
    可以访问到,this.name 也可以访问到
    Object.defineProperty(self, key, {
      enumerable: false,
      configurable: true,
      get() {   //当获取到这个属性值时,这个函数会自动执行
        return self.data[key];
      },
      set(newVal) {   //当更新这个属性值时,这个函数会自动执行
        self.data[key] = newVal
      }
    });
  }
};

observer.js

function Observer(data) {
  this.data = data;
  this.walk(data);
}

Observer.prototype = {
  walk(data) {
    let self = this;
    Object.keys(data).forEach((key) => {
      self.defineReactive(data, key, data[key]);
    });
  },
  defineReactive(data, key, val) {
    let dep = new Dep();
    //因为 observe 方法中已经有判断,可以直接调用,用来处理属性值是对象的情况,无论是对象还是
    数组,只要某一个属性值或者元素发生改变,都要匹配一个监听器来监听数据的变化,所以使用递归
    let childObj = observe(val);
    //为每一个属性值都绑定这个 API,如此可以在取值和更新值时作些操作
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        //将某个属性的所有观察者统统加入到一个观察者列表中
        if(Dep.target) {
          dep.addSub(Dep.target);
        }
        return val;
      },
      set(newVal) {
        if(val === newVal) return

        val = newVal;
        //如果新值是一个对象,要重新给每个属性或者元素一个监听器
        observe(newVal);
        //让每一个观察者执行 update 函数
        dep.notify();
      }
    });
  },
};

function observe(value) {
  if(!value || typeof value !== 'object') return

  return new Observer(value);
}

function Dep() {
  this.subs = [];
}

Dep.prototype = {
  addSub(sub) {
    this.subs.push(sub);
  },
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
};
//在 Dep 上定义了一个 target 属性
Dep.target = null;

watcher.js

function Watcher(vm, exp, cb) {
  this.vm = vm;
  this.exp = exp;
  this.cb = cb;
  //执行 get 函数,让自己(指的是观察者)缓存到 Dep.target 中,执行监听器中的 getter 函数,
  将自己添加到观察者列表中,并且将值保存起来,好跟新值作对比
  this.value = this.get();
}

Watcher.prototype = {
  update() {
    this.run();
  },
  run() {
    //在监听器中,属性值发生改变,就会执行 update 函数,也就是执行 run 函数,此时获取到的值是
    新值
    let newVal = this.vm.data[this.exp];
    if(this.value !== newVal) {
      this.value = newVal;
      //执行这个回调函数,这个回调函数在 compile.js 中被定义,将会改变页面数据
      this.cb.call(this, newVal);
    }
  },
  get() {
    Dep.target = this;  //缓存自己
    let value = this.vm.data[this.exp];  //只要取值就会自动触发监听器的 getter 函数
    Dep.target = null;  //释放自己,已经添加到观察者列表中之后就不用再重复添加了
    return value;
  }
}

compile.js

由于要遍历每一个节点,然后作相应的操作,为了尽可能避免重排和重绘,使用了文档碎片,在文档碎片中作相应的操作,然后将其一次性插入到 DOM 结构中。

function Compile(el, vm) {
  this.vm = vm;
  this.el = document.querySelector(el);
  this.fragment = null;
  this.init();
}

Compile.prototype = {
  init() {
    if(this.el) {
      this.fragment = this.nodeToFragment(this.el);
      this.compileElement(this.fragment);
      this.el.appendChild(this.fragment);
    }else {
      console.log('Dom 元素不存在');
    }
  },
  //将模版中所有子节点放入到文档碎片中
  nodeToFragment(el) { 
    let fragment = document.createDocumentFragment(),
      child = el.firstChild;
    //将 DOM 结构中的节点放入到文档碎片中,这个节点真的会在 DOM 结构中消失,所以反复使用
    firstChild 即可
    while(child) {
      fragment.appendChild(child);
      child = el.firstChild;
    }
    return fragment;
  },
  compileElement(el) {
    let childNodes = el.childNodes,
      self = this;
      //childNodes 是一个类数组,这种方式可以使类数组转变成真正的数组
      [].slice.call(childNodes).forEach((node) => {
        let reg = /{{(.*)}}/,
          text = node.textContent;
        //当遇到元素节点时,就做出相应的处理
        if(self.isElementNode(node)) {
          self.compile(node);
        }
        //当遇到文本节点时,并且文本节点中有双花括号表达式时,就做出相应的处理
        if(self.isTextNode(node) && reg.test(text)){
          //reg.exec() 函数的使用是为了获取到双花括号中间的字符串,即变量名,也可以使用 reg.match(),
          但在全局正则对象中只能使用 exec()。
          self.compileText(node, reg.exec(text)[1]);
        }

        //当子节点有子节点时,这个递归很重要,即便是文本节点,也会被编译,
        这就是为什么元素节点中的文本节点可以被处理到的原因
        if(node.childNodes && node.childNodes.length) {
          self.compileElement(node);
        }
      });
  },
  compile(node) {
    //左边的表达式返回一个对象,并且有 length 属性,且其他索引不为负数,因此是一个类数组
    let nodeAttrs = node.attributes,
      self = this;
    //类数组借用数组的 forEach 方法
    Array.prototype.forEach.call(nodeAttrs, (attr) => {
      let attrName = attr.name;
      //如果是 v- 开头的,就是一个指令
      if(self.isDirective(attrName)) {
        let exp = attr.value,
          dir = attrName.substring(2); //将 v- 去掉
        if(self.isEventDirective(dir)) { //是否为事件指令?
          self.compileEvent(node, self.vm, exp, dir);
        }else {  //如果不是事件指令,那就是 model 指令
          self.compileModel(node, self.vm, exp, dir);
        }
      }
    });
  },
  compileText(node, exp) {
    let self = this,
      initText = this.vm[exp];
    //初始化文本节点的值
    this.updateText(node, initText);
    //当 data 中的值被改变时,watcher 就会通过回调函数调用 updateText 函数,使页面数据发生改变
    new Watcher(this.vm, exp, function(newVal) {
      self.updateText(node, newVal);
    });
  },
  compileEvent(node, vm, exp, dir) {
    let eventType = dir.split(':')[1],  //取到事件名称
      cb = vm.methods && vm.methods[exp];  //这是一个表达式赋值方式,当两个都为真时,
                                           将右边的值给到 cb 变量
    if(eventType && cb) {
      //bind 函数跟 call 函数的差别就是不会马上执行,只会将 cb 函数中 this 指向改变
      node.addEventListener(eventType, cb.bind(vm), false);
    }
  },
  compileModel(node, vm, exp, dir) {
    let self = this,
      val = this.vm[exp];
    this.modelUpdate(node, val);
    new Watcher(this.vm, exp, function(newVal) {
      self.modelUpdate(node, newVal);
    });

    //使用 input 事件来监听页面数据的变化,在这里定义 input 事件,一旦数据改变就改变 data 中的数据
    node.addEventListener('input', function(e) {
      let newVal = e.target.value;
      if(val === newVal) return
      self.vm[exp] = newVal;
      val = newVal;
    });
  },
  updateText(node, val) {
    node.textContent = (typeof val == undefined) ? '' : val;
  },
  modelUpdate(node, val) {
    node.value = (typeof val == undefined) ? '' : val;
  },
  isDirective(attr) {
    return (attr.indexOf('v-') === 0);
  },
  isEventDirective(dir) {
    return (dir.indexOf('on') === 0);
  },
  isElementNode(node) {
    return (node.nodeType === 1);
  },
  isTextNode(node) {
    return (node.nodeType === 3);
  }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值