学习使人进步。
怀着这份心情耐心的看了一下vue的最初版本,虽然最开始也不太懂里面写的什么,到底是怎么处理的,但是通过大量的查阅资料和看视频解读,也是勉强的看的连贯。再知道vue的构建过程后就有了自己仿写的想法,顺便也能为自己的代码打下基础和强化自己的编程能力以及编程思想。要说仿写vue的代码,网上确实不少,有很多出色的代码,比如MVVM.js,在GitHub上也能搜到,本文中将实现的代码有很多也是参考了上面的代码和思想。

在看vue的时候一定要注意知道一点,它的处理过程,初始化在做了那些东西,请看下图:

20200728094012.png

基本上算是做了三步走:

  1. data 里面的所有属性值做数据代理,使其能 this.xx 能访问。
  2. 为每一个 data 数据里面的属性值创建 observe 数据劫持,并且为每一个数据添加一个 Dep()
  3. 最后就是编译模板 compile ,大致就是编译解析 {{}}v-指令 语法。
其中2, 3 两个关键步骤里面会涉及到很多巧妙的关联,在第二步的劫持数据中给每个 data 下面的数据加上 dep这里要注意,由于是先执行 observe 然后再执行 compile ,所有 dep 是比 watcher 先创建好的),然后在第三步中给每个表达式中添加一个对应的 watcher ,两者的联系是:初始化的时候当有表达式 {{}} 或者 “v-指令” 时,就会调用 observeget() 方法,将当前的绑定的所有 watcher 都添加到 dep 中;当改变 data 里面的值后,调用了 observeset() 方法,将通知 dep 下面的所有 watcher, 然后循环调用 watcher 下面的 update() 去更新页面。

先说大的框架,我们 new Vue() 时就像自己写框架一样,

function Vue() {}

var app = new Vue();

这样就算完成了vue的初始化,是不是很简答,虽然有初始化,但我们还没有传入参数,接下来我们要加参数。

function Vue(option) {
  // 获取绑定的区间 dom
  this.$el = (typeof options.el === 'string') ? document.querySelector(options.el) : (options.el || document.body);
  // 获取参数
  this.$options = options;
  var data = this.$data = options.data;
  // 数据代理(proxy data)使 data 、 method 、 computed 中的属性在此MVVM对象上可访问
  this.proxyData(options);
  // 监视,劫持数据(observe data)使新添加的数据都可以在此MVVM对象上可访问
  var observe = new Observer(data);
  // 编译模版、指令(compile template,direcitves)
  var compile = new Compiler(this.$el, this); 
}

var app = new Vue({
  el: '#app',
  data: {},
  methods: {},
  computed: {}
});

1. 数据代理(proxy data)使 data 、 method 、 computed 中的属性在此MVVM对象上可访问

看上面代码 this.proxyData(options) 这里就是上文所说的做数据代码,让数据可以 this.xx 可访问。接下来我们具体看一下 Vue 下面的 proxyData 方法。(多于的代码就不重复写了)

