这是我参与8月更文挑战的第8天,活动详情查看: 8月更文挑战” juejin.cn/post/698796…
介绍
本文目的是实现一个简易版本的Vue用以学习,也是对从网课学习的总结和复习。其中内容仅为简易实现,多有不足之处,请多多交流。
内容拆分
-
数据响应式处理
- 数据拦截
- 数组和对象的区分处理
- 数据代理
-
模板编译
- 对文本的处理
- 对元素特性(即指令)的处理
-
页面渲染
- 依赖收集
- 创建watcher实例
- 触发更新
项目测试
本文目的仅为简单版Vue实现,用于学习总结。所以采用html引入js方式进行测试即可。
<!--主要代码-->
<body>
<div id="app">
<p>{{counter}}</p>
<p k-text="counter"></p>
<p k-html="desc"></p>
</div>
<script src="./kvue.js"></script>
<script>
const app = new KVue({
el: '#app',
data: {
counter: 1,
desc: '<p>村长<span style="color:red">真棒</span></p>'
},
methods: {
onclick() {
console.log(this);
}
},
})
// 暂时可不放开
/*setInterval(() => {
app.counter++
}, 1000);*/
</script>
</body>
vue实现
数据响应式处理
数据拦截
有了解过Vue框架原理的同学都知道,vue2.x中数据拦截使用的是Object.defineProperty方法,代码如下:
function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
get(){
return val;
},
set(newVal){
if(newVal != val){
val = newVal;
}
}
}}
}
属性拦截
Object.defineProperty方法的三个参数:
-
obj 要拦截的数据对象
-
key 要给数据对象添加的属性
-
{xxx} 属性描述器
- 属性描述器分为两种:存取描述器和数据描述器,都是对象,此处为存取描述器。
configurable | enumerable | value | writable | get | set | 数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 | 存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
---|
- 此处最重要的属性为get和set属性。相当于给obj设置了key属性,当访问obj.key时触发拦截操作,执行的是get方法;obj.key的值发生变化时执行的是set操作。
- 因为知道了数据被读取和变化时的地方,所以我们可以在这些地方加入一些其他的操作。
闭包写法
在此处利用了闭包的写法。通过函数作用域内的某个函数将局部变量保留出去,即为闭包。
- 局部作用域:val被保存在defineReactive函数中作用域
- 通过defineProperty的get方法将val暴露出去
闭包中的局部变量不会被释放,一直保存在内存中。所以如果对值做了修改就会发生变化。
数组和对象的区分处理
相对于普通类型数据,进行拦截时不需要过多考虑,直接返回即可。而对于数组和对象的数据,依据不同的处理方法,需要做不同的处理。
class Observer {
constructor(value) {
if (Array.isArray(value)) {
// xxx
} else {
this.walk(value);
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
function observe(obj){
if (typeof obj != 'object') {
return obj;
}
new Observer(obj);
}
对象嵌套问题
考虑到数据的值可能是嵌套对象,所以需要在数据拦截时进行递归处理。
function defineReactive(obj,key,val){
observe(obj);
Object.defineProperty(obj,key,{
get(){
return val;
},
set(newVal){
if(newVal != val){
observe(newVal);
val = newVal;
}
}
}}
}
- observe(obj):当数据不为对象类型时,observe方法直接将数据返回,继而执行下面的数据拦截操作;当数据是对象类型时,会执行Observer类的操作去区分数组和对象,分别进行处理
- observe(newVal):此处的设置主要是考虑到给数据直接赋值对象的操作,需要将新对象进行数据响应式之后赋给数据。
数据代理
这一步的操作主要是用于可以通过Vue实例直接访问data中的数据,形如:this.xxx。经过了上一步observe的操作之后,访问data中数据需要通过this.$data.xxx形式访问,比较麻烦。
function proxy(vm) {
Object.keys(vm.$data).forEach(key => {
Object.defineProperty(vm, key, {
get() {
return vm.$data[key]
},
set(v) {
vm.$data[key] = v;
}
})
})
}
模板编译
对于vue模板语法和指令等,很多人都是了解的,但是对于背后的逻辑实现,却是一知半解。对于模板的编译需要实现一个编译器来进行这些操作。
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
// 执行编译
this.compile(this.$el);
}
compile(el) {
el.childNodes.forEach(node => {
if (node.nodeType === 1) {
// element元素
// 遍历元素特性
this.compileElement(node);
// 递归
if (node.childNodes.length > 0) {
this.compile(node);
}
} else if (this.isInter(node)) {
// text文本
// 插值表达式
this.compileText(node)
}
})
}
对文本的处理
对文本的处理,首先需要辨别插值表达式,可以通过一个正则来区分。
isInter(node) {
return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent);
}
之后对于插值表达式的文本替换,基于上文正则表达式可以获得插值表达式中的变量名为RegExp.$1。
compileText(node) {
node.textContent = this.$vm[RegExp.$1];
}
对元素特性(即指令)的处理
对元素的处理,主要是对元素特性的处理。对特性attributes进行遍历。
compileElement(node) {
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
const attrName = attr.name;
const attrExp = attr.value;
if (attrName.startsWith('k-')) {
// 对指令的处理
const dir = attrName.substring(2);
this[dir] && this[dir](node, attrExp);
}
})
}
// k-text
text(node, exp) {
node.textContent = this.$vm[exp]
}
// k-html
html(node, exp) {
node.innerHTML = this.$vm[exp];
}
两步结合
将前两步结合一起。
class KVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 1.响应式
observe(this.$data);
// 1.1 代理:用户可以通过KVue实例直接访问data中数据
proxy(this)
// 2.编译:传入宿主元素el和组件实例this
new Compile(options.el, this)
}
}
页面渲染
在Vue中数据与Dep、Watcher的对应关系是:1个数据 => 1个Dep => n个Watcher
依赖收集
根据上文的对应关系,Dep中存储着多个Watcher实例,所以应该是数组形式。同时Dep应该有一个添加方法和触发按更新的方法。
class Dep {
constructor() {
// 存储所有的watcher
this.deps = [];
}
addDep(watcher) {
this.deps.push(watcher);
}
notify() {
this.deps.forEach(dep => dep.update())
}
}
创建watcher实例
Watcher中需要有一个更新方法,另外需要注意的就是触发依赖筹集的地方,在下一步综合述说。
class Watcher {
constructor(vm, key, fn) {
// fn : 更新函数
this.vm = vm;
this.key = key;
this.fn = fn;
// 触发依赖收集:读取一次key
Dep.target = this; // 保存当前实例
this.vm[this.key]; // 读取一次key,触发getter
Dep.target = null;
}
update() {
this.fn.call(this.vm, this.vm[this.key])
}
}
触发更新
这一步的实质就是对Dep和Watcher的应用。
Dep应用
首先需要确定dep实例应该被创建的位子在哪里,由 1个数据 => 1个Dep 的关系可以确定,dep创建应与数据拦截在一处。
unction defineReactive(obj, key, val) {
observe(val);
// 创建对应的Dep实例
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
console.log('get', key, val);
//建立映射关系
// Dep.target就是watcher实例,自行触发getter并将自身填入dep中
Dep.target && dep.addDep(Dep.target);
return val;
},
set(newVal) {
console.log('set', key, newVal);
if (newVal != val) {
// 新设置的值也可能是对象:解决直接给已有属性赋值对象问题
observe(newVal)
val = newVal;
dep.notify();
}
}
})
}
- const dep = new Dep()
创建和数据对应的dep实例
- dep.notify()
这里是对数据的属性值进行修改后,需要对应的dep实例通知相关Watcher实例进行更新。
- Dep.target && dep.addDep(Dep.target)
这里是需要配合Watcher中的一段代码进行解释。
// 触发依赖收集:读取一次key
Dep.target = this; // 保存当前实例
this.vm[this.key]; // 读取一次key,触发getter
Dep.target = null;
Dep.target即为Watcher实例,下一步读取数据的key时会触发数据拦截的get方法,所以在这里通过dep实例的addDep方法将Watcher实例填充进入数据对应的dep数组中。之后再讲Dep.target清空为null。
Watcher应用
Watcher和页面上应用的动态数据一一对应,所以创建Watcher实例的地方最好是在编译模板里。结合上文中模板的编译的处理步骤,我们可以建立一个统一的update方法。
update(node, exp, dir) {
// 获取实操函数
const fn = this[dir + 'Updater'];
// 初始化
fn && fn(node, this.$vm[exp]);
// 更新
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val);
});
}
// k-text
text(node, exp) {
this.update(node, exp, 'text')
}
textUpdater(node, val) {
node.textContent = val;
}
// k-html
html(node, exp) {
this.update(node, exp, 'html')
}
htmlUpdater(node, val) {
node.innerHTML = val;
}
// 将插值表达式编译为文本
compileText(node) {
this.update(node, RegExp.$1, 'text')
}
至此,可以进行项目的调试,基本可以实现效果。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!