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

Vue类的创立

在此简化模仿本人手写一个Vue类:

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

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

index.htmlvar vm = new Vue({  el: '#app',  data: {    a: 1,    b: {      c: 2    }  },  watch: {    a() {      console.log('a扭转')    }  }});

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

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

Compile类-模板编译

Compile.jsexport 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.jsnode2Fragment (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.jscompile (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.jscompileElement (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.jsexport 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.jscompileText (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指令和生命周期