github地址:https://github.com/Rawsdom/my-vue-demo.git
new MyVue({
el: '#app',
data: {
name: 'hello world!',
}
});
(PS:MyVue构造函数接收上方这些参数并保存到实例中,并将实例往下传递)
class MyVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 数据劫持 (数据响应化处理)
observe(this.$data);
// 数据代理
proxy(this, "$data");
// 模板解析 (传递 this 实例)
new Compiler(options.el, this);
}
}
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// 被Dep收集过程
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update() {
//更新函数的调用, 由于更新函数内部可能需要this实例, 所以用call方法改变一下调用 this 指向
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
function observe(obj) {
if (typeof obj !== "object" || obj === null) return;
new Observer(obj);
}
class Observer {
constructor(obj) {
this.obj = obj;
if (typeof obj === "object") {
Array.isArray(obj) ? this.walkArr(obj) : this.walk(obj);
}
}
// 对象数据响应化
walk(obj) {
Object.keys(obj).forEach((key) => {
// 这里可以理解为取出对象的键值并对obj的key做劫持
// 下方会有defineReactive()方法
defineReactive(obj, key, obj[key]);
});
}
// 数组数据响应化
walkArr(arr) {
// 覆盖原型的7个方法
arr.__proto__ = this.arrayProto;
// 这里取出的键值其实是数组的下标
const keys = Object.keys(arr);
for (let i = 0; i < keys.length; i++) {
// 以arr的下标做数据劫持
defineReactive(arr, i, arr[i])
}
}
}
// Observer的类原型保存新的数组原型方法(如:push等)用于数组响应化时覆盖数组的原型方法
const defaultProto = Array.prototype;
// 深拷贝数组的原型链方法
const arrayProto = Object.create(defaultProto);
// 对以下的方法做重写。并生成新的数组原型方法
["push", "pop", "shift", "unshift"].forEach(
method => {
arrayProto[method] = function () {
// 默认原型链上的对应的方法还是需要执行,该 push 就还是要 push
defaultProto[method].apply(this, arguments);
// 这里的notify() 会在下方 defineReactive() 时保存到该数据的原型链上从而在此处可以调用
this.notify()
};
}
);
// 保存到 Observer 原型链上
Observer.prototype.arrayProto = arrayProto
defineReactive() 方法
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 创建一个Dep和当前key 一一对应
const dep = new Dep()
// 利用原型链把dep的notify方法保存起来,数组使用push的方法就可以直接通知修改数组的值
if(Array.isArray(obj[key])){
obj[key].__proto__.notify = function () {
dep.notify()
}
}
Object.defineProperty(obj, key, {
get() {
// 依赖收集在这里
// 这里是配合 Watcher 的构造方法里所触发的访问(get)后,进行收集
// 这里触发只会在new Watcher实例的时候触发,因为它一新建就被收集,他被收集完就立即置空
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newVal) {
if (newVal !== val) {
// 如果更新数据的是object或者array则需要重新对此做劫持
observe(newVal);
val = newVal;
// 通知更新
dep.notify()
}
},
});
}
// 代理 this,$data
function proxy(vm, sourceKey) {
// this.$data key: name
Object.keys(vm[sourceKey]).forEach((key) => {
// this, name
Object.defineProperty(vm, key, {
get() {
// this.$data.name
return vm[sourceKey][key];
},
set(newVal) {
// this.$data.name = newVal
vm[sourceKey][key] = newVal;
},
});
});
}
class Compiler{
// el 是宿主元素
// vm 是 MyVue 实例
constructor(el,vm){
this.$vm = vm;
this.$el = document.querySelector(el)
if(this.$el){
// 执行编译
this.compile(this.$el)
}
}
compile(el){
// 遍历 el 树
const childNodes = el.childNodes
// 将类数组转换为数组
Array.from(childNodes).forEach(node => {
// 判断是否是元素
if(this.isElement(node)){
// 解析元素
this.compileElement(node)
}else if(this.isInter(node)){
// 解析文本
this.compileText(node)
}
// 递归子节点
if(node.childNodes && node.childNodes.length > 0){
this.compile(node)
}
})
}
// node.nodeType 1:元素 2:属性 3: 文本
isElement(node) {
return node.nodeType === 1
}
isInter(node){
// 类型是文本同时也符合插值表达式双大或号包裹 如:{{xxx}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
compileText(node){
// RegExp 是js的内置对象,$1是匹配正则表达式的匹配正确的第一个
// 由于上方判断如果通过就会立刻调用当前的编译文本的方法所以也就是正则匹配的第一个
// 如 {{ name }} 这里得出的是 '空格'+name+'空格' 所以要trim()去除前后空格
this.update(node, RegExp.$1.trim(), 'text')
}
compileElement(node){
// 节点是元素
// 遍历其属下列表
const nodeAttrs = node.attributes
Array.from(nodeAttrs).forEach(attr => {
// 规定: 指令以 my-text="xxx" 定义
const attrName = attr.name // my-text
const exp = attr.value // xxx
// 判断是否是指令
if(this.isDirective(attrName)){
// 把 my-text 处理成 text
const dir = attrName.substring(3)
// 执行指令 this.text(node, exp)
this[dir] && this[dir](node, exp)
}
// 判断是否是事件
if(this.isEvent(attrName)){
// 如 @click='onClick'
// dir: click 上方定义好的 exp: onClick
const dir = attrName.substring(1)
// 事件的处理
this.eventHandler(node, exp, dir)
}
})
}
// 指令判断
isDirective(attr){
return attr.indexOf('my-') === 0
}
// 事件判断
isEvent(dir){
return dir.indexOf('@') === 0
}
// 更新方法(这里就是new Watcher 实例的地方,这里也就是完成模板解析和数据响应化之间的关联的地方)
// update() 方法相当于一个枢纽,在解析到指令或者插值表达式就会调用相应的更新方法(替换视图上匹配的插值或指令)
// 同时生成一个 Watcher 去保存更新函数(用于之后数据变化时更新视图上的插值或指令)
update(node, exp, dir){
// 指令对应更新函数 xxxUpdater 如:textUpdater()
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 新建 Watcher 实例并保存该更新方法,一旦数据变化就会通知 dep,dep也就通知Watcher并调用下方的更新方法
new Watcher(this.$vm, exp ,function(val) {
fn && fn(node, val)
})
}
// 更新函数 (下面使用的是原生的node节点更新里的内容)
// textContent: 文本 innerHTML: 识别并解析html标签如:(你好
)value: 用于input 输入框的 value 属性
textUpdater(node, value){
node.textContent = value
}
htmlUpdater(node, value){
node.innerHTML = value
}
modelUpdater(node, value) {
node.value = value
}
// 指令解析
text(node, exp){
this.update(node, exp, 'text')
}
html(node, exp){
this.update(node, exp, 'html')
}
model(node, exp)
this.update(node, exp, 'model')
// 事件监听 这里仅仅只是对input做的v-model所以监听的是input输入事件
node.addEventListener('input', (e) =>{
// 新的赋值给数据即可
this.$vm[exp] = e.target.value
})
}
// 事件处理
eventHandler(node, exp, dir){
// 从MyVue实例中获取 methods里的方法并匹配其中的方法
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp]
// 原生的事件监听
node.addEventListener(dir, fn.bind(this.$vm))
}
}
index.js
// 创建My Vue构造函数
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 创建一个Dep和当前key 一一对应
const dep = new Dep()
// 利用原型链把dep的notify方法保存起来,数组等使用push的方法就可以直接通知修改数组的值
if(Array.isArray(obj[key])){
obj[key].__proto__.notify = function () {
dep.notify()
}
}
Object.defineProperty(obj, key, {
get() {
// 依赖收集在这里
Dep.target && dep.addDep(Dep.target)
return val;
},
set(newVal) {
if (newVal !== val) {
// 如果传入的是新object需要再经过数据劫持
observe(newVal);
val = newVal;
// 通知更新
dep.notify()
}
},
});
}
function observe(obj) {
if(typeof obj !== 'object' && obj !== null){
return
}
new Observer(obj)
}
// 代理函数,方便用户直接访问$data中的数据
function proxy(vm, sourceKey) {
Object.keys(vm[sourceKey]).forEach(key => {
Object.defineProperty(vm, key, {
get(){
return vm[sourceKey][key];
},
set(newVal) {
vm[sourceKey][key] = newVal;
}
})
})
}
class MyVue {
constructor(options) {
// 保存选项
this.$options = options
this.$data = options.data
// 响应式处理
observe(this.$data)
// 代理
proxy(this, '$data')
// 创建编译器
new Compiler(options.el, this)
}
}
// 根据对象类型决定如何做响应化
class Observer {
constructor(obj) {
this.obj = obj;
if (typeof obj === "object") {
Array.isArray(obj) ? this.walkArr(obj) : this.walk(obj);
}
}
// 对象数据响应化
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
// 数组数据响应化
walkArr(arr) {
arr.__proto__ = this.arrayProto;
const keys = Object.keys(arr);
for (let i = 0; i < keys.length; i++) {
defineReactive(arr, i, arr[i])
}
}
}
const defaultProto = Array.prototype;
const arrayProto = Object.create(defaultProto);
["push", "pop", "shift", "unshift"].forEach(
method => {
arrayProto[method] = function () {
defaultProto[method].apply(this, arguments);
this.notify()
};
}
);
Observer.prototype.arrayProto = arrayProto
// 观察者: 保存更新函数, 值发生变化调用更新函数
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// Dep.target 静态属性上设置为当前的 watcher 实例
Dep.target = this
this.vm[this.key] // 读取触发了 getter
Dep.target = null // 收集完就置空
}
update(){
this.updateFn.call(this.vm, this.vm[this.key])
}
}
// Dep: 依赖,管理某个key相关所有的 Watcher 实例
class Dep{
constructor(){
this.deps = [];
}
addDep(dep){
this.deps.push(dep);
}
notify(){
this.deps.forEach(dep => dep.update())
}
}
compile.js
// 编译器
class Compiler {
// el 是宿主元素
// vm 是 MyVue 实例
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el);
if (this.$el) {
// 执行编译
this.compile(this.$el);
}
}
compile(el) {
// 遍历 el 树
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => {
// 判断是否是元素
if (this.isElement(node)) {
this.compileElement(node);
} else if (this.isInter(node)) {
this.compileText(node);
}
// 递归子节点
if (node.childNodes && node.childNodes.length > 0) {
this.compile(node);
}
});
}
isElement(node) {
return node.nodeType === 1;
}
isInter(node) {
// 首先是文本标签, 其次内容是 {{xxx}}
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
compileText(node) {
// node.textContent = this.$vm[RegExp.$1.trim()]
this.update(node, RegExp.$1.trim(), "text");
}
compileElement(node) {
// 节点是元素
// 遍历其属下列表
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach((attr) => {
// 规定: 指令以 my-xx="yyy" 定义
const attrName = attr.name; // xx
const exp = attr.value; // yyy
if (this.isDirective(attrName)) {
const dir = attrName.substring(3);
// 执行指令
this[dir] && this[dir](node, exp);
}
// 事件处理
if (this.isEvent(attrName)) {
// @click='onClick'
const dir = attrName.substring(1);
// 事件监听
this.eventHandler(node, exp, dir);
}
});
}
isDirective(attr) {
return attr.indexOf("my-") === 0;
}
isEvent(dir) {
return dir.indexOf("@") === 0;
}
update(node, exp, dir) {
// 初始化
// 指令对应更新函数xxUpdater
const fn = this[dir + "Updater"];
fn && fn(node, this.$vm[exp]);
// 更新处理
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val);
});
}
textUpdater(node, value) {
node.textContent = value;
}
htmlUpdater(node, value) {
node.innerHTML = value;
}
modelUpdater(node, value) {
node.value = value;
}
// my-text
text(node, exp) {
// node.textContent = this.$vm[exp]
this.update(node, exp, "text");
}
// my-html
html(node, exp) {
this.update(node, exp, "html");
}
// my-model
model(node, exp) {
// update 方法只完成赋值和更新
this.update(node, exp, "model");
// 事件监听
node.addEventListener("input", (e) => {
// 新的赋值给数据即可
this.$vm[exp] = e.target.value;
});
}
eventHandler(node, exp, dir) {
const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
node.addEventListener(dir, fn.bind(this.$vm));
}
}
index.html
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<div id="app">
<p @click="add">{{ counter }}p>
<p my-text="counter">p>
<p my-html="desc">p>
<input type="text" my-model="name" />
<p>{{name}}p>
<p>{{arr}}p>
div>
<script src="./index.js">script>
<script src="./compile.js">script>
<script>
const app = new MyVue({
el: "#app",
data: {
counter: 0,
desc: "这是描述
",
name: "123",
arr: [1, 3, 4, 5],
},
methods: {
add() {
this.counter++;
},
},
});
setTimeout(() => {
app.arr.push(9);
}, 1000);
script>
body>
html>
个人的一个学习经历,希望能帮到你,如果有大佬看出有优化的点可以指出,可以相互学习
ps:这个仅仅只是简易的Vue只为了更好的读源码,会有一些与源码不一样的