前言:这篇文章的核心是Vue2的指令和声明周期的架构,其立足于模板引擎虚拟Dom与Diff算法数据响应式原理抽象语法树之上,这就像要盖一座房子,所需要的砖,水泥,钢筋都准备好了,那么接下来就是怎么把它们组合起来发挥各自的作用让这个房子的架子先搭起来呢?

Vue类的创建

在此简化模拟自己手写一个Vue类:

Vue.js
export default class Vue {
    constructor(options) {
        // todo
    }
}

这个Vue构造函数中传入的对象就是我们日常实例化Vue类时的el,data,methods等等

index.html
var vm = new Vue({
  el: '#app',
  data: {
    a: 1,
    b: {
      c: 2
    }
  },
  watch: {
    a() {
      console.log('a改变')
    }
  }
});

这时候就把options存为$options,目的是为了让用户也可以使用,然后让数据变为响应式的,此后就要进行模板编译,调用Compile类,把options中的el和上下文(vue这个实例)传过去:

Vue.js
export default class Vue {
    constructor(options) {
        // 把参数options对象存为$options
        this.$options = options || {}
        // 数据
        this._data = options.data || undefined;
        // 数据变为响应式的,这里就是生命周期
        ...
        // 模板编译
        new Compile(options.el, this)
    }
}

Compile类-模板编译

Compile.js
export default class Compile {
    constructor(el, vue) {
        // vue实例
        this.$vue = vue;
        // 挂载点
        this.$el = document.querySelector(el);
        // 如果用户传入了挂载点
        if (this.$el) {
            // 调用函数,让节点变为fragment(片段),类似于mustache种的tokens。实际上用的是AST,这里就是轻量级的,fragment
            let $fragment = this.node2Fragment(this.$el);
            // 编译
            this.compile($fragment)
            // 替换好的内容要上树
            this.$el.appendChild($fragment)
        }
    }
}

fragment(片段)的生成

在Compile类中,创建node2Fragment()方法,目的是让所有dom节点,都进入fragment

Compile.js
node2Fragment (el) {
  // createDocumentFragment 创建虚拟节点对象
  var fragment = document.createDocumentFragment();
  // console.log(fragment)
  var child;
  // 让所有dom节点,都进入fragment
  while (child = el.firstChild) {
    fragment.appendChild(child)
  }
  return fragment
}

compile()方法编译fragment

在Compile类的compile()方法中,对上一步的fragment片段循环遍历每一个元素,在Vue源码中是处理虚拟节点和diff,此处采用fragment简略表明要意

Compile.js
compile (el) {
  console.log(el)
  // 得到子元素
  var childNodes = el.childNodes;
  // 上下文
  var self = this;
  var reg = /\{\{(.*)\}\}/;
  childNodes.forEach(node => {
    let text = node.textContent;
    if (node.nodeType == 1) {
      self.compileElement(node)
    } else if (node.nodeType == 3 && reg.test(text)) {
      let name = text.match(reg)[1]
      self.compileText(node, name)
    }
  })
}

根据节点的类型,对节点作不同处理,节点类型为1,说明是标签,那就调用compileElement()方法~

compileElement()方法分析指令

对这个标签作进一步分析,看标签上是否有属性,再看属性列表中有没有vue的指令。

Compile.js
compileElement (node) {
  // console.log('node', node)
  // 这里的方便之处在于不是将html结构看做字符串,而是真正的属性列表
  var nodeAttrs = node.attributes;
  // console.log('nodeAttrs', nodeAttrs)
  var self = this;
  // 类数组对象变为数组
  Array.prototype.slice.call(nodeAttrs).forEach(attr => {
    // 这里就分析指令
    var attrName = attr.name;
    var value = attr.value;
    // 指令都是v-开头的
    var dir = attrName.substring(2);
    // 看看是不是指令
    if (attrName.indexOf('v-') == 0) {
      // v-开头的就是指令
      if (dir == 'model') {
        new Watcher(self.$vue, value, value => {
          node.value = value
        });
        var v = self.getVueVal(self.$vue, value);
        node.value = v;
        node.addEventListener('input', e => {
          var newVal = e.target.value;
          self.setVueVal(self.$vue, value, newVal)
          v = newVal;
        })
    } else if (dir == 'for') {

   } else if (dir == 'if') {
     console.log('if指令')
   }
  }
 })
}

在上文的v-model指令处理中,对带着这个指令的载体input进行监听,回到定义Vue类,必须使Vue类构造函数中的数据处理为响应式的

初始数据的响应式和watch

Vue.js
export default class Vue {
    constructor(options) {
        ...
        observe(this._data)
        // _initData()方法就是让默认数据变为响应式的,这里就是生命周期
        this._initData();
        // this._initComputed(); // 计算后的数据
        // 模板编译
        new Compile(options.el, this)
        // options.created() // Vue主动调用用户传进来的生命周期函数
    }
    _initData() {
        var self = this;
        Object.keys(this._data).forEach(key => {
            Object.defineProperty(self, key,  {
                get() {
                    return self._data[key];
                },
                set(newVal) {
                    self._data[key] = newVal;
                }
            })
        })
    }
}

如果在Vue的实例中,监听了data中某一个属性,那么在Vue的构造函数中,必然也需要初始化watch

Vue.js
_initWatch() {
  var self = this;
  var watch = this.$options.watch;
  Object.keys(watch).forEach(key => {
    new Watcher(self, key, watch[key])
  })
}

此时打印Vue的实例vm,会发现data中的a属性,有一个Observer类的__ob__属性,而__ob__里边有一个dep,它就是依赖收集系统,如图:

此时Vue的响应式部分大致模拟编写完毕,但是fragment还没有上树,所以下面就对compile()中nodeType为3,说明是文本,进行处理。

Compile.js
compileText (node, name) {
  console.log('name', name)
  node.textContent = this.getVueVal(this.$vue, name);
  new Watcher(this.$vue, name, value => {
    node.textContent = value;
  });
}
getVueVal (vue, exp) {
  var val = vue;
  exp = exp.split('.');
  exp.forEach(k => {
    val = val[k];
  })
  return val;
}
setVueVal (vue, exp, value) {
  var val = vue;
  exp = exp.split('.');
  exp.forEach((k, i) => {
    if (i < exp.length - 1) {
      val = val[k];
    } else {
      val[k] = value
    }
  })
  return val;
}

在这一步,实现了“{{”,“}}”的识别,并对{{}}中的数据进行watcher监听,而且对于复杂结构如{{a.b.c}}这种结构进行了解构。


通过模拟Vue类的实现,了解了当一个对象调用了Vue类进行实例化时是如何工作的:

  • 我们耳熟能详的new Vue()里传入的el,data,created,watch到Vue的构造函数中,首先将data数据设置为响应式数据,然后对el进行模板编译,最后调用实例化时传入的Vue各生命周期函数如created、mounted等。
  • 在模板编译阶段,递归地让所有节点转换为片段(fragment),再对这些fragment进行编译,之后上树。
  • fragment编译时,对子元素进行了分类处理,如果是文本,且是在{{}}内的文本,就读取vue的data中对应文本的值替换到该文本处,多层嵌套利用getVueVal()方法对其解构;若是标签,就获取该标签的所有属性,逐个分析,匹配v-开头的指令,本例中实现了v-model指令的工作原理。
    完整代码:Vue2指令和生命周期
03-05 21:25