该版把上一次的数据修改就更新全部页面改为了局部更新,相比于上一版的在数据绑定上不是简单的一个监听set再全部更新,具体见下文。

总体流程

仍然是根据自己理解来实现的绑定,相较于上一版的数据更新就全部刷新,这次改成了部分页面更改,总体流程大致如图:(字本来就丑,那个笔芯写更丑了,希望能看懂吧)

这里就从头介绍下怎样实现整个流程的

createApp

这里是整个Vue的入口,通过传入options参数会将里面的data,methods等挂载到Vue实例上,再通过代理,让对vm的属性访问转换为对vm.$data中属性的访问:

static createApp(options) {
//? 将data代理到vm上
const vm = new Proxy(new Vue(), {
get(target, p) {
if (Reflect.get(target, p)) {
return Reflect.get(target, p);
} else {
return target.$data[p]._isref ? target.$data[p].value : target.$data[p];
}
},
set(target, p, value) {
if (target[p]) {
Reflect.set(target, p, value);
} else if (target.$data[p]?._isref) {
Reflect.set(target.$data[p], "value", value);
} else {
Reflect.set(target.$data, p, value);
}
return true;
},
}); options.onBeforCreate?.call(vm); vm.$data = options.data.call(vm);
new Observer(vm).observeData(); //! 将data的数据转为响应式 for (const key in options.methouds) {
vm.$methouds[key] = options.methouds[key].bind(vm);
} options.onCreated?.call(vm);
return vm;
}

将data中的数据转换为响应式

这个步骤通过Observer实例中的observeData来进行,我这里通过Proxy来实现(Vue2.x中使用Object.defineProperty)。

import Dep from "./dep.js";

const dep = new Dep();

export default class Observer {
constructor(vm) {
this.vm = vm;
}
observeData() {
const data = this.vm.$data;
for (const key in data) {
data[key] = this.ref(data[key]);
}
}
// *===============↓ 将数据转换为响应式数据的方法 ↓===============* //
reactive(data) {
//? 如果对象里还有对象,递归实现响应式
for (const key in data) {
if (typeof data[key] === "object") {
data[key] = this.reactive(data[key]);
}
}
return new Proxy(data, {
get(target, p) {
window.target && dep.add(window.target);
window.target = null; //? 将watch实例保存后删除
return Reflect.get(target, p);
},
//todo 修改对象属性后修改Vnode
set(target, p, value) {
target._isref
? Reflect.set(target, "value", value)
: Reflect.set(target, p, value); dep.notify(); return true;
},
});
}
ref(data) {
//? 基本数据类型会被包装为对象再进行代理
if (typeof data != "object") {
data = {
value: data,
_isref: true,
toSting() {
return this.value;
},
};
}
return this.reactive(data);
}
}

这里在get上设置了dep.add,在第一次渲染页面的时候会读取到对应的$data中的属性,在这个时候将这个属性的位置和一个用来更新视图的回调函数打包进Watcher的实例再放入dep中储存起来,在以后数据更新时会触发set,通知dep调用储存的所有watcher实例上的update方法,update方法会比较储存的旧值来决定是否触发回调函数来更新视图。

Dep:

import { nextTick } from "./util.js";

export default class Dep {
constructor() {
this.watchers = [];
this.lock = true;
}
add(watcher) {
this.watchers.push(watcher);
}
notify() {
//? 放入微任务队列,只要触发一次notify就不再触发,在微任务里更新视图,这样所有数据都更新后再触发更新
if (this.lock) {
this.lock = false;
nextTick(() => {
this.watchers.forEach((watcher) => {
watcher.update(); //? 用watcher实例的update更新视图
});
this.lock = true;
});
}
}
}

Watcher:

import { getByPath } from "./util.js";

export default class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key; //? 代表该数据在$data哪里的字符串
this.cb = cb; //? 更新页面的回调函数
window.target = this;
//! 获得旧数据,同时触发vm[key]的get把上面一行设置watcher实例push进dep 见observer.js
this.oldValue = getByPath(vm, key);
} //? dep调用notify来调用所有的update更新视图
update() {
let newValue = getByPath(this.vm, this.key);
if (newValue === this.oldValue) return;
this.oldValue = newValue;
this.cb(newValue);
}
}

为了使用方便,这里把Watcher的实例化过程挂载到vm上,实例化Watcher并推入dep的过程全由vm.$watche完成:

class Vue {
constructor() {
this.$watch = function (key, cb) {
new Watcher(this, key, cb);
};
}
}

页面渲染

通过修改原来的第一版渲染函数,这里改为了挨个读取节点来转换,通过读取每个节点的字符串形式来把数据替换或把方法挂载:

export default function render($el, vm) {
const nodes = $el.children;
Array.prototype.forEach.call(nodes, (el) => {
if (el.children.length > 0) {
render(el, vm); //? 递归渲染子节点
} else {
renderTemplate(vm, el);
}
});
} function renderTemplate(vm, el) {
renderData(vm, el);
renderEvent(vm, el);
renderVModel(vm, el);
} //? 将{{}}里的数据渲染
function renderData(vm, el) {
const nodeText = el.textContent;
const regexp = /\{\{(\s*)(?<data>.+?)(\s*)\}\}/g;
if (regexp.test(nodeText)) {
return nodeText.replace(regexp, (...arg) => {
const groups = JSON.parse(JSON.stringify(arg.pop()));
//! 将这个数据相对于vm的位置储存进dep,每次dep收到更新时触发回调
vm.$watch(groups.data, (newValue) => {
el.textContent = newValue;
});
el.textContent = getByPath(vm, groups.data);
});
}
} ... ...

再说明一下,现在的渲染操作只在进行mount的时候会执行,当以后$data属性改变时会触发在这里设置的回调函数,通过它来修改页面。

