MVC、MVVM
MVC
- M-Model 数据
- V-VIew 视图、界面
- C-Controller 控制器、逻辑处理
通常是使用controller更新model,视图从model中读取数据渲染,当有用户输入时,由控制器去更新模型,并通知视图更新
缺点:controller任务太重,项目越大,controller越复杂,不利于维护,还有就是不灵活
MVVM
- Model-模型、数据
- View-视图、模版(视图和模型是分离的)
- ViewModel-连接Model和View,“桥”
<body>
// view
<div id="app">
<ul>
<li v-for="i in list" :key="i.id">{{i.name}}</li>
</ul>
<button @click="handleAdd">+</button>
</div>
<script src="../../dist/vue.js"></script>
<script>
// model
var list = []
// ViewModel
var vm = new Vue({
el: '#app',
data: {
list
},
methods: {
handleAdd() {
this.list.push({id: Math.floor(Math.random()*1000),name: 1})
}
}
})
</script>
</body>
在MVVM中引入了viewModel概念,只关心数据和业务的处理,不关心view如何处理数据,view、model都可以独立出来,任何一方改变也不一定需要改变另一方,并且可以将复用的逻辑放到ViewModel中,让多个view复用ViewModel.
以Vue为例,ViewModel就是组件的实例,View就是模板,Model在引入Vuex后,是完全可以和组件分离的。
在MVVM中还引入了一个隐式的Binder层,实现了View、ViewModel绑定,以Vue为例,就是Vue通过解析模板中的插值、指令来实现View和ViewModel的绑定.
对于MVVM,最重要的不是通过双向绑定将View、ViewModel绑定,而是通过ViewModel将视图中的状态和用户的行为分离出一个抽象。
路由原理
前端路由实现的本质就是监听url变化,然后匹配路由规则,显示对应的页面,且不用刷新页面。
路由实现模式:hash、history
- hash
www.test.com/#/ 就是hash url,当 # 后边hash变化时,通过hashchange监听url变化。无论hash如何变化,服务器端收到的url都是www.test.com?
window.addEventListener('hashchange', () => {
// ... 具体逻辑
})
实现简单,兼容性也好
- History
h5新功能,主要用history.pushState、 history.replaceState改变url。通过这种模式改变url不会引起页面刷新,只会更新浏览器的历史记录
// 新增历史记录
history.pushState(stateObject, title, URL)
// 替换当前历史记录
history.replaceState(stateObject, title, URL)
当用户点击后退时,触发popState事件
window.addEventListener('popstate', e => {
// e.state 就是 pushState(stateObject) 中的stateObject
console.log(e.state)
})
两种模式的对比:
- hash只能修改#后边的,history可以设置任意url
- history可以通过api添加任意类型数据到历史记录,hash只能修改hash值
- hash无需后端配置,兼容性好。history在用户手动输入地址或刷新后发起url请求,需要后端配置
Vue
virtual dom
虚拟dom,就是使用js对象来描述一个真实的dom结构,真实dom属性过于多,任何操作都是性能杀手,基本上所有的优化,都会提到少操作dom。
我们在任何数据修改后,操作dom之前,都对数据作对比
虚拟dom提升性能是个优势,其他的优势还在于:
- 虚拟dom作为兼容层,可以跨端开发,毕竟只是个对象,可以实现非Web应用
- 可以渲染到ssr、同构渲染等
- 实现组件的高度抽样化?
生命周期
beforeCreate
基本上是初始化数据之外的各种方法,都在init方法里
vm._self = vm
// 初始化组件属性,比如:$children、$parent、ref等
initLifecycle(vm)
// 添加各种事件
initEvents(vm)
// 在实例上添加createElement
initRender(vm)
callHook(vm, 'beforeCreate')
export function initRender (vm: Component) {
...
// 主要用在render函数的 with(this){} 里边的_c就是这里,与用户无关
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// render(h){},里的h就是这里,就是经常提到的渲染函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// 使用科里化定义上边两个方法,目的就是保留当前的实例,以便在with(this)中使用
...
}
created
基本上所有的数据拦截,都在这里了
// 获取inject,并对其observe
initInjections(vm) // resolve injections before data/props
// 这里处理的数据就多了,data、methods都有
initState(vm)
// 将project挂载到实例的_provided上
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
beforeMount
挂载之前,init方法最后一步
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
$mount
执行原型上的$mount方法,生成render的mount方法
找render,如果有用户自定义的render。
- 如果没有找template,找到后通过compileToFunctions编译,生成render函数,
- 中间又经过ast、代码优化、代码生成。
- 将render挂到实例的options
$mount
包含mountComponent的mount方法 检查是否有render,
callHook(vm, 'beforeMount')
mounted
注册更新函数,创建Watcher实例,至关重要
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
Watcher实例化时,会执行一次updateComponent
- 执行_render函数,会触发依赖收集的get取值,生成vnode
- 执行_update,这里已经有vnode了,先将vnode挂到实例的_vnode属性上。执行_patch_,将vnode渲染到页面
- 真实dom挂到实例的$el,完毕
callHook(vm, 'mounted')
beforeUpdate
在Watcher实例化时候,注册的 修改页面数据时,触发set,执行dep.notify(),就是以上Watcher中的before
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
updated
watcher.run(),内部还是执行updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
重新执行_render,得到新的vnode
传入新旧节点,做patch。更新完以后:
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated')
}
}
}
keep-alive
独有生命周期:activated、deactivated,切换时不会进行销毁,是缓存到内存中
切换组件时,保存一些组件状态防止多次渲染,就可以使用keep-alive组件报过组要保存的组件
extend
扩展一个组件生成一个构造函数,通常与$mount配合使用
const Ctor = Vue.extend(Component)
const comp = new Ctor({
propsData: props
})
// 只挂载,不插入。源码中$mount后会将真实dom挂到$el
comp.$mount();
document.body.appendChild(comp.$el)
comp.remove = () => {
// 移除dom document.body.removeChild(comp.$el) // 销毁组件
comp.$destroy();
}
let SuperComponent = Vue.extend(Component)
new SuperComponent({
created() {
console.log(1)
}
})
new SuperComponent().$mount('#app')
mixin、mixins
- mixin 多用于混入,会影响到每个组件,插件一般就是基于这个做的初始化
let Vue;
class VueRouter {
constructor(options) {
this.$options = options; // path、component映射
this.routeMap = {};
// current保存当前hash、vue使其是响应式的
this.app = new Vue({
data: {
current: "/"
}
});
}
}
// 插件逻辑:注册$router,初始化router
VueRouter.install = function (_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
// router选项存在确定是根组件
if (this.$options.router) {
// 执行一次,将router实例放到Vue原型,以后所有组件实例就均有$router
Vue.prototype.$router = this.$options.router;
this.$options.router.init();
}
}
});
};
export default VueRouter;
- mixins 如果多个组件需要引入相同的数据,或者相同的业务逻辑,就可以将这些抽离出来,通过mixins混入代码
import billMixin from "../mixin/bill";
export default {
name: "resubmit",
mixins: [billMixin],
components: {
...
}
}
混入的钩子,先于组件同名钩子函数执行,同名选项会合并
响应式原理
- 缺陷 通过下标方式修改数组数据或者给对象新增属性,组件不会重新渲染
var vm = new Vue({
el: '#app',
data: {
obj: {
cur: 0
},
arr: [1]
},
methods: {
handleClick() {
this.obj.cur++
this.obj.num = 10
},
handleClick2() {
this.obj.num ++
this.arr[0] = 30
}
}
})
使用$set,对象是defineReactive(ob.value, key, val),又做了一次响应式处理,数组是将修改下标的值,转为使用slice修改值
数组是重写了7个方法

