主要分析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_NODE1一个元素节点,如: 或等
Node.TEXT_NODE3文本节点Node.DOCUMENT_FRAGMENT_NODE11一个DocumentFragment节点因此documentFragment节点也属于节点
【案例】:将ul下的li内容更改为’hi
<ul id='test'> <li>hello</li> <li>hello</li> <li>hello</li> </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值,来从页面查找对应的标签 <div id = 'test'></div>
将该元素中所有【子元素】都【拷贝】到fragment容器中。此时test容器中的标签为空。注意fragment也是节点,它的所有子节点就是之前test元素中的所有子节点
遍历fragment中的所有子节点,对遍历的每一个节点:
a. 判断它是【元素节点】还是【文本节点】
元素节点:编译元素节点内的所有指令(一般指令和事件指令)文本节点且它的textContent有大括号:根据大括号里面的变量从vm的配置对象中的data获取数据,然后在赋值给文本节点b. 判断遍历的子节点内是否还有子元素节点,如果有就再次递归调用进行编译
编译好的fragment重新插入到页面中
得到指令名和指令值 (一条指令:v-text='msg')
从vm的配置对象的data中根据指令值获取对应的数据
根据指令名来确定需要操作元素节点的什么属性
v-text==> textContent属性
v-html==>innerHtml属性
v-class==> class属性
将得到的表达式的值设置到对应的属性上
移除元素的指令属性
一旦更新了data中的某个属性数据,所有界面上直接使用或间接使用了此属性的节点都会更新
换句话说:更新数据=》对应界面会改变
【数据劫持】:
是vue中用来实现数据绑定的一种技术(使用Observer劫持监听所有属性)基本思想:通过defineProperty()来监视,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的结构】:
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: '<App/>', 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 ... } }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) { //更改状态 ... } }