目录框架
- vue原理
- mvvm框架
- vue中的MVVM
- vue-router原理
- vuex原理
- vue-ssr
vue原理
MVC和MVVM
- M:
model模型只是负责处理数据的,它根本不知道数据到时候会拿去干啥 - V:
view 视图简单来说,就是我们在界面上看见的一切。 - C:
Controller: 控制器是应用程序中处理用户交互的部分 Controller里面把Model的数据赋值给View来显示, Controller不需要关心Model是如何拿到数据的,只管调用就行了 - VM:
viewModel: 视图模型
mvc: M到V的更新是通过C, M的数据通过C赋值给V显示,C接受用户行为,通过向C发射action将数据传给M
mvvm: 是vue数据双向绑定的实现原理,在MVVM模式中,VM的出现使得Controller的存在感被完全的降低了,C直接持有VM,VM作为V和M的中间桥梁。
手动实现一个MVVM
大体流程图
创建Vue基类
// 基类
class Vue {
constructor(options) {
// 在当前实例中绑定$el,$data,对options的各种处理
this.$el = options.el
this.$data = options.data
...
}
}
数据绑定
- 主要就是解析指令,解析插值,将数据替换
- 过程:
在解析过程中,为了减少浏览器的回流与重绘,我们将处理dom放在内存中,经过解析操做后,统一在插入到容器中.
遍历存在内存中的dom,判断各个节点,判断如果是文本,进行变量替换,如果是标签,则进行属性解析的相关操作,如果该元素中还有子节点,继续遍历。
// 通过nodeType
class Compiler {
/*
@el元素
@vm当前实例子
*/
constructor(el, vm) {
// el是不是一个元素,如果不是则获取
this.el = this.isElementNode(el) === 1 ? el : document.querySelector(el)
this.vm = vm
// 将节点内容全部放在内存中, appendChild() 方法移除元素到另外一个元素。
const domFragment = this.createNodeFragment(this.el) // 为了减少浏览器的回流与重绘
// 编译内存节点
this.compile(domFragment)
// 将编译后的碎片放在el中
this.el.appendChild(domFragment)
}
// 编译内存节点
compile(node) {
if (!node) {
return
}
/** 判断类型
* 文本, {{}}
* 标签, v-
*/
// 拿到内存节点的子节点(只取一层) childNodes
const childNodes = node.childNodes // 类数组
Array.from(childNodes).forEach(child => {
if(this.isElementNode(child)) {
// 是标签
this.compileElement(child)
// 递归编译
this.compile(child)
} else {
// 是文本
this.compileText(child)
}
});
}
// 编译文本
compileText(text) {
const content = text.textContent
const metchResult = /\{\{(.+)\}\}/.exec(content)
if (/\{\{(.+)\}\}/.test(content)) {
CompilerUtil['text'](text, content, this.vm)
}
}
// 编译标签
compileElement(node) {
// v-, 获取元素属性,,
const attributes = node.attributes
// 遍历属性,是否有指令, 有指令的才需要双向绑定
Array.from(attributes).forEach(attr => {
const {name, value} = attr
// name是否为指令
if (this.isDirective(name)) { // 指令很多,需要区分 //v-mdel
const [,directive]= name.split('-') //v-on:click
// 根据指令左对应的处理,将节点树数据
CompilerUtil[directive](node, value, this.vm)
}
})
}
isDirective(name) {
return name.startsWith('v-')
}
// 判断节点类型
isElementNode(node) {
return node.nodeType === 1
}
// 将节点内容移动到内存中
createNodeFragment(el) {
let firstChild;
let fragment = document.createDocumentFragment()
while(firstChild = el.firstChild) {
fragment.appendChild(firstChild)
}
return fragment
}
}
// 基类
class Vue {
constructor(options) {
...
new Compiler(this.$el, this)
}
}
为了便于管理,统一将解析的过程放在一起,为下步的数据劫持做一些准备
// 解析工具
...省略,先把完整代码放这
CompilerUtil = {
getValue(name, vm) {
return name.split('.').reduce((acc, current) => {
return acc[current]
}, vm.$data)
},
setValue(expr, value) {
return expr.split('.').reduce((acc, current, index, arr) => {
console.log(acc, current, index, arr)
// 在最后一项赋值
if (index === arr.length - 1) {
return acc[current] = value
}
return acc[current]
}, vm.$data)
},
model(node, name, vm) {
// 双向绑定 node.value = value
node.addEventListener('input', (e) => {
const newValue = e.target.value
this.setValue(name, newValue)
})
const value = this.getValue(name, vm)
// 生成watcher,在cb中改变双向绑定的值
new Watcher(vm, name, (newValue) => {
fn(node, newValue)
})
const fn = this.updater.modelUpdate
fn(node, value)
},
on(node, name, vm, eventname) {
node.addEventListener(eventname, (e) => {
vm[name].call(vm, e)
})
},
html(node, name, vm) {
const value = this.getValue(name, vm)
// 生成watcher,在cb中改变双向绑定的值
new Watcher(vm, name, (newValue) => {
fn(node, newValue)
})
const fn = this.updater.htmlUpdate
fn(node, value)
},
getContentValue(vm, content) {
return content.replace(/\{\{(.+?)\}\}/g, (...arg) => {
return this.getValue(arg[1], vm)
})
},
text(node, content, vm){
const value = content.replace(/\{\{(.+?)\}\}/g, (...arg) => {
new Watcher(vm,arg[1], (newValue) => {
// 不能拿a的值替换 {{a}}, {{b}}
fn(node, this.getContentValue(vm, content))
})
return this.getValue(arg[1], vm)
})
const fn = this.updater.textUpdate
fn(node, value)
},
updater: {
modelUpdate(node, value) {
// 双向绑定
node.value = value
},
htmlUpdate(node, value) {
node.innerHTML = value
},
textUpdate(node, value){
node.textContent = value
},
}
}
数据劫持
- 基于发布订阅模式(观察者模式)其实就是一个一个的watcher
- 通过Observe监视data, 这个过程其实就是将传入的data变成Object.definedproperty的形式,然后对它的get和set进行对象的处理
- Watcher: 存储老值,拿到新值,当老值与新值不一致时,执行回调
- 通过Dep收集watcher, 与分发依次执行watcher.update()
当最开始取值时,我们会触发Object.definedProperty中的get方法,在数据绑定模块说过,最终会走到CompilerUtild的model或者text方法,因此我们可以在这里new Watcher(),监视数据是否有变化,同时将实例的watcher挂在Dep类的target属性上,在我们new Watcher()的同时,因为会获取一次老值,因此又会出发一次get方法,此时我们实例化Dep,根据Dep是否有target属性,动态的收集watcher,当用户修改数据时,会出发,set()方法,我们在此时发,触发notify,让收集的watcher依次执行他们的update方法
...
class Dep {
constructor(vm, watcher){
this.subs = []
}
add(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
// 发布订阅模式,是一个一个的watcher
class Watcher {
constructor(vm, expr, cb) {
// 观察有某个属性,当该属性变化时执行cb方法
this.vm = vm
this.expr = expr
this.cb = cb
Dep.target = this
// 先存放一个老值,好对比有没有变
this.oldValue = this.get()
Dep.target = null
}
get() {
return CompilerUtil.getValue(this.expr, this.vm)
}
update() {
const newValue = CompilerUtil.getValue(this.expr, this.vm)
// 当新值和老值不一样时,执行回调
if (newValue !== this.oldValue) {
this.cb(newValue)
}
}
}
// 数据劫持
class Observe {
// 将数据变成Object.definedproperty类型的
constructor(data) {
this.observe(data)
}
isObject(data) {
return typeof data === 'object' && data !== null
}
defineReactive(data, key, value) {
this.observe(value)
const dep = new Dep()
const _this = this
Object.defineProperty(data, key, {
get() {
Dep.target && dep.add(Dep.target)
return value
},
set(newValue) {
if (newValue !== value) {
_this.observe(newValue)
value = newValue
dep.notify()
}
}
})
}
observe(data) {
if (this.isObject(data)) {
// 是对象,将对象转为Object.definedProperty
for(let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
}
// 基类
class Vue {
constructor(options) {
// 在当前实例中绑定$el,$data
this.$el = options.el
this.$data = options.data
let computed = options.computed // 有依赖关系
let methods = options.methods
if (this.$el) {
new Observe(this.$data)
...
this.proxyVm(this.$data)
// 将vm.$data放在vm上
new Compiler(this.$el, this)
}
}
proxyVm(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key] //进行代理操作
},
set(newValue) {
data[key] = newValue
}
})
}
}
}
computed以及methods实现
class Vue {
constructor(options) {
// 在当前实例中绑定$el,$data
this.$el = options.el
this.$data = options.data
let computed = options.computed // 有依赖关系
let methods = options.methods
if (this.$el) {
new Observe(this.$data)
for(let key in computed) {
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this)
}
})
}
for(let eventName in methods) {
Object.defineProperty(this, eventName, {
get() {
return methods[eventName]
}
})
}
this.proxyVm(this.$data)
// 将vm.$data放在vm上
new Compiler(this.$el, this)
}
}
proxyVm(data) {
for (let key in data) {
Object.defineProperty(this, key, {
get() {
return data[key] //进行代理操作
},
set(newValue) {
data[key] = newValue
}
})
}
}
}
...省略
vue-router原理
vue-router作为vue的插件,利用 Vue.js 提供的插件机制使用.use(plugin) 来安装 VueRouter,而这个插件机制则会调用该 plugin 对象的 install 方法
手动实现vue-router的hash模式
install阶段
+ 在install方法中,我们使用'Vue.mixin',在根组件中定义_router属性,如果是子组件,通过$parent使得每个子组件都可以访问根组件的_router。
+ 为vue的原型注入$router和$route属性使得每个组件都可以通过this.$route和this.$router
+ 将_route属性指向current(当前路由信息),并将它转为响应式,以后如果我们更新app的_route属性,就会重新渲染视图
+ 将router-view注册为全局组件
+ 定义全局router的init方法
// install
export function install (Vue) {
if (install.installed) return
install.installed = true
// 赋值私有 Vue 引用
_Vue = Vue
// 注入 $router $route
Object.defineProperty(Vue.prototype, '$router', {
get () { return this.$root._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this.$root._route }
})
// beforeCreate mixin
Vue.mixin({
beforeCreate () {
// 判断是否有 router,判断根实例,通过vue.mixin使每个组建都可以拿到根是例子
if (this.$options.router) {
this._rooter
// 赋值 _router
this._router = this.$options.router
// 初始化 init
this._router.init(this)
// 定义响应式的 _route 对象
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._router = this.$parent && this.$parent.$options.router
}
}
})
// 注册组件
Vue.component('router-view', View)
Vue.component('router-link', Link)
// ...
}
init阶段
+ init阶段,根据扁平化用户传入的参数
+ 根据参数创建router实例
+ 根据当前的路径,渲染并设置监听
import install from './install'
import createMatcher from './createMatcher'
import HashHistory from './history/HashHistory'
export default class VueRouter {
constructor(options) {
// 根据不同的路径,渲染不同的组件
// 将传进来的数据扁平化,创建映射表,动态添加routes
this.matcher = createMatcher(options.routes || [])
this.mode = options.mode || 'hash' //切换模式
/*
//这两个类都继承与History基类
hash: HashHistory
history:HTML5History
*/
this.history = new HashHistory(this)
}
match(path) {
return this.matcher.match(path)
}
init(app) { // app是根实例
// 根据路由,显示到制定的组件,并且设置监听
const setUpListener = () => { // 跳转成功的回调
this.history.setUpListener()
}
this.history.transitionTo(
this.history.getCurrentLocation(),
setUpListener
)
this.history.listen((route) => {
app._route = route // 响应式的原理
})
// transitionTo公共的,getCurrentLocation属于hash,对于history,直接找path.name
}
}
VueRouter.install = install
createMatcher
这个方法主要有两个方法: match, 以及addRoutes
+ 将传入的数据扁平化,生成路由映射表
+ addRoutes在做权限管理时,我们经常会临时添加路由配置
+ Match根据当前的路径在路由映射表中找到匹配项
import createRouterMap from './createRouterMap'
import {createRoute} from './history/BaseHistory'
export default function createMatcher(routes) {
/* 主要是扁平化用户传入的数据,
路径集合[/...]
创建路由映射表 {/: []...}
*/
const { oldPathMap: pathMap } = createRouterMap(routes) // 初始化数据
function match(path) {
// 如果有记录,则生成一个{path, match:[]}的对应关系
const record = pathMap[path]
if (record) {
return createRoute(record, {
path
})
}
// 拿到path取pathMap中找/,找到对应的记录,并且根据记录产生一个匹配数组
return createRoute(null, {path})
}
// 动态添加的方法
function addRoutes() {
// 做权限管理时用
const { pathList, pathMap } = createRouterMap(routes, pathList, pathMap) // 添加新的数据
}
return {
match,
addRoutes
}
}
// createRouterMap.js
export default function createRouterMap(routes = [], oldPathList = [], oldPathMap = Object.create(null)) {
// 扁平化数组
routes.forEach(route => {
addRouteRecord(route, oldPathList, oldPathMap)
});
return {
oldPathList,
oldPathMap
}
}
function addRouteRecord(route, oldPathList, oldPathMap, parent) {
// path, name, component
let path = parent ? `${parent.path}/${route.path}` : route.path
let record = {
path,
component: route.component,
parent
}
if (!oldPathMap[path]) {
oldPathList.push(path)
oldPathMap[path] = record
}
if (route.children) {
route.children.forEach(child => {
addRouteRecord(child, oldPathList, oldPathMap, route)
})
}
}
History类
路由分为hash和history模式
+ hash模式的原理是监听hashChange事件,由于锚点变化也会在历史记录栈添加新的记录,所以history.length也会在锚点变化之后改变。
+ histroy模式的原理:
+ 在HTML5,history对象提出了 pushState() 方法和 replaceState()方法,这两个方法可以用来向历史栈中添加数据,然后监听popState事件,
+ 当活动历史记录条目更改时,将触发popstate事件。如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用的影响,popstate事件的state属性包含历史条目的状态对象的副本。
+ 需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back())
// history基类
export function createRoute(record, location) {
let res = []
if (record) {
while(record) {
res.unshift(record)
record = record.parent
}
}
return {
...location,
matched: res
}
}
export default class Base {
constructor(router) { // Vue-router
this.router = router
this.curret = createRoute(null, {
path: '/'
})
}
transitionTo(path, callback) {
// 拿到路径到映射表里取匹配
const route = this.router.match(path)
if(this.curret.path !== path && route.matched.length !== this.curret.matched.length) {
this.updateRoute(route)
}
callback && callback()
}
updateRoute(route) {
this.curret = route
this.cb && this.cb(this.curret)
}
listen(cb) {
this.cb = cb
}
}
// hash路由类
import BaseHistory from './BaseHistory'
function getHash() {
return window.location.hash.slice(1)
}
function ensureSlash() {
if (window.location.hash) {
return
}
window.location.hash = '/'
}
// 让HashHistory继承自BaseHistory
export default class HashHistory extends BaseHistory {
// 继承base基类
constructor(router) {
super(router)
ensureSlash() // 确保路径有/
}
getCurrentLocation() {
return getHash()
}
setUpListener() {
// 监听hash变化
window.addEventListener('hashchange', () => {
ensureSlash()
this.transitionTo(this.getCurrentLocation())
// 路径变化之后,要将新的route属性覆盖current
})
}
}
函数式组件
// view.js
// 函数式组件
export default {
functional: true,
render(h, { parent, data }) {
// h为渲染方法,context为上下文
// 拿到match一次渲染, curret中的match
// data = { attr: 1}
let matched = parent.$route.matched
let dep = 0
data.routerView = true
while(parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
dep++
}
parent = parent.$parent
}
let record = matched[dep]
if (!record) {
return h()
}
let component = record.component
return h(component, data)
}
}
vuex原理
- vue中的action与mutation有什么区别
- 如何解决vuex持久化的问题
- mutation是同步操作,actions是异步操作
vuex是vue的状态管理器, 采取的flux思想,主要思路是单向数据流。 放一张最经典的图,用户通过视图层触发(dispatch)actions,actions载提交(commit)mutations更新state,从而促进视图的更新
源码分析
- 数据:state --> data 获取数据:getters --> computed 更改数据:mutations --> methods
- 同vue-router一样,作为vue的插件,需提供一个install方法
install
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.store) {
//找到根组件 main 上面挂一个$store
this.$store = this.$options.store
} else {
//非根组件指向其父组件的$store
this.$store = this.$parent && this.$parent.$store
}
}
})
state
- 我们可以看出Vuex的state状态是响应式,是借助vue的data是响应式,将state存入vue实例组件的data中;
// 对state的处理
class Store{
constructor() {
this._s = new Vue({
data: {
// 只有data中的数据才是响应式
state: options.state
}
})
}
get state() {
return this._s.state
}
}
...
getters
- Vuex的getters则是借助vue的计算属性computed实现数据实时监听。
...
let getters = options.getters || {}
this.getters = {};
Object.keys(getters).forEach((getterName) => {
// 将getterName 放到this.getters = {}中
Object.defineProperty(this.getters, getterName, {
// 当你要获取getterName(myAge)会自动调用get方法
// 箭头函数中没有this
get: () => {
return getters[getterName](this.state)
}
})
})
...
mutations与actions
...
let mutations = options.mutations || {}
this.mutations = {};
Object.keys(mutations).forEach(mutationName=>{
this.mutations[mutationName] = (payload) =>{
this.mutations[mutationName](this.state,payload)
}
})
...
// actions的原理
const actions = options.actions
this.actions = {}
Object.keys(actions).forEach(actionName => {
this.actions[actionName] = payload => {
this.actions[actionName](this, payload) // 内部函数执行,第一个参数是状态
}
})
commit dispatch的实现
// commit
commit = (type,payload){ // 保证this指向
this.mutations[type](payload)
}
// dispatch
dispatch = (type,payload)=>{
this.actions[type](payload)
}
modules (分模块)
- 将传入的参数组合成树结构,然后遍历+发布订阅
如图所示:
class ModuleCollections {
constructor(options) {
this.regiester([], options) // 将模块注册成树结构
}
regiester(path, rootModule) {
let module = {
_rowModule: rootModule,
_children: {},
state: rootModule.state
}
if (path.length === 0) {
this.root = module // 根数据
} else {
// reduce [a, b, c] = { a: {b: {c{}}}}
path.reduce((root, item, index, arr) => {
if (arr.length -1 === index) {
return root._children[item] = module
} else {
return root = root._children[item]
}
}, this.root)
}
if (rootModule.modules) {
Object.keys(rootModule.modules).forEach((moduleName) => {
this.regiester(path.concat(moduleName), rootModule.modules[moduleName])
})
}
}
}
Store类
- 所有的getters都会定义在this.getters中,即使是模块中的,actions以及mutations同理
class Store {
constructor(options) {
this.s = new Vue({
data() {
return {
state: options.state
}
}
})
this.getters = {}
this.mutations = {}
this.actions = {}
this._modules= new ModuleCollections()
installModule(this, this.state, [], this._modules.root)
}
commit = (name, payload) => {
this.mutations[name].forEach(fn => fn(preload))
}
dispatch = (dispatchName, payload) => {
this.actions[dispatchName].forEach(fn => fn(preload))
}
get state() { // 类的属性访问器 //调用this.state会走到这里
return this._s.state
}
}
// installModule
const installModule = (store, rootState, path = [], rootModule) => {
// 如果path 子[a]
if (path.length > 0) {
let parent = path.slice(0, -1).reduce((root, item) => {
return root = root[item]
}, rootState)
// 实现
Vue.$set(parent, path[path.length -1], rootModule.state)
}
let getters = rootModule._rowModule.getters
if (getters) {
Object.keys(getters).forEach(getterName => {
Object.defineProperty(store.getters, getterName, {
get() {
return getters[getterName](rootModule.state) // 更新自己的状态
}
})
})
}
// 整合mutaions
let mutations = rootModule._rowModule.mutations
if (mutations) {
Object.keys(mutations).forEach(mutationName => {
// 同名的事件可能是多个
let mutationArr = store.mutations[mutationName] || []
mutationArr.push((preload) => {
mutations[mutationName](rootModule.state, preload)
})
store.mutations[mutationName] = mutationArr
})
}
// 整合actions
let actions = rootModule._rowModule.actions
if (actions) {
Object.keys(actions).forEach(actionName => {
// 同名的事件可能是多个
let actionsArr = store.actions[actionName] || []
actionsArr.push((preload) => {
actions[actionName](rootModule.state, preload)
})
store.actions[actionName] = actionsArr
})
}
Object.keys(rootModule._children).forEach((childName) => {
installModule(store, rootState, path.concat(childName), rootModule._children[childName])
})
}
数据持久化
- 在store中加插件
const persits = (store) => {
store.subscribe(mutations, state) {
localStorage.setItem('vuex-store', JSON.stringify(state))
}
}
new Vuex.Store({
plugin: [
persits
]
...
})
参考文献
~~ 未完,持续更新