编译过程
parse Ast
通过各种正则表达式去匹配模板内容,将内容提取出来做各种逻辑操作,生成最基本的ast
optimize Ast
只是对静态节点打上标记static,以便在patch过程直接跳过。 官方还说静态节点,在每次从新渲染就不用了再重新创建了
generate code
nextTick
可以让我们在下次dom更新循环结束后,执行延时回调,用于获取更新后的dom
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
...
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
React
生命周期
Fiber本质是虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧。将同步渲染改成异步渲染,在不影响体验的情况下分段计算、更新
对于异步渲染,生命周期分两个阶段:reconciliation、commit,前者可以打断,可能会调用多次,后者不能打断
Reconciliation阶段:componentWillMount、componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate
commit阶段:componentDidMount、componentDidUpdate、componentWillUnmount
除了shouldComponentUpdate,其他周期函数尽量少用。新加了两个周期函数解决这个问题
- getDerivedStateFromProps用于替换componentWillReceiveProps,该函数会在初始化和update时被调用。
- getSnapshotBeforeUpdate用于替换componentWillUpdate,该函数会在update后dom更新前被调用,用于读取最新dom结构
setState
- 直接修改值,值会改变但是不会触发render,想触发需要forceUpdate
this.state.counter += 1
this.forceUpdate()
- 批量执行,只会执行最后一次操作,{...{counter: 1}, ...{counter: 2}}
componentDidMount() {
this.setState({ counter: this.state.counter + 1 })
this.setState({ counter: this.state.counter + 2 })
}
// 页面显示2
如果想让每次操作都生效,比如上例,页面想输出3,三种方法:定时器、传递函数、原生事件
总结:setState只有在合成事件和⽣生命周期函数中是异步的,在原 ⽣生事件和setTimeout中都是同步的,这⾥里里的异步其实是批量量更更 新
shouldComponentUpdate
PureComponent,底层就是实现了浅比较 state
// 函数组件:
const Test = React.memo(() => <div>PureComponent</div>)
组件通讯
import React from 'react';
const StoreContext = React.createContext('light');
export {
StoreContext
}
// parent组件
render() {
return (
<StoreContext.Provider value="dark">
<Child />
</StoreContext.Provider>
)
}
// 子组件
render() {
return (
<StoreContext.Consumer>
{(context) => <span>{context}</span>}
</StoreContext.Consumer>
)
}
事件机制
React其实是自家实现了一套事件机制,jsx并没有将事件绑定到真实dom上,而是通过事件代理绑定到了document,这样做可以减少内存消耗,还能在组件销毁时,统一订阅和移除事件?
event.stopPropagation没用,要用event.preventDefault
抹平了浏览器之间的兼容问题,有跨平台的能力。
vue和react对比
虚拟dom
Vue组件外部是响应式,组件内部是虚拟dom。 源码中渲染组件时,mountComponent会实例化一个Watcher,后期通过Watcher来执行渲染函数。组件内部虚拟dom变化是通过dom diff比较
React里边全是虚拟dom
Vue表单可以使用v-model,相对于react更方便些,但v-model就是个语法糖,本质上和react写表单方式没啥区别
数据响应式
改变数据方式不一样,Vue修改状态相比react简单,react需要手动触发setState去改变,并且使用这个也有一些坑?。Vue底层使用依赖跟踪,页面渲染已经是最优了,但react还需要用户手动去优化这些方面的问题,就是说Vue已经为你考虑了很多,但是react给了你自由
React16以后,由于fiber架构,生命周期可能会触发多次
上手难度
React需要使用Jsx有上手成本,并需要一整套的工具链,但是完全可以通过js控制页面,更加灵活。
Vue使用模板语法,相比jsx,没那么灵活。但完全脱离工具链?,直接编写render函数就能直接在浏览器运行
Vue开始的定位就是尽可能的降低开发难度,React更多的是改变用户去接受它的概念和理念