Vue底层学习5——插值文本编译与依赖收集
全手打原创,转载请标明出处:https://www.cnblogs.com/dreamsqin/p/15044033.html, 多谢,=。=~(如果对你有帮助的话请帮我点个赞啦)
作为一个Web前端开发人员,使用Vue框架进行项目开发已经有一阵子,掐指一算,是时候认真探索一下Vue的底层了,以前的了解比较偏理论,这一次打算在弄清基本原理的前提下自己手写Vue中的核心部分,也许这样我才敢说自己“深入理解”了Vue。上篇简述了整个编译器原理并拟定了三项编译目标,完成编译器框架搭建,在遍历Dom子节点时实现分流处理,本篇主要实现第一个目标插值文本编译和依赖收集~
插值文本编译
由上一篇提供的demo2
可以得到如下的运行结果:
但实际上我们想要展示的是各个变量对应的值,而不是变量名,所以需要编译Dom中的插值变量,并将其替换为对应的值,这里新建一个compileText
方法实现:
/*** compile.js ***/
// new Compile(el, vm)
class Compile{
constructor(el, vm) {
// 需要遍历的Dom节点
this.$el = document.querySelector(el);
// 数据缓存
this.$vm = vm;
// 编译
if (this.$el) {
// 提取指定节点中的内容,提高效率,减少Dom操作
this.$fragment = this.node2Fragment(this.$el);
// 执行编译
this.compile(this.$fragment);
// 将编译完的html追加至$el
this.$el.appendChild(this.$fragment);
}
}
// 提取指定Dom节点中的代码片段
node2Fragment(el) {
const fragment = document.createDocumentFragment();
// 将el中的所有子元素移动至fragment中
let child = null;
while(child = el.firstChild) {
fragment.appendChild(child);
}
return fragment;
}
// 编译过程
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach(node => {
// 类型判断
if (this.isElement(node)) {
// 节点
console.log('编译节点' + node.nodeName);
} else if(this.isInterpolation(node)) {
// 编译插值文本
this.compileText(node);
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
})
}
isElement(node) {
return node.nodeType === 1;
}
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
// 插值文本编译
compileText(node) {
node.textContent = this.$vm.$data[RegExp.$1];
}
}
需要特别注意的是RegExp.$1
的巧用,在做子节点分流时我们通过正则表达式对插值文本进行了匹配分组,所以在执行compileText
方法时我们可以通过RegExp.$1
获取到分组中的内容,也就是插值括号{{}}
中的变量,例如name
、location
、locationAgain
,然后通过传递的Vue实例this.$vm
获取到$data
中的属性变量值,再对节点内容进行替换操作,最终运行结果如下:
可以看到页面中的变量成功被替换,但这种方式只会初始化一次,当变量值发生改变时,页面中展示的内容是不会同步变更的,可以利用demo2
(源码可参见《Vue底层学习4——编译器框架搭建》)中created
方法的延迟赋值操作测试一下,我们在MVue
的构造函数中执行一下created
方法:
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {
// 数据缓存
this.$options = options;
this.$data = options.data;
// 数据遍历
this.observe(this.$data);
new Compile(options.el, this);
// created执行
if (options.created) {
options.created.call(this);
}
}
}
调用时使用call
绑定this
指向是为了方便在Vue实例的created
方法中轻松使用this
访问当前的Vue实例对象,例如我们日常用this.data
去访问实例的数据属性。created
执行后结果如下:
开始啦
成功打印,但name
的重新赋值并没有同步更新至页面,与上面的猜想一致。其主要原因是没有做依赖收集,也就是之前MVue.js
的 constructor
中模拟Watcher
激活getter
的部分,除此之外,我们编译器中还需要一个更新函数,之前Watcher
中update
方法都是通过console
实现视图更新的预留,这些事还是得编译器来完成。
更新函数
触发更新的操作有很多,视图中不仅仅只有插值文本,还有一系列的v-
指令或者事件,所以我们需要抽象出一个更新函数供所有的触发调用,在编译器中定义一个更新函数update
,它接收4个参数,分别表示需要更新的节点、当前的Vue实例、属性标识、触发更新的指令标识。
/*** compile.js ***/
// new Compile(el, vm)
class Compile{
// 更新函数
update(node, vm, exp, dir) {
const updateFn = this[dir + 'Updater'];
// 如果存在就执行,实现初始化
updateFn && updateFn(node, vm.$data[exp]);
}
}
updateFn
的执行只能达到初始化的作用,跟上述compileText
函数实现的效果一致,但当数据变更时想要同步更新,就需要做依赖收集,跟之前模拟的一样,我们需要创建一个Watcher
实例,接收3个参数,分别表示当前的Vue实例、属性标识、当属性变更时执行的更新回调函数:
/*** compile.js ***/
// new Compile(el, vm)
class Compile{
// 更新函数
update(node, vm, exp, dir) {
const updateFn = this[dir + 'Updater'];
// 如果存在就执行,实现初始化
updateFn && updateFn(node, vm.$data[exp]);
// 依赖收集
new Watcher(vm, exp, function(value) {
updateFn && updateFn(node, value);
});
}
}
那么对于插值文本的更新我们就需要创建一个对应的更新函数textUpdater
,并且之前用于插值文本编译的compileText
函数就需要做对应的变更:
/*** compile.js ***/
// new Compile(el, vm)
class Compile{
// 更新函数
update(node, vm, exp, dir) {
const updateFn = this[dir + 'Updater'];
// 如果存在就执行,实现初始化
updateFn && updateFn(node, vm.$data[exp]);
// 依赖收集
new Watcher(vm, exp, function(value) {
updateFn && updateFn(node, value);
});
}
// 插值文本更新
textUpdater(node, value) {
node.textContent = value;
}
// 插值文本编译
compileText(node) {
this.update(node, this.$vm, RegExp.$1, 'text');
}
}
可以看到以前我们在模拟依赖收集时,实例化Watcher
时是不会传参的,但是现在接收了3个参数,所以需要同步修改MVue
中的Watcher
类,并通过Watcher
拿到的Vue实例及属性标识激活getter
实现依赖收集:
/*** MVue.js ***/
class Watcher {
constructor(vm, exp, cb) {
// 数据缓存
this.$vm = vm;
this.$key = exp;
this.$cb = cb;
// 将当前Watcher的实例指定到Dep静态属性target
Dep.target = this;
// 激活属性的getter,添加依赖
this.$vm.$data[this.$key];
// 置空,防止重复添加
Dep.target = null;
}
update() {
// 预留视图更新
console.log('数据更新了,需要我们更新视图');
}
}
那么现在预留的视图更新就可以直接执行传入的cb
回调了,并绑定其中的this
指向为当前的Vue实例,同时将修改后的值作为参数传递进去:
/*** MVue.js ***/
class Watcher {
constructor(vm, exp, cb) {
// 数据缓存
this.$vm = vm;
this.$key = exp;
this.$cb = cb;
// 将当前Watcher的实例指定到Dep静态属性target
Dep.target = this;
// 激活属性的getter,添加依赖
this.$vm.$data[this.$key];
// 置空,防止重复添加
Dep.target = null;
}
update() {
// 视图更新
this.$cb.call(this.$vm, this.$vm.$data[this.$key]);
}
}
为了方便我们获取和设置data
中的属性,我们可以做一层代理,将data
属性挂载到Vue的实例上,实现通过Vue实例就可以直接访问或设置data
属性:
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {...}
observe(data) {
// 确定data存在并且为对象
if (!data || typeof data !== 'object') {
return;
}
// 遍历data对象
Object.keys(data).forEach(key => {
// 重写对象属性的getter和setter,实现数据的响应化
this.defineReactive(data, key, data[key]);
// 代理data中的属性到Vue实例上
this.proxyData(key);
})
}
defineReactive(obj, key, val) {...}
proxyData(key) {
Object.defineProperty(this, key, {
get: function() {
return this.$data[key];
},
set: function(newVal) {
this.$data[key] = newVal;
}
})
}
}
接下来就可以把代码中通过this.$vm.$data
访问或设置data
中属性的操作修改为this.$vm
直接进行访问和设置,修改后的代码就不贴出来了,全局搜索一下~
下面就是见证奇迹的时刻,再次运行一下demo2
,效果如下,1.5s左右后视图被同步更新了:
参考资料
1、Vue源码:https://github.com/vuejs/vue;