一些其它细节的地方

在页面渲染时读取$data属性只能通过写在模板上的字符串,这里用了reduce方法来获取字符串对应的值:

export function getByPath(obj, path) {
const pathArr = path.split(".");
return pathArr.reduce((result, curr) => {
return result[curr];
}, obj);
}

nextTick函数在这里只是用了开启微任务队列的方式实现:

export function nextTick(cb, ...arg) {
Promise.resolve().then(() => {
cb(...arg);
});
}

测试

最后简单写个计数器来看看实现的所有功能,可以看到和预期的一样

代码仓库

重新手写一个Vue的更多相关文章

  1. 写一个vue组件

    写一个vue组件 我下面写的是以.vue结尾的单文件组件的写法,是基于webpack构建的项目.如果还不知道怎么用webpack构建一个vue的工程的,可以移步到vue-cli. 一个完整的vue组件 ...

  2. 写一个Vue loading 插件

    什么是vue插件? 从功能上说,插件是为Vue添加全局功能的一种机制,比如给Vue添加一个全局组件,全局指令等: 从代码结构上说,插件就是一个必须拥有install方法的对象,这个方法的接收的第一个参 ...

  3. 如何优雅的写一个Vue 的弹框

    写Vue或者是react 都会遇见弹框的问题.也尝试了多种办法来写弹框,一直都不太满意,今天特地看了一下 Element UI 的源码,模仿着写了一个简易版. 大概有一下几个问题: 1.弹框的层级问题 ...

  4. 面试题:你能写一个Vue的双向数据绑定吗?

    在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理.本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫myVue吧.结合注释,希望 ...

  5. 写一个vue的滚动条插件

    组件源码如下: vue-scroll.vue <template> <div class="vue-scroll" ref="vueScrollW&qu ...

  6. 学习如何写一个vue插件【入门篇】

    #### 疑答 1.市面上已经有那么多插件可用,为什么还要造轮子?学习.借鉴思想.应用到开发 2.能否在项目中使用?与网上插件使用相同   更新维护问题怎么解决? 自身动力,使用者反馈等 #### 准 ...

  7. 写一个简单易用可扩展vue表单验证插件(vue-validate-easy)

    写一个vue表单验证插件(vue-validate-easy) 需求 目标:简单易用可扩展 如何简单 开发者要做的 写了一个表单,指定一个name,指定其验证规则. 调用提交表单方法,可以获取验证成功 ...

  8. 剖析手写Vue,你也可以手写一个MVVM框架

    剖析手写Vue,你也可以手写一个MVVM框架# 邮箱:563995050@qq.com github: https://github.com/xiaoqiuxiong 作者:肖秋雄(eddy) 温馨提 ...

  9. Java基础-继承-编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight。小车类Car是Vehicle的子类,其中包含的属性有载人数 loader。卡车类Truck是Car类的子类,其中包含的属性有载重量payload。每个 类都有构造方法和输出相关数据的方法。最后,写一个测试类来测试这些类的功 能。

    #29.编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight.小车类Car是Vehicle的子类,其中包含的属性有载人数 loader.卡车类T ...

随机推荐

  1. Spring Aop的执行顺序

    Spring Aop的执行顺序 首先回忆一下 AOP 的常用注解 @Before:前置通知:目标方法之前执行 @After:后置通知:目标方法之后执行 @AfterReturning:返回后通知:执行 ...

  2. 【Android编程】android平台的MITM瑞士军刀_cSploit源码解析及中间人攻击复现

    /文章作者:Kali_MG1937 作者博客ID:ALDYS4 QQ:3496925334 未经允许,禁止转载/ 何为MITM欺骗,顾名思义,中间人攻击的含义即为在局域网中充当数据包交换中间人的角色 ...

  3. 聊聊IOC中依赖注入那些事 (Dependency inject)

    What is Dependency injection 依赖注入定义为组件之间依赖关系由容器在运行期决定,形象的说即由容器动态的将某个依赖关系注入到组件之中在面向对象编程中,我们经常处理的问题就是解 ...

  4. Centos7搭建内网DNS服务器

      一.配置阿里云yum源 执行脚本配置阿里云的yum源,已配置yum源的可以忽略 #!/bin/bash # ******************************************** ...

  5. Android Studio使用Gradle引入第三方库文件

    原文链接:https://blog.csdn.net/qiutiandepaomo/article/details/81538937 使用AndroidStudio开发Android应用的时候,会经常 ...

  6. 32、sed命令详解

    32.1.sed介绍: 1.sed(sed软件常称做)是流编辑器,是操作.过滤.和转换文本内容的工具: 2.sed的模式空间和保持空间介绍: (1)模式空间:sed处理文本内容行的一个临时缓冲区,模式 ...

  7. python之struct详解

    python之struct详解 2018-05-23 18:20:29 醉小义 阅读数 20115更多 分类专栏: python   版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议 ...

  8. Docker:Centos7更新yum源下载docker

    前言: Docker 要求 CentOS 系统(6.5及以上)的内核版本高于 3.10 ,查看本页面的前提条件来验证你的CentOS 版本是否支持 Docker . 通过 uname -r 命令查看你 ...

  9. [心得体会]jvm

    1. jvm基本架构图 橙色: 线程共享的, gc主要的场所 灰色: 线程不共享 2. 类加载器 启动类加载器(c++加载器) 扩展类加载器(java应用程序加载器) 应用加载器(加载classpat ...

  10. redis广播/订阅模式演示

    参考博客 http://www.pianshen.com/article/7183315879/ 1.首先在本地启动redis服务 2.启动4个客户端 redis-cli 3.将其中三个客户端设置监听 ...