function Vue(option) {
  this.proxyData(options)
}
Vue.prototype = {
  proxyData: function(data) {
    var self = this;
    // 需要代理的项
    var proxy = ['data', 'methods', 'computed'];
    proxy.forEach(function (item) { 
      if(data[item]) {
        Object.keys(data[item]).forEach(function(key){
          // 如果是方法的话就不做 get(), set() 操作
          if(item === 'methods') {
            self[key] = self.$options.methods[key];
          } else {
            Object.defineProperty(self, key, {
              enumerable: true,      // 可以枚举
              configurable: false,   // 不可以重新再定义
              get: function gettter() {
                if (typeof self.$data[key] !== 'undefined') {
                  return self.$data[key];
                }
              },
              set: function setter(newVal) {
                if (self.$data.hasOwnProperty(key)) {
                  self.$data[key] = newVal;
                }
              }
            })
          }
        })
      }
    }
  }
}
// 以上就完成了对数据 data、methods、computed 的相关绑定。

2. 监视,劫持数据(observe data)使新添加的数据都可以在此MVVM对象上可访问

function Observer(data) {
  this.observe(data);
}
Observer.prototype = {
  observe: function(data) {
    var self = this
    if (!data || typeof data !== 'object') {
      return;
    }
    Object.keys(data).forEach(function(key) {
      self.observeObject(data, key, data[key]);
    })
  },
  observeObject: function(data, key, val) {
    var self = this;
    // 创建dep
    var dep = new Dep();
    Object.defineProperty(data, key, {
      enumerable: true,        // 可枚举
      configurable: false,    // 不可重新定义
      get: function gettter() {
        // 收集依赖,建立 dep 与 watcher 之间的关系
        if (_target) {
          dep.addDep(_target);
        }
        return val;
      },
      set: function setter(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 数据改变,通知所有的 watcher 更新
        dep.notify();
      }
    });
   // 递归给data数据里面的所有层次属性的数据加上 observe 劫持
   self.observe(val);
  }
}

3. 编译模版、指令(compile template,direcitves)

compile 也是很简单的操作,分三步:

  1. 取出 el 元素中所有子节点保存到一个 fragment 对象中
  2. 编译 fragment 中所有层次子节点
  3. 将编译好的 fragment 一次性添加到页面的 el 元素中

其中在第2中会复杂一点,分两种情况,需要解析文本内 大括号表达式 {{}},另外要解析标签内 “v-指令

function Compiler(el, vm) {
  this.$el = el;
  // 保存 vm 到 compiler 对象上
  this.vm = vm;
  if (this.$el) {
    // 1. 取出 el 元素中所有子节点保存到一个 fragment 对象中
    this.$fragment = this.nodeToFragment(this.$el);
    // 2. 编译 fragment 中所有层次子节点
    this.compile(this.$fragment);
    // 3. 将编译好的 fragment 一次性添加到页面的 el 元素中
    this.$el.appendChild(this.$fragment);
  }
}
Compiler.prototype = {
  // 复制 node 节点到 documentFragment
  nodeToFragment: function(node) {
    // 创建一个空的 fragment
    var fragment = document.createDocumentFragment(), child;
    // 循环将 node 中所有的子节点都转移到 fragment 中
    while (child = node.firstChild) {
      // 将子节点从原来的位置移动到 fragment 中
      fragment.appendChild(child);    
    }
    return fragment;
  },
  compile: function(node, scope, profor) {
    var self = this;
    // 是否有子节点
    if(node.childNodes && node.childNodes.length) {
      // 子节点遍历
      node.childNodes.forEach(function(child) {
        // 是否是文本节点
        if(child.nodeType === 3) {
          self.compileTextNode(child, scope, profor);
        } 
        // 是否是标签节点
        else if (child.nodeType === 1) {
          self.compileElementNode(child, scope, profor);
        }
      })
    }
  },
  // 编译文本节点
  compileTextNode: function(node, scope, profor) {
    var text = node.textContent.trim();
    if(!text) return;
    // 提取表达式的内容,去掉 {{}}
    var exp = parseTextExp(text, profor);
    scope = scope || this.vm;
    this.textHandler(node, scope, exp);
  },
  // 编译标签节点
  compileElementNode: function(node, scope, profor) {...},
  // v-text
  textHandler: function(node, scope, exp) {
    this.bindWatcher(node, scope, exp, 'text');
  },
  // 给每一个 exp 添加一个 watcher
  bindWatcher: function(node, scope, exp, dir, prop) {
    var updateFn = updater[dir];
    // 通过 Watcher, 获取 exp 表达式字段在 data 对应的值 newVal
    var watcher = new Watcher(exp, scope, function(newVal) {
      if(updateFn) {
        // 更新 fragment 的值,替换成 data 里面对应的字段
        updateFn(node, newVal, prop);
      }
    })
  }
}

var updater = {
  // 更新节点的 textContent 属性值
  text: function (node, newVal) {
    node.textContent = typeof newVal === 'undefined' ? '' : newVal;
  },
  ...
}