主要分析vue作为MVVM框架的基本实现原理
数据代理
模板代理
数据绑定
不直接看vue.js源码
剖析github上某基友模仿vue实现的mvvm库:https://github.com/DMQ/mvvm
documentFragment
:文档对象(批量更新多个节点)
document:
对应显示的页面,且包含n个element,一旦更新document里面定义一个element,页面也会更新
documentFragment:
不与界面相关联,包含n个element的【容器对象】,如果更新frament里面的某一个element,页面不会更新
【常见node和nodeType】:
常量 | 值 | 说明 |
---|---|---|
Node.ELEMENT_NODE | 1 | 一个元素节点,如:
或
等 |
Node.TEXT_NODE | 3 | 文本节点 |
Node.DOCUMENT_FRAGMENT_NODE | 11 | 一个DocumentFragment节点 |
因此documentFragment节点也属于节点
【案例】:将ul下的li内容更改为’hi
<ul id='test'>
<li>helloli>
<li>helloli>
<li>helloli>
ul>
【代码实现】:
const test = document.getElementById('test')
// 创建fragment容器对象
const fragment = document.createDocumentFragment()
// 获取test中的所有节点(也包含文本节点),并插入到fragment对象中
let child
while(child = test.firstChild){
fragment.appendChild(child) // 取出test中子节点并保存到fragment中
}
// 更改这些节点的内容
Array.prototype.slice.call(fragment.childNodes).forEach(
node => {
if(node.nodeType == 1){
// 元素节点
node.textContent = 'hi'
}
}
)
// 将更改好的fragment插入到document中
test.appendChild(fragment)
数据代理:通过一个对象代理对另一个对象中属性的操作(读/写)
vue数据代理:通过vm对象代理对data对象中所有属性的操作
好处:更方便的操作data里面的数据
基本实现流程:
遍历data中的每一个属性,并通过Object.defineProperty()给vm添加与data对象的属性对应的属性描述符
所有添加属性都包含getter/setter
getter/setter内部去操作data中对应的属性数据
【预备知识】:
var reg = /\{\{.*}\}\/
匹配左右两边的大括号,以及中间是任意多个字符
var reg = /\{\{(.*)}\}\/
里面的小括号=》子匹配。既匹配了左右两边的大括号,又将里面的变量值进行匹配并保存在RegExp.$1
里面。如果有多个子匹配,则分别保存到:RegExp.$2
、RegExp.$3
、RegExp.$4
等
【大括号解析步骤】:
根据
$options
配置对象里面的el
值,来从页面查找对应的标签将该元素中所有【子元素】都【拷贝】到
fragment
容器中。此时test容器中的标签为空。注意fragment也是节点,它的所有子节点就是之前test元素中的所有子节点遍历fragment中的所有子节点,对遍历的每一个节点:
a. 判断它是【元素节点】还是【文本节点】
- 元素节点:编译元素节点内的所有指令(一般指令和事件指令)
- 文本节点且它的textContent有大括号:根据大括号里面的变量从vm的配置对象中的data获取数据,然后在赋值给文本节点
b. 判断遍历的子节点内是否还有子元素节点,如果有就再次递归调用进行编译
编译好的
fragment
重新插入到页面中
v-on:click="show"
从vm的methods方法中得到对应的事件处理函数得到指令名和指令值 (一条指令:v-text='msg'
)
从vm的配置对象的data中根据指令值获取对应的数据
根据指令名来确定需要操作元素节点的什么属性
v-text
==>textContent
属性
v-html
==>innerHtml
属性
v-class
==>class
属性
将得到的表达式的值设置到对应的属性上
移除元素的指令属性
一旦更新了data中的某个属性数据,所有界面上直接使用或间接使用了此属性的节点都会更新
换句话说:更新数据=》对应界面会改变
【数据劫持】:
当执行
vm.msg = 'a'
时,vm.set()最先察觉到,会更改data里面的值,然后data里面的set()被调用去更新对应的界面因此vm里面的set是实现数据代理的;data里面的set是实现数据绑定的
【数据劫持的准备】:
对配置对象中的data的每一个属性(每一层)都进行defineProperty()
操作。具体设置如下:
遍历每一个data属性
- 给当前属性创建Dep实例(data中的属性和dep是一一对应的关系)
- 设置属性的getter, 每次获取属性值时,对watcher进行判断进行关联
- 设置属性的setter,每次更改属性值时,通知当前属性的dep所关联到的所有watcher进行更新
// 每一个属性都对应一个新的Dep实例
var dep = new Dep()
// 当前属性值如果是对象,则再次遍历对其也进行数据劫持
var childOjb = observer(val)
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
// 获取该属性的值时系统自动调用
get: function() {
// 如果该watcher用到了该属性,且没有和当前属性的dep进行关联,则将watcher添加到dep.subs里面
if (Dep.target) {
dep.depend();
}
return val;
},
// 设置该属性的值时系统自动调用
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知订阅者。通知dep.subs里面所有的watcher进行更新数据
dep.notify();
}
});
【注意:】
上面属性的getter调用的情况:
- 在首次模板编译时:当遇到
{ {msg}}
或一般指令v-text='msg'
时,都会用到data里面的属性的值,因此这个时候会调用该属性的getter,此时每一个{ {msg}}
或一般指令都会各自对应自己的watcher。即此时Dep.target不为空(等于大括号或一般指令的watcher),这个时候在获取属性的值调用getter时,就会将watcher关联到当前的属性的dep.subs数组里面。最后要记得Dep.target置为null- 当更该数据时:比如:
this.msg='哈哈'
时,系统会自动调用该属性的setter,在setter里面会跟新属性的值,同时还会通知该属性dep所关联的所有watcher进行更新。而watcher在进行更新时,会获取vm中data里该属性值进行页面的更新。(此时获取vm中data里面的数据时也会自动调用该属性的getter,而当前watcher早已经在进行模板编译时就已经关联到该属性的dep上了。此时的Dep.target=null,因此不会再次进行关联,这次的getter操作只是单纯的获取属性的值而已)
实例创建的时机 | 个数 | 结构 | |
---|---|---|---|
Dep | 初始化时给data的每一个属性进行数据劫持时 | 与data里面的属性一一对应 | 如下: |
Watcher | 初始化时解析大括号表达式或一般指令时创建 | 模板中大括号表达式+一般指令的数量 | 如下: |
【Dep的结构】:
ids: 标识
subs: [] // n个相关的Watcher容器
【Watcher的结构】:
this.cb = cb; // 用于更新界面的回调
this.vm = vm; // vm对象
this.exp = exp; // 对应的表达式
this.depIds = {
}; // 相关的n个dep的容器对象
this.value = this.get(); // 当前表达式对应的value
【dep的初始化】:
【watcher的初始化】:
【Dep的创建时机】:初始化时给data的每一个属性进行数据劫持时
【Watcher的创建时机】:初始化时解析大括号表达式或一般指令时创建
【dep和watcher建立关系的时机】:
【要知道的】:
每个属性在进行数据劫持实现数据绑定时就已经创建了对应的dep
当编译解析大括号表达式或一般指令时
【dep和watcher的关系】?
两者是多对多的关系
name => 一个dep => n个watcher
模板中有多个表达式使用到此属性。(wacher个数 = { {}} + 一般指令)
一个表达式=>一个watcher=>n个dep
一个表达式是多层表达式时,比如:
this.user.sex = 'male'
(使用到多个属性,每一个属性分别对应一个dep)
【dep和watcher什么时候建立关系的】:初始化解析模板中的表达式时,创建watcher的内部就会建立两者之间的关系
dep先被创建(Observer数据劫持中)
watcher后被创建(解析模板的时候)
【dep和watcher在哪里建立关系的】:data中每个属性对应的get()方法中
【分析】:如果执行vm.name = 'xx'
会发生什么??
this.name = '李四'
data中的name
属性值发生改变
name
属性对应的set()
方法被调用
在set()
方法中调用dep.notify
因为Dep实例和属性值一一对应,因此每一个属性在数据劫持
Observer()
都会创建一个dep实例,且每一个dep都存放着和他相关联的所有watcher
遍历相关的所有watcher
每一个Dep实例都存有相关的watcher(数组存放)分别执行每一个watcher的run(),根据新的value和旧的value调用watcher的更新回调
调用watcher的更新回调
创建Watcher实例时会传入更新的回调作为参数。最终去调用updater中对应的指令
Watcher实例的个数 = 编译
{ {}}
的个数 + 编译【一般指令】的个数【一般指令】:
v-text
、v-html
、v-class
等
调用对应的updater
【案例】:根据配置对象中data以及页面中的代码来推断dep和watcher之间的关系
【要知道:】单向数据绑定的流程?
更改配置对象中data里面的某一个属性比如:
name = 'jack'
。则该属性的set被调用 =》 set里面的dep.notify
被调用 =》相关的watcher被调用 =》每一个watcher对应的页面数据更新
双向数据绑定时建立在单向数据绑定的基础上。首先会执行this.bind(node, vm, exp, 'model');
双向数据绑定的实现流程:
a: 在解析v-model指令时,给当前元素添加input监听
b: 当input的value发生变化时,将最新的值赋值给当前表达式所对应的data属性。data属性发生变化=》该属性的set()被调用 =》 dep.notify();通知该属性dep所关联的watcher =》 watcher更新 =》 页面更新
// v-model 双向数据绑定
model: function(node, vm, exp) {
// 即双向绑定是建立在单项绑定的基础上
this.bind(node, vm, exp, 'model');
var me = this,
// 获取该属性的值
val = this._getVMVal(vm, exp);
// 给该节点绑定input监听,一旦输入内容就会执行传入的回调
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
// 数据有更新,调用compile的_setVMVal() => 调用该属性的set()方法
// =》 调用该属性dep所关联的watcher => watcher更新 =》 对应的页面更新
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
作用:对vue应用中多个组件的共享状态进行集中式的管理(读/写)
redux和react没有直接关联,它是一个独立的库
vuex是vue的一个插件
【案例问题】:
多个视图依赖同一状态,这个时候来自不同视图的行为如果需要变更同一状态,该怎么处理呢??
【以前处理】:
将数据以及操作数据的行为都定义在父组件,然后在将数据和这些行为传递给需要的子组件(此时就有可能需要多级传递)
现在vuex就是来解决这个问题的!!
【目录结构】:
store.js
// vuex的引入(同时也要将vue引入,因为vuex依赖于vue)
import Vue from 'vue'
import Vuex from 'vuex'
// 声明使用
Vue.use(Vuex)
// 暴露核心对象模块store
const state = {
}
const mutations = {
}
const actions = {
}
const getters = {
}
export default new Vuex.Store({
state, // 状态对象,唯一的
mutations, // 包含多个更新state函数的对象
actions, // 包含多个对应事件回调函数的对象
getters // 包含多个getter计算属性的函数
})
此时store并没有和项目发生关系,需要在main.js中进行映射处理
【main.js】
import Vue from 'vue'
import App from './App.vue'
import Store from './store'
const vm = new Vue({
el: '#app',
components: {
App},
template: ' ',
Store
})
Vue.use(vm)
vuex管理的状态对象
它应该是唯一的
// state.js
export default{
xxx: initValue,
todosArr: []
}
包含多个事件回调函数的对象
通过执行commit()来触发mutation的调用,从而间接更新state
谁来触发action??
组件中:
$store.dispatch('handleIncrement',data1) // 第一个参数时 对应action名称,第二个参数:执行action所需的数据
可以包含异步代码(比如:定时器,ajax)
// actions.js
export default{
zzz({
commit,state},data1){
commit('yyy',{
data1})
},
addTodo({
commit},todoItem){
// 这里的ADD_TODO是mutation的常量标识
// 传入的数据从actions传递给mutations都会包装成一个对象的形式传过去,(和数据本身是什么类型无关)
commit(ADD_TODO,{
todoItem})
}
/**
handleIncrement ({commit}) {
// 执行commit来触发mutation的调用,间接更新state
commit('increment')
}
handleIncrementIfOdd ({commit,state}) {
if(state.num % 2 !== 0){
commit('increment')
}
**/
}
包含多个直接更新state的方法(即回调函数)的对象
谁来触发mutations里面的方法?
action中的commit(‘mutation名称’)
只能包含同步代码,不可以有异步代码
// mutations.js
import {
ADD_TODO} from './mutation-type.js'
export defalut{
yyy(state,{
data1}){
更新state的某个属性
},
// 因为要直接更新数据,因此第一个参数为state
// 因为从actions里面传到mutations里面的数据是以对象包含然在传过来,因此接收时,还需以对象的形式,且名字要和actions里面数据对象的名字一直
ADD_TODO(state,{
todoItem}){
state.todosArr.unshift(todoItem)
}
/**
increment (state) {
// 更新state中某个属性
state.num++
}
*/
}
// mutation-type.js
export const ADD_TODO = 'add_todo' // 对应添加操作,这里的add_todo大小写都可以,只是一个更新标识。
只要保证actions.js里面的
commit(ADD_TODO,{todoItem})
的第一个参数和mutations里面的有对应的函数就可以。mutation-type.js文件:修改里面的标识,同时actions.js和mutations.js里面的函数名也会改变
包含多个计算属性(get)的对象
谁来读取??
组件中:$store.getters.xxx
const getters = {
mmm (state) {
return ...
}
}
// 引入vue、vuex、actions、mutations、state、getters
Vue.use(Vuex)
export default new Vuex.Store({
actions,
mutations,
state,
getters
})
main.js
import store from './store'
new Vue({
...
store
...
})
所有用vuex管理的组件中都多了一个属性$store
,他就是一个store对象(是vuex的管理者)
【属性】:
$store{
// 注册的state对象
state: {}
// 注册的getters对象
getters:{}
}
【方法】 :$store.dispatch(actionName,data)分发调用action
【要知道的】:一旦在main.js中做了映射配置,所有的组件对象都多了一个属性$store
一般使用顺序:
组件中$store.dispatch('handleIncrement')
actions中定义:
const actions : {
handleIncrement({
commit}){
commit('increment')
}
}
mutations中定义increment
const mutations: {
increment (state) {
//更改状态
...
}
}