概述
最近在学习如何实现一个简版的Vue(MVVM)
的课程,为了理解课程内容,将相关的学习内容进行梳理记录,便于以后查阅。
前置知识
MVVM
MVVM(Model-View-ViewModel)
就是在MVC
的基础上把业务处理的逻辑分离到ViewModel
层中。
MVVM分别指的是
-
M
:
Model
层,表示请求的原始数据 -
V
View
层,负责视图的展示,由ViewModel
层控制 -
VM
ViewModel
层,负责业务处理和数据转化
MVVM的原理
MVVM
的作用就是API
请求完数据(从数据库中查询数据),接着将请求的数据解析成Model
,然后在ViewModel
层中将Model
层中的数据转化成能够直接在视图层中使用的数据,最后将转化的数据交付给View
层,最终将数据渲染到页面,呈现给用户查看;
MVVM vs MVC
Vue是什么?
Vue.js
是一个渐进式的JavaScript
库。
Vue的设计思想
核心思想:
- 数据驱动
- 组件系统
数据驱动
Vue.js
是一个MVVM
框架
MVVM框架的三要素
数据响应式原理
数据响应式即是数据的变化能够在视图(页面)中体现,即数据(变量)的变化会引起页面中所有放置了该数据的地方发生更新;
官网说明:
当你把一个普通的 JavaScript 对象传入 Vue 实例作为
data
选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty
把这些 property 全部转为 getter/setter。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
即是Vue2.x
中通过Object.defineProperty
方法将我们传入Vue
实例的data
选项逐一转换为具有getter
和setter
方法的可以动态修改属性值的属性。
使用Object.defineProperty
给对象添加属性的原因:
可以实时的监听到数据的变化,然后更新视图
栗子
- 给目标对象添加单个属性
使用Object.defineProperty给对象添加属性并监听数据变化然后更新DOM
使用Object.defineProperty给对象添加属性并监听数据变化然后更新DOM:
现在是几点:
- 给目标对象添加多个属性
- 对象嵌套
- 新值是对象
使用Object.defineProperty给对象添加属性
处理添加对属性和属性值嵌套对象的问题
Vue中的数据响应式
Vue的基本使用
Document
时间:{{time}}
原理分析
-
new Vue()
⾸先执⾏初始化,对data
执⾏响应化处理,这个过程发⽣在Observer
中. - 同时对模板执⾏编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发⽣在
Compile
中. - 同时定义⼀个更新函数和
Watcher
,将来对应数据变化时Watcher
会调⽤更新函数. - 由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个
Watcher
- 将来
data
中数据⼀旦发⽣变化,会⾸先找到对应的Dep
,通知所有Watcher
执⾏更新函数
涉及的类
1.CVue
- 框架的构造函数
2.Observer
- 执行数据的响应式处理(分辨数据是对象还是数据)
Compile
- 编译模板,初始化视图,收集依赖(更新函数,
watcher
创建)
Watcher
- 执行更新函数(更新
DOM
)
Dep
- 管理多个
Watcher
,批量更新
CVue类及数据响应式的实现
1.响应式处理函数的实现
/*
* @Author: xl
* @Date: 2021-04-25 10:10:43
* @LastEditTime: 2021-04-25 10:21:21
* @LastEditors: Please set LastEditors
* @Description: 数据响应式(数据劫持)函数
* @FilePath: \vue-study-demo\CVue\reactive.js
*/
/**
* @description 给指定对象添加属性
* @param {Object} target 需要添加属性的目标对象
* @param {String} key 要添加的属性名
* @param {String} val 要添加的属性对应的值
* @return {Null} 没有返回值
*/
function defineReactive(target, key, val){
// 如果对象内嵌套对象,则需要递归处理
observe(target)
Object.defineProperty(target,key,{
get() {
return val;
},
set(newVal) {
if (newVal !== val) {
// 如果属性值是对象
observe(newVal)
val = newVal;
}
}
})
}
/**
* @description 给指定的目标对象批量添加属性
* @param {Object} target 要添加属性的目标对象
*/
function observe(target) {
// 判断参数是否是对象,不是对象直接返回
if (typeof target !== 'object' && target == null) {
return target;
}
let keys = Object.keys(target);
if (keys.length) {
keys.map(key => {
defineReactive(target,key,target[key])
})
}
}
-
Observer
类,对数组进行数据劫持
/*
* @Author: xl
* @Date: 2021-04-25 10:34:50
* @LastEditTime: 2021-04-25 10:39:38
* @LastEditors: Please set LastEditors
* @Description: 数据劫持 -- 处理数组
* @FilePath: \vue-study-demo\CVue\Observe.js
*/
class Observer {
constructor(value){
this.value = value;
if (Array.isArray(value)) {
this.walk(value)
}
}
//对象的响应式处理
walk(target) {
Object.keys(target).map(key => {
defineReactive(target, key, target[key])
})
}
}
-
CVue
类的实现--以及初始化时对data选项进行响应式处理
// 定义一个CVue类
class CVue {
constructor(options){
// 缓存options选项
this.$options = options;
// 缓存data选项
this.$data = options.data;
// 数据劫持 -- 初始化(new CVue)的时候对 data数据进行响应式处理
observe(this.$data)
}
}
- 数据代理
为了直接能访问数据,即是不通过this.$data.xxx
的方式访问属性,而是通过this.xxx
的方式访问属性,将this.$data
中的数据全部加入到CVue
的实例上,实现如下
/*
* @Author:xl
* @Date: 2021-04-25 10:45:36
* @LastEditTime: 2021-04-25 10:50:53
* @LastEditors: Please set LastEditors
* @Description: 数据代理--将data选项中的数据全部加入到CVue的实例上
* @FilePath: \vue-study-demo\CVue\proxy.js
*/
/**
* @description 数据代理--将data选项中的数据全部加入到CVue的实例上
*
* @param {Object} vm CVue的实例对象
*/
function proxy(vm) {
// vm存在
if (Object.keys(vm) && Object.keys(vm).length) {
let {$data} = vm;
let keys = Object.keys($data);
if (keys.length) {
keys.map(key => {
// 将数据加入vm上也要保证数据是响应式的
Object.defineProperty(vm,key,{
get() {
return $data[key]
},
set(newVal) {
if (newVal != $data[key]) {
$data[key] = newVal;
}
}
})
})
}
}
}
在CVue
中调用该函数
// 定义一个CVue类
class CVue {
constructor(options){
// 缓存options选项
this.$options = options;
// 缓存data选项
this.$data = options.data;
// 数据劫持 -- 初始化(new CVue)的时候对 data数据进行响应式处理
observe(this.$data);
// 数据代理:把data代理到CVue的实例上
proxy(this);
}
}
模板编译处理 - Compile
编译模板中vue
模板特殊语法,初始化视图、更新视图
- 创建Complie类
/**
* @description 获取DOM元素
* @param {String} selector 选择器
* @return {Object} 返回获取的DOM元素
*/
function getEl(selector) {
return document.querySelector(selector) ? document.querySelector(selector) : null;
}
class Compile {
constructor(el,vm) {
this.vm = vm;
// 根据el获取DOM元素
this.$el = getEl(el)
}
}
在CVue
中初始化
// 定义一个CVue类
class CVue {
constructor(options){
// 缓存options选项
this.$options = options;
// 缓存data选项
this.$data = options.data;
// 数据劫持 -- 初始化(new CVue)的时候对 data数据进行响应式处理
observe(this.$data);
// 数据代理:把data代理到CVue的实例上
proxy(this);
// 初始化编译模板,更新视图
if (options.el) {
new Compile(options.el,this)
}
}
}
- 初始化视图
获取DOM
以及其所以的子节点,根据节点类型进行相关的编译
- 获取
el
对应的DOM
元素,然后获取该DOM
元素的所有子节点 - 遍历获取到的子节点,根据节点的类型执行相应的编译函数
- 如果是文本节点,并且值是插值表达式
{{xxx}}
,则将该文本节点的textContent
设置为对应的xxx
数据的具体值
4.如果是元素节点,则获取该节点的所有属性,找出对应的指令节点,针对不同的指令执行不同的编译函数 - 如果是
c-text
指令,则对应节点的textContent
属性值,取当前绑定数据对应的值;
6.如果是c-html
指令,则对应节点的innerHTML
属性值,取当前绑定数据对应的值;
/*
* @Author: xl
* @Date: 2021-04-25 10:56:49
* @LastEditTime: 2021-04-28 16:12:42
* @LastEditors: Please set LastEditors
* @Description: 模板编译
* @FilePath: \vue-study-demo\CVue\Compile.js
*/
/**
* @description 获取DOM元素
* @param {String} selector 选择器
* @return {Object} 返回获取的DOM元素
*/
function getEl(selector) {
return document.querySelector(selector) ? document.querySelector(selector) : null;
}
class Compile {
constructor(el, vm) {
this.$vm = vm;
// 根据el获取DOM元素
this.$el = getEl(el);
if (this.$el) {
this.compile(this.$el);
}
}
// 编译函数
compile(el) {
// 获取所有的子节点
let nodes = el.childNodes || [];
// 遍历子节点,获取节点类型
Array.from(nodes).map(node => {
let {
childNodes
} = node;
// 元素节点
if (this.isElement(node)) {
this.compileEle(node)
}
// 文本节点并且是插值表达式的形式
if (this.isInterpolation(node)) {
this.compileText(node)
}
// 存在子节点,则进行递归处理
if (childNodes && childNodes.length) {
this.compile(node)
}
})
}
// 是否是插值表达式: 形如{{xxx}}
isInterpolation(node) {
return /\{\{(.*)\}\}/.test(node.textContent);
}
// 文本节点
isText(node) {
return node.nodeType == 3;
}
// 是否是元素节点
isElement(node) {
return node.nodeType == 1;
}
// 是否是指令
isDirective(attr) {
return attr.indexOf("c-") == 0 && attr.indexOf("c-bind:") == -1;
}
// 是否是绑定的事件: c-bind:xxx | @xxx
isBind(attr) {
return attr.indexOf("c-bind:") == 0 || attr.indexOf('@') == 0;
}
// 编译元素节点
compileEle(node) {
let that = this;
// 获取节点的所有属性
let attrs = node.attributes;
// 遍历所有的属性节点
Array.from(attrs).map(attr => {
let {
name,
value
} = attr;
// 判断当前属性是否是指令:c-xxx : c-text c-html,c-model
if (this.isDirective(name)) {
// 截取指令的名称
let directName = name.substring(2);
// 执行对应的编译函数
this[directName] && this[directName](node, value)
} else if (this.isBind(name)) { // 处理绑定事件
this.event(node, name, value)
}
})
}
// 处理on:xxx | @xxx
event(node, name, eventName) {
let that = this;
let eventType = null
// c-bind:xxx
if (name.indexOf('c-bind:') == 0) {
eventType = name.split(':')[1];
} else { // @xxx
eventType = name.substring(1);
}
node.addEventListener(eventType, () => {
that.$vm.$methods[eventName] && that.$vm.$methods[eventName].call(that.$vm);
})
}
update(node, exp, dir) {
if (!node) {
return;
}
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
textUpdater(node, val) {
node.textContent = val;
}
htmlUpdater(node, val) {
node.innerHTML = val
}
// c-text
text(node, exp) {
//node.textContent = this.$vm[exp];
this.update(node, exp, 'text')
}
// c-html
html(node, exp) {
// node.innerHTML = this.$vm[exp];
this.update(node, exp, 'html')
}
// c-model
model(node, exp) {
let that = this;
node.addEventListener('input', function (e) {
that.$vm[exp] = e.target.value;
})
}
// 编译文本节点
compileText(node) {
// node.textContent = this.$vm[RegExp.$1];
// 调⽤update函数执插值⽂本赋值
this.update(node, RegExp.$1, 'text')
}
}
依赖收集
- 依赖
视图中会用到data
选项中的某个Key
,即是视图中的数据展示需要依赖于data
选项中的某个属性key
; - 依赖收集
同一个key
可能出现在视图中的多个位置,每次都需要收集出来⽤⼀个Watcher
来维护它们,即是视图中同一个key出现多少次就需要多少个Watcher
和key
建立联系
多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
- 实现思路
-
defineReactive
时为每⼀个key
创建⼀个Dep
实例; - 初始化视图时读取某个
key
,例如name1
,创建⼀个watcher1
; - 触发
name1
的getter
⽅法时,便将watcher1
添加到name1
对应的Dep
中; - 当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
- 实现类
-
Watcher
类
监听器:负责更新视图
Watcher
类的声明
/*
* @Author:xl
* @Date: 2021-04-27 10:30:47
* @LastEditTime: 2021-04-27 11:05:20
* @LastEditors: Please set LastEditors
* @Description: 监听器:根据监听的Key的值变化执行对应的更新函数去更新视图
* @FilePath: \vue-study-demo\CVue\Watcher.js
*/
class Watcher {
// vm: CVue的实例对象 key: Watcher监听的key updateFn: 更新函数
constructor(vm,key,updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
}
// 更新函数
update() {
this.updateFn.call(this.vm,this.vm[this.key])
}
}
编写更新函数,创建Watcher
/*
* @Author: xl
* @Date: 2021-04-25 10:56:49
* @LastEditTime: 2021-04-28 16:12:42
* @LastEditors: Please set LastEditors
* @Description: 模板编译
* @FilePath: \vue-study-demo\CVue\Compile.js
*/
/**
* @description 获取DOM元素
* @param {String} selector 选择器
* @return {Object} 返回获取的DOM元素
*/
function getEl(selector) {
return document.querySelector(selector) ? document.querySelector(selector) : null;
}
class Compile {
constructor(el, vm) {
this.$vm = vm;
// 根据el获取DOM元素
this.$el = getEl(el);
if (this.$el) {
this.compile(this.$el);
}
}
// 编译函数
compile(el) {
// 获取所有的子节点
let nodes = el.childNodes || [];
// 遍历子节点,获取节点类型
Array.from(nodes).map(node => {
let {
childNodes
} = node;
// 元素节点
if (this.isElement(node)) {
this.compileEle(node)
}
// 文本节点并且是插值表达式的形式
if (this.isInterpolation(node)) {
this.compileText(node)
}
// 存在子节点,则进行递归处理
if (childNodes && childNodes.length) {
this.compile(node)
}
})
}
// 是否是插值表达式: 形如{{xxx}}
isInterpolation(node) {
return /\{\{(.*)\}\}/.test(node.textContent);
}
// 文本节点
isText(node) {
return node.nodeType == 3;
}
// 是否是元素节点
isElement(node) {
return node.nodeType == 1;
}
// 是否是指令
isDirective(attr) {
return attr.indexOf("c-") == 0 && attr.indexOf("c-bind:") == -1;
}
// 是否是绑定的事件: c-bind:xxx | @xxx
isBind(attr) {
return attr.indexOf("c-bind:") == 0 || attr.indexOf('@') == 0;
}
// 编译元素节点
compileEle(node) {
let that = this;
// 获取节点的所有属性
let attrs = node.attributes;
// 遍历所有的属性节点
Array.from(attrs).map(attr => {
let {
name,
value
} = attr;
// 判断当前属性是否是指令:c-xxx : c-text c-html,c-model
if (this.isDirective(name)) {
// 截取指令的名称
let directName = name.substring(2);
// 执行对应的编译函数
this[directName] && this[directName](node, value)
} else if (this.isBind(name)) { // 处理绑定事件
this.event(node, name, value)
}
})
}
// 处理on:xxx | @xxx
event(node, name, eventName) {
let that = this;
let eventType = null
// c-bind:xxx
if (name.indexOf('c-bind:') == 0) {
eventType = name.split(':')[1];
} else { // @xxx
eventType = name.substring(1);
}
node.addEventListener(eventType, () => {
that.$vm.$methods[eventName] && that.$vm.$methods[eventName].call(that.$vm);
})
}
update(node, exp, dir) {
if (!node) {
return;
}
const fn = this[dir + 'Updater'];
fn && fn(node, this.$vm[exp]);
new Watcher(this.$vm, exp, function (val) {
fn && fn(node, val)
})
}
textUpdater(node, val) {
node.textContent = val;
}
htmlUpdater(node, val) {
node.innerHTML = val
}
// c-text
text(node, exp) {
//node.textContent = this.$vm[exp];
this.update(node, exp, 'text')
}
// c-html
html(node, exp) {
// node.innerHTML = this.$vm[exp];
this.update(node, exp, 'html')
}
// c-model
model(node, exp) {
let that = this;
node.addEventListener('input', function (e) {
that.$vm[exp] = e.target.value;
})
}
// 编译文本节点
compileText(node) {
// node.textContent = this.$vm[RegExp.$1];
// 调⽤update函数执插值⽂本赋值
this.update(node, RegExp.$1, 'text')
}
}
-
Dep
类
用于收集同一个key
的Watcher
,并监听key
值的编译,一旦key
的值有变化则通知其下管理的Watcher
进行更新视图
/*
* @Author: xl
* @Date: 2021-04-27 10:30:28
* @LastEditTime: 2021-04-27 11:17:13
* @LastEditors: Please set LastEditors
* @Description: Dep: 负责收集同一个key对应的watcher,监听key的值变化,通知其管理的Watcher执行各自的更新函数
* @FilePath: \vue-study-demo\CVue\Dep.js
*/
class Dep {
constructor() {
this.deps = [];
}
// 保存watcher
addDep(dep) {
this.deps.push(dep)
}
// 通知watcher进行更新
notify() {
this.deps.map(dep => dep.update())
}
}
创建watcher
时触发getter
class Watcher {
// vm: CVue的实例对象 key: Watcher监听的key updateFn: 更新函数
constructor(vm,key,updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// 把watcher保存在Dep的静态属性target上
Dep.target = this;
// 创建watcher时触发getter -- 即是手动获取一次key
this.vm[this.key];
Dep.target = null;
}
// 更新函数
update() {
this.updateFn.call(this.vm,this.vm[this.key])
}
}
依赖收集,创建Dep
实例
function defineReactive(target, key, val){
// 如果对象内嵌套对象,则需要递归处理
observe(val)
// 创建Dep实例
const dep = new Dep()
Object.defineProperty(target,key,{
get() {
// 保存key的Watcher
Dep.target && dep.addDep(Dep.target);
return val;
},
set(newVal) {
if (newVal !== val) {
// 如果属性值是对象
observe(newVal)
val = newVal;
// 通知watcher更新
dep.notify();
}
}
})
}
代码地址
2021_kaikeba_memos
学习资料
- 开课吧-22期全栈架构师-手写Vue
- Vue.js
- 深入响应式
- RegExp.9