vue原理图
编译前的知识准备
- 节点类型 – nodeType。document可以看成是一棵节点树,它是由一个个的节点组成的。
<p key='index'>text</p
//每一个标签都属于元素节点,
//包裹在标签里面的是文本节点,
//开始标签里面的健值对是属性节点
//所有以上的都统称为节点,这些节点都有同一个属性nodeType
//元素的nodeType为1,文本的nodeType为3
- fragment节点。我们知道,操作document是非常耗资源且昂贵的操作,如果文档中有大量的节点需要增删改查的话,那性能就会很低。这时fragment节点为我们提供了解决思路,fragment是内存中的元素节点,我们可以把document中节点转移到内存中来进行各种操作,最后把得到的结果一次性的再转移回document中,这样就避免了大量的直接的document操作。
- 正则表达式捕获组的概念,假设我们需要匹配{{name}}并将name给提取出来,我们的正则表达式应该怎么写呢?
const reg = /\{\{.*\}\}/; //这样我们就可以匹配{{name}}了,但要提取出name,还需要一些工作
const reg = /\{\{(.*)\}\}/; //这时我们通过在".*"外层加一对小括号就可以提取到name.
//这一对小括号就是一个捕获组,可以帮助我们在匹配字符串的同时捕获字符串中更精细的信息,
//并保存到RegExp.$n中,n=1,2,3...
- 类数组转数组,有两个方法,假设现有一个类数组likeArray。
//ES5:
const realArray = Array.prototype.slice.call(likeArray);
//ES6:
const realArray = Array.from(likeArray);
实现编译
下面我们来对模板中的大括号表达式以及指令进行编译工作。
//ES6实现
class Vue {
constructor(options) {
//缓存配置项
this.$el = options.el || document.body;
this.$options = options;
const data = this.$data = options.data;
//劫持数据
this.hijackData(data);
//编译模版
this.compile(this.$el);
}
compile(el) {
//找到vue管理的区域
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
if (this.$el) {
//将this.$el中的子节点转移到内存中,document --> 内存
this.$fragment = this.node2Fragment(this.$el);
//编译
this.compileElements(this.$fragment);
//内存 --> document
this.$el.appendChild(this.$fragment);
}
}
//将文档节点转移到fragment(内存)中
node2Fragment(node) {
let child = null;
let fragment = document.createDocumentFragment();
while (child = node.firstChild) {
//节点有且只有一个父节点,
//所以是转移,不是复制,不会出现两份,
//相当于是一个水缸的水舀到另一个水缸里,fragemnt满了,this.$el空了
fragment.appendChild(child);
}
return fragment;
}
compileElements(vNode) {
let text = '';
//正则表达式,用于匹配大括号表达式
const reg = /\{\{(.*)\}\}/;
//转为真数组并遍历所有节点
Array.from(vNode.childNodes).forEach(node => {
text = node.textContent;
if (this.isElementNode(node) { //元素节点,解析所有的指令属性
let exp = ''; //表达式
let dir = ''; //指令
let attrName = '';
let attrs = node.attributes;
//取出所有属性,转为数组并遍历
Array.from(attrs).forEach(attr => {
exp = attr.value;
attrName = attr.name;
// 普通指令v-text,v-html,v-model等
if (this.isDirective(attrName) {
dir = attrName.substring(2);
this.update(node, exp, dir);
node.removeAttribute(attrName);
//事件指令@click等
} else if (this.isEvent(attrName) {
dir = attrName.substring(1);
this.eventHandler(node, exp, dir);
node.removeAttribute(attrName);
}
});
} else if (this.isTextNode(node) && reg.test(text)) { //文本节点,大括号表达式
this.update(node, RegExp.$1.trim(), 'text');
}
//递归遍历所有层次的节点
if (node.hasChildNodes()) {
this.compileElements(node);
}
});
}
//事件处理器
eventHandler(node, exp, eType) {
const cb = this.$options.methods && this.$options.methods[exp];
cb && node.addEventListener(eType, cb.bind(this));
}
//更新视图,依赖的统一入口,每个引用过data中数据的依赖都会进来这里
update(node, exp, dir) {
//拿到相对应的更新函数
const fn = this[dir+'Updater'];
//初始化更新显示,这里需要指定调用的实例this,即vue实例
fn && fn.call(this, node, exp);
//变化更新显示,下一节内容
//。。。。
}
textUpdater(vm, node, exp) {
node.textContent = vm[exp];
}
htmlUpdater(vm, node, exp) {
node.innerHTML = vm[exp];
}
modelUpdater(vm, node, exp) {
node.value = vm[exp];
//添加input事件监听
//v-model双向数据绑定的其中一个方向,即:视图 --> 内存
node.addEventListener('input', e => {
vm[exp] = e.target.value;
});
}
//判断是否为元素节点
isElementNode(node) {
return (node.nodeType === 1);
}
//判断是否为文本节点
isTextNode(node) {
return (node.nodeType === 3);
}
hijackData(data) {
if (!data || typeof data !== 'object') { //不存在或者不为对象,返回(递归退出条件)
return;
}
//拿到data中所有可枚举的属性
Object.keys(data).forEach(key => {
//递归遍历所有层次
this.hijackData(data[key]);
//这里可以把第一节([数据代理](https://blog.csdn.net/huolinianyu/article/details/100111065))
//中的数据代理放到下面,复用相同的处理逻辑
//this.proxyData(key);
//创建观察者
new Observer(data, key);
});
}
}
//观察者实现
class Observer {
constructor(data, key) {
//对数据进行观察
this.observe(data, key, data[key]);
}
observe(data, key, val) {
//data中每一个数据对应一个Dep容器,存放所有依赖于该数据的依赖项
const dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true, //可枚举
configurable: false,//不能再配置
get() {
if (Dep.target) {//Dep.target存放具体的依赖,在编译阶段检测到依赖后被赋值
dep.addDep(Dep.target); //依赖收集
}
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify(); //当数据发生变化时,通知所有的依赖进行更新显示
}
});
}
}
//Dep容器,data中的每个数据会对应一个,用来收集并存储依赖
class Dep {
constructor() {
this.deps = []; //所有的依赖将存放在该数组中
}
//收集依赖
addDep(dep) {
this.deps.push(dep);
}
//通知更新
notify() {
this.deps.forEach(dep => {
dep.updata();
});
}
}
下一节:更新显示
上一节:数据观察Observer