前言:这篇文章的外围是 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 指令和生命周期