Author: Yu Li
date: 2020/04/17
- 框架相关总结
- 1. MVVM Model-View-ViewModel
- 2. 发布订阅消息模式
- 3. Vue的 computed 与 watch 异同
- 4. Vue深入响应式原理
- 5. Vue-router
- 6. Vue生命周期钩子
- 7. Virtual DOM(JS和DOM引擎是独立的)
- 8. diff方法(简化)
- 9. 微信小程序原理
- 10. 小程序为什么要设计双线程?
- 11. 小程序为什么使用Hybrid的渲染方式?
- 12. 微信小程序生命周期
13. Vue生命周期- 14. Vue相比jQuery优势?
- 15. MVC、MVP、MVVM区别?
- 16. Vue3.0有哪些新特性?
- 17. TypeScript和JavaScript区别?
- 18. 为什么Vue的data必须是一个函数,而不能是对象
- 19. React生命周期
- 20. Vue和React差异
- 21. Vue Virtual Dom Diff算法详解
- 22. 为什么v-for的key必须唯一?
- 23. 为什么key最好不要用index?
- 24. webpack的热更新(Hot Module Replacement,HMR)原理?
- 25. 关于Vue DOM的异步更新
- 26. webpack打包流程
- 27. 小程序性能优化方法
- 28. Vue computed 源码理解
React不是MVVM,Vue和Angular是MVVM
优点:1. View Model同步,双向绑定 2. 关注数据,不关心同步
缺点:1. 大型项目,ViewModel冗杂,维护成本高 2. 数据绑定没法debug
Vue 数据劫持,Object.defineProperty(),详见/Vue底层/wue-learn
Angular 脏检查
Knockout 发布订阅
发布 publish
订阅 subscribe
最简单的例子:Vue组件传值
let bus = new Vue();
Vue.prototype.bus = bus;
this.bus.emit('funcName',parm);
this.bus.on('funcName',回调);
与观察者模式区别:
(1)Observer模式(针对对象)
指函数自动观察数据对象,一旦对象有变化,会自动执行,实现方法Proxy+Reflect或defineProperty。详见/js原理/观察者模式.js
(2)发布订阅模式(针对事件)
emit发布
on订阅
once订阅一次
remove取消订阅
publisher发布[thingA,thingB...]触发subscriber订阅事件的回调函数[funcA,funcB,...]
详见/js原理/发布订阅模式.js
computed:内部有属性改变,触发,没有改变,读取缓存。可以多个属性影响一个属性
watch:监听data,监听对象要用深度监听,默认第一次不监听:immediate:false
数据改变,加入队列,多次改变取最新的一个,下一次事件循环tick(nextTick)会对新旧VNode做diff,然后统一渲染变化的节点
路由绑定组件,监听路由变化,渲染指定组件
详见/Vue底层/Vue路由原理.html
https://zhuanlan.zhihu.com/p/27588422
https://zhuanlan.zhihu.com/p/24574970
mode三种模式:HashHistory | HTML5History | AbstractHistory;
beforeCreate->created
->beforeMount->mounted
->beforeUpdate->updated
->beforeDestroy->destroyed
VD是简单JS对象,最少包含3个属性:tag\props\children
VD与DOM一一对应,由HTML生成
作用:将页面状态抽象成JS对象,配合不同渲染工具,可跨平台渲染,不会立即回流重绘,会与内存比较,一次性更新,提高页面渲染速度
VD如何生成真实DOM?
JSX编译器将HTML转为函数形式(h函数)
h函数(hyperscript)是生成HTML的脚本(生成虚拟DOM),作用类似createElement(真实DOM)
//Vue中
render:function(h){return h(App)}
等价于
render:h=>h(App)
//完整版有三个参数
render(h) {
return h('div', {}, [...])
}
// 第一种是元素的类型(这里显示为 div)。
// 第二个是数据对象。 我们在这里主要包括:props, attrs, dom props, class 和 style。
// 第三个是一组子节点。 然后,我们将嵌套调用并最终返回一个虚拟DOM节点树。
VD递归->如果是string或number直接doc.createTextNode->doc.createElement(tag)->doc.setAtrribute(props)->children.map(createElement).forEach(element.appendChild.bind(element))->return element
diff(oldNode,newNode)
返回比较结果对象patch
patch = {
type, // 1.create 2.remove 3.replace 4.update(update和replace区别是replace是类型、标签改变等)
vdom, // VNode
props:[{
type, // 1.remove 2.update
key,
value
}],
children:[patch,...]
}
思路:1.state改变,生成新的VD 2.比较新旧VD异同 3.生成差异对象patch 4.遍历patch,更新DOM
VD的优势在于,减少了渲染事件
劣势在于,增加了JS计算事件
在DOM比较复杂的情况下,VD的优势比较明显
https://segmentfault.com/a/1190000018631528
小程序通信模型如图:
WXML模板和WXSS样式工作在渲染层WebView,JS脚本工作在逻辑层JsCore,分别由两个线程管理。
渲染层存在多个WebView线程,线程间通信经过客户端做中转;
逻辑层发送网络请求也经由Native转发。
通过setData方法,把数据从逻辑层传递到渲染层,经过对比前后差异,把差异应用在原来的Dom树上,渲染出正确的UI界面。
事件处理:通过事件捕获和事件冒泡,通过native传给JSCore。
数据通信:
由于是双线程模型,需要保证时序正确。
小程序生命周期中,存在若干次页面数据通信,逻辑层向视图层发送页面数据,视图层向逻辑层反馈用户事件。
缓存机制:每个小程序缓存空间上线10MB,如果到达10MB,再调用setStorage会触发fail回调
主要还是处于管控和安全上的考虑。我们需要阻止开发者使用一些浏览器提供的,诸如跳转页面、操作DOM、动态执行脚本的开放性接口。同时不会阻塞渲染
小程序考虑到了原生开发和纯web开发的不利之处:用纯客户端原生技术来编写小程序的话,小程序代码需要与微信代码一起编包
,显然过于繁重
了;而用纯Web技术来渲染小程序,在一些有复杂交互的页面上可能会面临一些性能
问题。
因此,小程序选择了Hybrid
的渲染方式,将UI渲染跟JavaScript的脚本执行分在了两个线程,巧妙了解决了上述问题,作为一种全新的前端开发框架能够很快成为许多开发者的最爱也就不足为奇了。
总结,视图层用Web渲染为主,原生渲染为辅,逻辑层用JSCore
onLoad>onShow>onReady>onHide>onUnload
不需要手动操作DOM节点来更新视图,只需要修改数据即可。
jQuery把Model和View-Model写在一起,太复杂。
jQuery没有对修改数据做diff,多次更新视图,渲染效率低。
https://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html
MVC是单向,View>Controller>Model>View
MVP是双向,View>Presenter>View, Presenter>Model>Presenter
MVVM是双向,View<>ViewModel<>Model
MVP和MVVM区别在于,MVP是双向通信(主动),而MVVM是双向绑定(监听)
(1)更快
虚拟DOM重写
静态树提升(跳过修补整棵树,从而降低渲染成本)
静态属性提升(跳过不会改变节点的修补过程,但是它的子组件会保持修补过程)
Proxy替代Object.defineProperty,节省内存开销,但浏览器可能不兼容(ie11)
(2)更小
更友好的tree-shaking
core runtime压缩后10kb
(3)更易于维护
Flow->TypeScript
包的解耦
编译器重写
(4)更多的原生支持
运行时内核也将与平台无关,使得Vue可以更容易地与任何平台(例如Web,iOS或Android)一起使用
(5)更易于开发使用
暴露响应式的api,如
import {observable, effect} from 'vue'
const state = observable({
count:0
})
effect(()=>{
console.log(`count is: ${state.count}`)
}) // count is: 0
state.count++ // count is: 1
轻松识别组件重新渲染的原因
const Comp = {
render(props){
return h('div',props.count)
},
renderTriggered(event){
debugger
}
}
提供对TypeScript的支持(TSX)
TypeScript是ECMAScript 2015的语法超集,是JavaScript的语法糖。JavaScript程序可以直接移植到TypeScript,TypeScript需要编译(语法转换)生成JavaScript才能被浏览器执行。一图胜千言:
静态类型语言和动态类型语言得核心区别在于,静态类型语言(statically-typed languages)会在编译时(compile time)进行类型检查,而动态语言(dynamically-typed)则是在运行时进行类型检查(runtime)。
对于组件而言,如果是对象,那么每个组件实例的data属性都是共享的(引用),无法复用。而用函数的话,每个实例的data都是独立的。
对于根示例而言,由于不存在复用情况,所以用对象是可行的
(1)数据是否可变
React整体是函数式思想,数据不可变,单向数据流,意思就是数据改变之后布局不会自动更新,必须通过api来触发(shouldComponentUpdate())
Vue是响应式的,数据可变,双向绑定,通过对每一个属性添加Watcher监听数据变化,自动更新视图
(2)生命周期不同
React的生命周期见19,Vue的生命周期见6
其中,React中render()函数中,创建虚拟dom,进行diff算法,更新dom树都在此进行。此时就不能更改state了。
(3)JSX和模板
react:
HTML 语言直接写在 JavaScript 语言之中,不加任何引号,这就是 JSX 的语法,它允许 HTML 与 JavaScript 的混写
vue:
Vue.js 使用了基于 HTML 的模版语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。Vue.js 的核心是一个允许你采用简洁的模板语法来声明式的将数据渲染进 DOM 的系统。
(4)组件作用域内的css
Vue: scoped
React: import或class对象形式
(5)组件传值
React: props和callback函数
Vue: props和emit函数,需要声明
https://juejin.im/post/6844903607913938951
数据变化后,会生成一棵新的Virtual Dom树,新旧dom树需要进行比较,看哪些地方修改了。这个比较的过程就是diff算法。如图
新旧节点只会在同层进行比较。这是如何实现?
因为首先,新旧dom树都只有一个唯一的根节点,那么从根节点开始进行遍历,如果是SameNode,则进行patchVNode,否则直接创建新的根节点,替换旧的根节点。
如何判断isSameVNode?
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是的时候,type必须相同
)
}
patchVNode简单来说,就是先比较oldVNode和newVNode是否指向同一个对象,如果是,直接return;
否则比较oldVNode和newVNode的文本,如果存在文本但不一致,则修改真实dom的文本;
否则(说明有改动),先更新根节点,然后需要判断根节点的children是否一致。这个过程是:如果oldCh和newCh都存在,但不同,则updateChildren,如果newCh存在,则创建子节点,如果oldCh存在,则移除子节点。
从流程图可以清晰看到,当被订阅者的setter被触发(修改),触发通知重新渲染(Dep.notify)。调用patch函数,判断isSameVNode,若不是,则直接替换。若是,则patchVNode。下面详解updateChildren
两个dom数组,oldS和oldE分别指向旧children首尾,newS和newE分别指向新children首尾
[a,b,c] // old [b,d,c,a] // new
while(oldS<=oldE && newS<=newE)执行以下判断
如果四个节点有等于null的,指针往中间靠;
首先有四种匹配模式:
(1)sameVNode(oldS,newS)
patchVNode(oldS,newS);
oldS++;
newS++;
(2) sameVNode(oldE,newE)
patchVNode(oldE,newE); oldE--; newE--;
(3) sameVNode(oldS,newE)
patchVNode(oldS,newE); insertBefore(oldS,oldE); //把oldS移到oldE后面(末尾) oldS++; newE--;
(4) sameVNode(oldE,newS)
patchVNode(oldE,newS); insertBefore(oldE,oldS); //把oldE移到oldS前面(开头) oldE--; newS++;
当以上四种规则都不匹配,则根据key来判断:
首先计算出oldS到oldE之间节点的key对应的index,有key就存入对象(createKeyToOldIdx函数){key:index}
(1)如果map[newS]不存在,表明是新节点
let a = createElm(newchildren[newS]) insertBefore(a,oldS)
(2)map[newS]存在,表明新旧节点key相同,这里还需要判断两种情况:
//1 是同一节点 let oldIndex = map[newS] if(sameVNode(oldchildren[oldIndex],newchildren[newS])) patchVnode(oldchildren[oldIndex],newchildren[newS]) oldchildren[oldIndex] = undefined insertBefore(oldIndex,oldS)
//2 不是同一节点,当作新节点插入 let a = createElm(newchildren[newS]) insertBefore(a,oldS)
以上两种情况判断完之后都要
newS++;
当oldchildren或newchildren有一个遍历完,就跳出循环,分两种情况:
(1)oldchildren遍历完,则说明有新增节点,把newchildren中剩余的节点插入
(2)newchildren遍历完,说明有节点被删除,把oldchildren中剩余节点删除
反向思考,如果key不唯一会出现什么情况,比如children中有两个key相同的子节点
a:'0' b:'1' c:'1' d:'2' e:'3'
假如在diff的时候,根据上面四种规则不匹配的情况下,需要利用key来判断。根据上面的知识我们知道,会首先在oldS和oldE之间根据key算一次hash,这时候会得到这样的map:
map = { '0':0, '1':2, '2':3, '3':4 }
也就是说,有一个节点的key和实际的dom key不一致,即使是同一个节点也无法复用。假设old children如下:
a:'0' c:'4' b:'1' e:'3' d:'2'
我们做一次updateChildren的过程:
(1)oldS == newS,oldS++, newS++
(2)四种情况都不符合,计算[b,c,d,e]key hash,发现找不到key=='4'的元素,则创建新元素
,插入在b之前,newS++
(3)oldS == newS,oldS++, newS++
(4)oldE == newS,oldE--, newS++
(5)oldE == newE,oldE--, newE--
(6)new children遍历完毕,删除old children的c元素
如果new children如下,则再看一次updateChildren过程:
a:'0' b:'1' c:'4' d:'2' e:'3'
(1)oldS == newS,oldS++, newS++
(2)四种情况都不符合,计算[b,c,d,e]key hash,找到c元素,isSameVNode,patchVNode(c,c),在oldS之前插入c,oldchildren中的c=undefined
(3)oldS == newS,oldS++, newS++
(4)oldS == undefined,oldS++
(5)oldE == newS,oldE--, newS++
(6)oldE == newE,oldE--, newE--
可以看出,差别在于(2)(4)(6),后者复用了所有节点元素,但是前者的c元素没有复用!
综上,如果有元素的key相同,必然会导致有节点在diff过程中不能得到复用,降低了patch的效率,如果该节点后面有很多层子节点,那么后面所有的子节点都需要重新创建
,而得到复用的节点继续往后updateChildren即可
。
依旧是举例说明,加入我们v-for一个数组
<div v-for="(item,index) in arr" :key="index">{{item}}</div>
arr = [1,2,3,4]
此时渲染出来的结果是
<div key="0">1</div>
<div key="1">2</div>
<div key="2">3</div>
<div key="3">4</div>
假如此时我们给这个数组中间插入一个5元素
arr.splice(2,0,5) // arr = [1,2,5,3,4]
此时渲染结果如下:
<div key="0">1</div>
<div key="1">2</div>
<div key="2">5</div>
<div key="3">3</div>
<div key="4">4</div>
让我们做个diff:
//旧节点 { '0':1, '1':2, '2':3, '3':4, }
//新节点 { '0':1, //复用 '1':2, //复用 '2':5, //无法复用 '3':3, //无法复用 '4':4, //无法复用 }
可以发现后面三个节点都无法复用,包括3和4两个相同节点。而实际上如果key是唯一的且不改变的,应该有4个节点都可以复用。这就是一般不用index的原因。在操作数组的过程中,节点和index之间的关系可能改变!导致节点的key值被修改
https://juejin.im/post/6844903933157048333
https://segmentfault.com/a/1190000013314893非常详细
简单来说:原理就是,当data被修改,setter被触发,Dep.notify()执行,watcher.update()执行,如果是同步更新,那么遇到一个data修改,就更新一次DOM,这样浏览器渲染开销很大。
所以提出异步更新的机制:每次watcher.update()都会把update推入一个队列queue,如果watcher已经在队列中,则不重复推入,保证没有重复的watcher。并且在第一次update的时候就执行nextTick函数。该函数会生成一个timerFunc函数,用来控制队列update的执行时间点。timerFunc的实现方式可以是promise、mutaionobserver、setImmediate、setTimeout。如果是涉及视图渲染的dom操作,会采用宏队列的方法,其余的会是微队列。也就是本轮事件循环所有微队列事件执行完毕后,会调用nextTick,进行dom修改。UI render则发生在每轮事件循环结束后,进行统一的视图渲染。
- webpack插件本质上是一个函数,它的原型上存在一个名为apply函数。webpack在初始化时 (在最早触发的environment事件之前) 会执行这个函数,并将一个包含了webpack所有配置信息的compiler作为参数传递给apply函数。
- 插件可以通过监听webpack本身触发的事件,在不同的时间阶段介入进行你想做的操作。
- 通过获取到的compiler对象,我们可以结合tapable在插件中自定义事件并将其广播。
- 在插件中监听一些特定的事件 (thisCompilation到afterEmit这个阶段的事件),你可以拿到一个compilation对象,里面包含了各种编译资源,你可以通过操作这个对象对生成的资源进行添加和修改等操作。
https://juejin.im/post/6844903909664931853
(1)
精简代码,清除无用代码
减少在代码包中直接嵌入的资源文件
图片放在cdn,使用适当的图片格式
(2)
分包
(3)
提前请求:在页面 onLoad 阶段就可以发起异步请求,不用等页面 ready。如果能在前置页面点击跳转时预请求当前页的核心异步请求,效果会更好;
善用缓存:对一些变动频率很低的异步数据进行缓存,下次启动时可以直接利用;
优化交互:在首屏渲染的期间,利用 loading 效果或展示骨架图,来缓解用户等待的焦虑。
(4)
与界面渲染无关的数据最好不要设置在 data 中,可以考虑设置在 page 对象的其他字段下;
不要过于频繁调用 setData,应考虑将多次 setData 合并成一次 setData 调用;
列表局部更新 在更新列表的某一个数据时。不要用 setData 进行全部数据的刷新。查找对应 id 的那条数据的下标(index是不会改变的),用 setData 进行局部刷新。
合理使用小程序组件 自定义组件的更新只在组件内部进行,不会影响页面其他元素。
首先要理解Vue的MVVM机制原理:
- data的每个属性,都对应一个Dep实例,且都设置了get和set劫持,get用于如果是视图渲染watcher(Dep.Target存在),会给watcher的deps推入该dep,且给该dep的sub推入该watcher
- watcher有三种类型:computed型、视图渲染型(异步)、其他(同步),三种类型的update处理不同
上一段watcher的update方法代码:
update(){
if (this.lazy) {//computed型
this.dirty = true
} else if (this.sync) {//其他(同步)
this.run()
} else {//视图渲染类型(异步)
queueWatcher(this)
}
}
从上述代码可以看出,computed型的watcher在update时,仅仅是把dirty赋值true
下面看下initComputed函数:
// 源码位置:/src/core/instance/state.js
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true }
)
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
从上述代码可以看出,initComputed中,会遍历每个computed属性,然后分别new一个Watcher实例,然后再defineComputed
defineComputed函数如下:
const noop = function() {}
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
){
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
总结就是用Object.defineProperty劫持computed的get属性,且get函数为createComputedGetter
核心
下面看createComputedGetter代码:
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 依赖更新,dirty会是true
if (watcher.dirty) {
// 重新获取值,并dirty=false
// 注意这里获取值的时候,调用了watcher的get方法,
// 这一步会收集computed的依赖,并往每个依赖的dep推入computed的watcher
// 因此在依赖更新时,触发watcher的update,对于computed的watcher,会设置dirty为true,
// 因为这一步是同步的,所以对于视图渲染的queueWatcher(异步),computed的get触发时必然会调用watcher.evaluate()获取最新值
watcher.evaluate()
}
// 视图渲染依赖了computed属性,Dep.target此时是视图渲染型watcher
if (Dep.target) {
// 遍历computed的watcher的deps,
// 把每个dep都推入Dep.target的deps中,且往每个dep的subs中都推入Dep.target(视图渲染型watcher),即收集依赖
// 此后,当computed的依赖更新,首先会触发同步的dirty=true,然后会触发queueWatcher,异步渲染视图。
// 至于谁先触发都无所谓,因为同步的先执行,然后再执行异步。即dep的subs中watcher的顺序无所谓。
watcher.depend()
}
return watcher.value
}
}
}