框架
MVVM
Model–View–ViewModel(MVVM) 是一个软件架构设计模式
View 层:View 是视图层,也就是用户界面。前端主要由 HTML 和 CSS 来构建,或者通过框架模板构建。View 层做的是 数据绑定的声明、 指令的声明、 事件绑定的声明。
Model 层:Model 是指数据模型,存储数据及对数据的处理如增删改查。
ViewModel:业务逻辑层,即视图数据层,作为视图的模型,为视图服务。
- view 和 Model 之间并没有直接的联系,而是通过 ViewModel 进行交互,Model 和 ViewModel 之间的交互是双向的, 因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上。
- ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理,实现了数据驱动。
开发者在 View 层的视图模板中声明 数据绑定、 事件绑定 后,在 ViewModel 中进行业务逻辑的 数据 处理。事件触发后,ViewModel 中 数据 变更, View 层自动更新。因为 MVVM 框架的引入,开发者只需关注业务逻辑、完成数据抽象、聚焦数据,MVVM 的视图引擎会帮你搞定 View。因为数据驱动,一切变得更加简单。
Virtual DOM
Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
简单来说,可以把 Virtual DOM 理解为一个简单的 JS 对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。 了避免不必要的 DOM 操作,虚拟 DOM 在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行 DOM 操作,从而避免操作其他无需改动的 DOM。
Virtual DOM 的优势
- 具备跨平台的优势。
由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
- 操作 DOM 慢,js 运行效率高。我们可以将 DOM 对比操作放在 JS 层,提高效率。
因为 DOM 操作的执行速度远不如 Javascript 的运算速度快,因此,把大量的 DOM 操作搬运到 Javascript 中,运用 patching 算法来计算出真正需要更新的节点,最大限度地减少 DOM 操作,从而显著提高性能。
- 提升渲染性能
Virtual DOM 的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
# Virtual DOM 真的比操作原生 DOM 快吗?
- 需要分情况回答:
- 初始渲染:Virtual DOM > 依赖收集
- 小量状态更新:依赖收集 >> Virtual DOM
- 大量状态更新:Virtual DOM > 依赖收集 虚拟 Dom 的出现并不总是为了帮助应用更快,而是为了追求更重要的好处,提供过的去的性能。 。并且随着大型应用承载的状态越来越复杂,这种占用 js 主线程的运行时 diff 会造成比较严重的页面掉帧与卡顿,也需要对这种技术进行优化。
参考: www.zhihu.com/question/31… mp.weixin.qq.com/s/GbJXU15EM…
React
React为了践行“构建快速响应的大型 Web 应用程序”理念做出的努力。
其中的关键是解决CPU的瓶颈与IO的瓶颈。而落实到实现上,则需要将同步的更新变为可中断的异步更新。
- 在浏览器每一帧的时间中,预留一些时间给JS线程,
React利用这部分时间更新组件
React15架构
React15架构可以分为两层:
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
React15架构的缺点
在Reconciler中,mount的组件会调用mountComponent,update的组件会调用updateComponent。这两个方法都会递归更新子组件。
递归更新的缺点
由于递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。
React16架构
React16架构可以分为三层:
- Scheduler(调度器,React16新增)—— 调度任务的优先级,高优任务优先进入Reconciler
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
Scheduler(调度器)
既然我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。
其实部分浏览器已经实现了这个API,即requestIdleCallback。但是由于以下因素,React放弃使用:
- 浏览器兼容性
- 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的
requestIdleCallback触发的频率会变得很低
基于以上原因,React实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。
Reconciler(协调器)
在React15中Reconciler是递归处理虚拟DOM的,React16的更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间。
当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记。 整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。
Renderer(渲染器)
Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。
React 技术揭秘:react.iamkasong.com/preparation…
函数式组件、calss组件
Hooks
"hooks" 直译是 “钩子”,指:
系统运行到某一时期时,会调用被注册到该时机的回调函数。
以 react 为例,hooks 是:
一系列以
“use”作为开头的方法,它们提供了让你可以完全避开class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。
而在 vue 中, hooks 的定义可能更模糊,姑且总结一下:
在
vue组合式API里,以“use”作为开头的,一系列提供了组件复用、状态管理等开发能力的方法。
mixins的弊端
- 难以追溯的方法与属性
- 属性、方法的覆盖、同名
mixins复用的复杂性高,可读性低。
Hooks的优点
- 解决了mixins的追溯、覆盖、复用弊端
- 组织方式的混乱程度变低,做到高度聚合
- 一个页面中,N件事情的代码在一个组件内互相纠缠确实是在
Hooks出现之前非常常见的一种状态。Hooks写法都能做到,将“分散在各种声明周期里的代码块”,通过Hooks的方式将相关的内容聚合到一起。
- 一个页面中,N件事情的代码在一个组件内互相纠缠确实是在
- 代码可阅读性提升,易理解
- 友好的渐进式
-你依然可以在项目里一边写
class组件,一边写Hooks组件,在项目的演进和开发过程中,这是一件没有痛感,却悄无声息改变着一切的事情。
vue 和 react 自定义 Hook 的异同
-
相似点: 总体思路是一致的 都遵照着 "定义状态数据","操作状态数据","隐藏细节" 作为核心思路。
-
差异点:
组合式API和React函数组件有着本质差异
vue3的组件里,setup是作为一个早于 “created” 的生命周期存在的,无论如何,在一个组件的渲染过程中只会进入一次。
React函数组件则完全不同,如果没有被memorized,它们可能会被不停地触发,不停地进入并执行方法,因此需要开销的心智相比于vue其实是更多的。
异步setState
React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会立即更新。
setState需要实现的两个功能:
- 异步更新state,将短时间内的多个setState合并成一个
- 为了解决异步更新导致的问题,增加另一种形式的setState:接受一个函数作为参数,在函数中可以得到前一个状态并返回下一个状态 实现步骤:
- setState队列,为了合并setState,我们需要一个队列来保存每次setState的数据,然后在一段时间后,清空这个队列并渲染组件
- 清空队列,先合并state再利用js的事件队列机制延迟执行; 队列为空时在下一个事件循环清空队列,这样在本次事件循环内添加的任务会进入下一个事件循环执行。
- 渲染组件我们需要另一个队列保存所有组件,不同之处是,这个队列内不会有重复的组件
const queue = [];
const renderQueue = [];
function enqueueSetState( stateChange, component ) {
// console.log('enqueueSetState',queue.length)
// 如果queue的长度是0,也就是在上次flush执行之后第一次往队列里添加
if ( queue.length === 0 ) {
defer( flush );
}
queue.push( {
stateChange,
component
} );
// 如果renderQueue里没有当前组件,则添加到队列中
if ( !renderQueue.some( item => item === component ) ) {
renderQueue.push( component );
}
}
function flush() {
let item;
// 遍历
while( item = queue.shift() ) {
const { stateChange, component } = item;
// 如果没有prevState,则将当前的state作为初始的prevState
if ( !component.prevState ) {
component.prevState = Object.assign( {}, component.state );
}
// 如果stateChange是一个方法,也就是setState的第二种形式
if ( typeof stateChange === 'function' ) {
let newState=stateChange( component.prevState, component.props )
Object.assign( component.state, newState );
} else {
// 如果stateChange是一个对象,则直接合并到setState中
Object.assign( component.state, stateChange );
}
component.prevState = component.state;
}
// 渲染每一个组件
while( component = renderQueue.shift() ) {
renderComponent( component );
}
}
function defer( fn ) {
return Promise.resolve().then( fn );
}
function renderComponent(){
console.log('renderComponent')
}
// demo
let component1={state:{num:0}}
function setState( stateChange ) {
enqueueSetState( stateChange, component1 );
}
for ( let i = 0; i < 100; i++ ) {
setState( prevState => {
console.log('prevState num:', prevState.num ,i);
return {
num: i
}
} );
}
参考: mp.weixin.qq.com/s?__biz=Mzg…
VUE
生命周期
ustbhuangyi.github.io/vue-analysi…
Vue 的响应式原理中 Object.defineProperty 的缺陷
- 深度遍历:Object.defineProperty 只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果,属性值是对象,还需要深度遍历。
- 对数组的监听会有性能问题
- Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。 Vue 官方的描述不准确:由于 JavaScript 的限制, Vue 不能检测以下变动的数组:
当你利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue 当你修改数组的长度时,例如: vm.items.length = newLength
Object.defineProperty 可以检测数组属性的变化的,但是动态添加的无法监听到。
let arr = [1,2,3]
arr.forEach((item,index)=>{
Object.defineProperty(arr,index,{
set:function(val){
console.log('set',val)
item = val
},
get:function(val){
console.log('get',val)
return item
}
})
})
arr[1];
arr[1] = 1; // 可以检测
arr[4] = 4; // 无法检测
Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性和数组的长度变化。
let arr=[1,2,3]
let proxy = new Proxy(arr, {
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});
proxy[0]=0
proxy[3]=4
proxy.length=6
nextTick 原理
- nextTick每次将拿到的回调函数存放到数组中,初始时没有正在执行回调函数,就会开启异步任务函数来清空回调队列,并改变执行状态(padding),不再启动异步任务直至本轮任务完成。
- 后续已开始执行清空任务,再调nextTick时回调函数会被放进回调函数队列,等待事件循环的完成时清空队列执行回调函数,重置状态。这样可以保证在同一个 Tick 内多次执行
nextTick,不会开启多个异步任务,而把这些异步任务都压成一个同步任务,在下一个 Tick 执行完毕。 - 异步函数会寻找宏任务或者微任务函数,依赖环境选择setTimeout、setImmediate、MessageChannel、postMessage、MutationObsever 等方法实现,然后异步去执行回调函数队列。
let callbacks = [];
let pending = false;
function nextTick(cb, ctx) {
let _resolve;
// 0. 加入回调函数队列,开启异步任务
// 2. 加入队列,等待异步任务完成时清空
callbacks.push(() => {
if (cb) {
cb.call(ctx);
} else if (_resolve) {
_resolve(ctx);
}
});
// 1. 初始时,开启异步任务以清空队列
if (!pending) {
pending = true;
Promise.resolve().then(flushCallbacks);
}
if (!cb) {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}
function flushCallbacks() {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
参考: blog.csdn.net/qq_42072086…
响应式原理
defineReactive
defineReactive 函数最开始初始化 Dep 对象的实例,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。
/**
* Define a reactive property on an Object.
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep();
const property = Object.getOwnPropertyDescriptor(obj, key);
// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// watcher.get 会pushTarget
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
// ...
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
Watcher
export default class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// ... options
if (typeof expOrFn === "function") {
this.getter = expOrFn;
}
this.value = this.lazy ? undefined : this.get();
}
get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
//.. handleError
} finally {
if (this.deep) {
traverse(value);
}
popTarget();
// ... cleanupDeps
}
return value;
}
addDep(dep) {
//...
dep.addSub(this);
}
update() {
if (this.lazy) {
// 表示需要重新计算
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
run() {
if (this.active) {
const value = this.get();
if (value !== this.value || isObject(value) || this.deep) {
// set new value
const oldValue = this.value;
this.value = value;
this.cb.call(this.vm, value, oldValue);
}
}
}
// ComputedGetter 中调用,dirty为true后重新计算
// dirty 为false时 ComputedGetter会返回 watcher.value
evaluate() {
// watcher.value 缓存计算的值
this.value = this.get();
this.dirty = false;
}
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();·
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown() {}
}
渲染 watcher
当对数据对象的访问会触发他们的 getter 方法,那么这些对象什么时候被访问呢? Vue 的 mount 过程是通过 mountComponent 函数实现,其中有一段比较重要的逻辑,大致如下:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
以上的 watcher 就是渲染 watcher,当我们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑:
watcher.get()->pushTarget(this)->Dep.target=watcher
依赖收集、派发更新
渲染的时候会对数据进行访问,就触发了 getters,Dep.target 已经被赋值为渲染 watcher,那么就执行到 dep.depend 方法进行依赖收集:
dep.depend->Dep.target.addDep(this)->dep.addSub(watcher)
数据更的时候会触发 setter:dep.notify->subs.update()
export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean
) {
// ...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// ...
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}
Dep
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
computed 的实现(computed watcher)
computed 的实现逻辑简化如下:
function initComputed(vm, computed) {
const watchers = (vm._computedWatchers = Object.create(null));
for (const key in computed) {
const userDef = computed[key];
const getter = typeof userDef === "function" ? userDef : userDef.get;
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{lazy:true}
);
}
if (!(key in vm)) {
defineComputed(vm, key, userDef);
}
}
}
export function defineComputed(target, key, userDef) {
if (typeof userDef === "function") {
sharedPropertyDefinition.get = createComputedGetter(key);
sharedPropertyDefinition.set = noop;
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
以上的 watcher.depend 中的 watcher 就是 computed watcher watcher.depend 流程: watcher.depend->deps[i].depend() deps[i].depend()就是依赖收集: Dep.target.addDep(this)-> dep.addSub(watcher)
evaluate 流程: watcher.get();dirty=false ->pushTarget(this) 数据更新时: setter -> dep.notiry -> watcher.update;dirty=true
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
Computed 如何控制缓存
首先 computed 计算后,会把计算得到的值保存到一个变量(watcher.value)中。读取 computed 并使用缓存时,就直接返回这个变量。当 computed 更新时,就会重新赋值更新这个变量。
TIP:computed 计算就是调用你设置的 get 函数,然后得到返回值。
computed 控制缓存的重要一点是 【脏数据标志位 dirty】 dirty 是 watcher 的一个属性。
当 dirty 为 true 时,读取 computed 会执行 get 函数,重新计算。
当 dirty 为 false 时,读取 computed 会使用缓存。
依赖的 data 变化,computed 如何更新?
- 被依赖通知更新后,重置 脏数据标志位 ,页面读取 computed 时再更新值。
- data 改变,正序遍历通知,computed 先更新,页面再更新。
- 页面更新时,会重新读取 computed 的值。此时,由于 dirty = true, 执行 computed - evaluate 方法,重新计算 computed。
参考: zhuanlan.zhihu.com/p/357250216
Vue3 Effect
vue3 effect zhuanlan.zhihu.com/p/95012874
Vue3源码分析
Vue Diff算法
为了降低算法复杂度,React的diff会预设三个限制:
- 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。
- 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
- 开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。
patch方法
对比当前同层的虚拟节点是否为同一种类型的标签(同一类型的标准,下面会讲):
- 是:继续执行
patchVnode方法进行深层比对 - 否:没必要比对了,直接整个节点替换成
新虚拟节点
来看看patch的核心原理代码
function patch(oldVnode, newVnode) {
// 比较是否为一个类型的节点
if (sameVnode(oldVnode, newVnode)) {
// 是:继续进行深层比较
patchVnode(oldVnode, newVnode)
} else {
// 否
const oldEl = oldVnode.el // 旧虚拟节点的真实DOM节点
const parentEle = api.parentNode(oldEl) // 获取父节点
createEle(newVnode) // 创建新虚拟节点对应的真实DOM节点
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
// 设置null,释放内存
oldVnode = null
}
}
return newVnode
}
sameVnode方法
patch关键的一步就是sameVnode方法判断是否为同一类型节点,那问题来了,怎么才算是同一类型节点呢?这个类型的标准是什么呢?
咱们来看看sameVnode方法的核心原理代码,就一目了然了
function sameVnode(oldVnode, newVnode) {
return (
oldVnode.key === newVnode.key && // key值是否一样
oldVnode.tagName === newVnode.tagName && // 标签名是否一样
oldVnode.isComment === newVnode.isComment && // 是否都为注释节点
isDef(oldVnode.data) === isDef(newVnode.data) && // 是否都定义了data
sameInputType(oldVnode, newVnode) // 当标签为input时,type必须是否相同
)
}
patchVnode方法
这个函数做了以下事情:
- 找到对应的
真实DOM,称为el - 判断
newVnode和oldVnode是否指向同一个对象,如果是,那么直接return - 如果他们都有文本节点并且不相等,那么将
el的文本节点设置为newVnode的文本节点。 - 如果
oldVnode有子节点而newVnode没有,则删除el的子节点 - 如果
oldVnode没有子节点而newVnode有,则将newVnode的子节点真实化之后添加到el - 如果两者都有子节点,则执行
updateChildren函数比较子节点,这一步很重要
updateChildren方法
- 头尾交叉比较,找到相同节点
- 如果匹配不到,再把所有旧子节点的
key做一个映射到旧节点下标的key -> index表,然后用新vnode的key去找出在旧节点中可以复用的位置。
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
列表组件中 Key 的作用
官网描述: 当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。 为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key attribute。
- 没有绑定 key 的情况下,并且在遍历模板简单的情况下,会导致虚拟新旧节点对比更快,节点也会复用。而这种复用是就地复用,适用范围有限制,只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
- key 是给每一个 vnode 的唯一 id,可以依靠 key,更准确, 更快的拿到 oldVnode 中对应的 vnode 节点。 在交叉对比中,当新节点跟旧节点头尾交叉对比没有结果时,会根据新节点的 key 去对比旧节点数组中的 key,从而找到相应旧节点(这里对应的是一个 key => index 的 map 映射)。如果没找到就认为是一个新增节点。而如果没有 key,那么就会采用遍历查找的方式去找到对应的旧节点。一种一个 map 映射,另一种是遍历查找。相比而言。map 映射的速度更快。
Keep-alive
export default {
name: "keep-alive",
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number],
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this;
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache;
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
};
keys.push(keyToCache);
// ... remove key
this.vnodeToCache = null;
}
},
},
created() {
this.cache = Object.create(null);
this.keys = [];
},
mounted() {
this.cacheVNode(); },
updated() {
this.cacheVNode();
},
render() {
const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot);
const componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
// check pattern
const name = getComponentName(componentOptions);
const { include, exclude } = this;
if ((include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))) return vnode;
const { cache, keys } = this;
const key =
vnode.key == null
? componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// LRU:删除后再更新到前面
remove(keys, key);
keys.push(key);
} else {
// delay setting the cache until update
this.vnodeToCache = vnode;
this.keyToCache = key;
}
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0]);
},
};
Render逻辑:
-
获取keep-alive包裹着的第一个子组件对象及其组件名;
-
根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
-
根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该
key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步; -
在
this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。 -
最后并且很重要,将该组件实例的
keepAlive属性值设置为true。 -
组件更新后,缓存组件(cache[key].componentInstance),更新key值
-
再次访问被包裹组件时,
vnode.componentInstance的值就是已经缓存的组件实例,那么会执行insert(parentElm, vnode.elm, refElm)逻辑,这样就直接把上一次的DOM插入到了父元素中。
// src/core/vdom/patch.js
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
let i = vnode.data
if (isDef(i)) {
const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
参考 ustbhuangyi.github.io/vue-analysi…
Vue3
- Performance:性能优化 新的响应式api:reactive、ref 响应式侦听:watch、watchEffect
数据绑定建立响应:
-
初始化阶段: 初始化阶段通过组件初始化方法形成对应的proxy对象,然后形成一个负责渲染的effect。
-
get依赖收集阶段:通过解析template,替换真实data属性,来触发get,然后通过track方法,将proxy对象和key形成对应的deps,将负责渲染的effect存入deps。(这个过程还有其他的effect,比如watchEffect存入deps中 )。
-
set派发更新阶段:当我们 this[key] = value 改变属性的时候,首先通过trigger方法,通过proxy对象和key找到对应的deps,然后给deps分类分成computedRunners和effect,然后依次执行,如果需要调度的,直接放入调度。
相对于vue2 的提升: 在vue2.0的时候。响应式是在初始化的时候就深层次递归处理了 但是
与vue2.0不同的是,即便是深度响应式我们也只能在获取上一级get之后才能触发下一级的深度响应式。
-
Tree-shaking support:支持摇树优化
- 在2.x版本中,很多函数都挂载在全局Vue对象上,比如nextTick、nextTick、nextTick、set等函数,因此虽然我们可能用不到,但打包时只要引入了vue这些全局函数仍然会打包进bundle中。而在Vue3中,所有的API都通过ES6模块化的方式引入,这样就能让webpack或rollup等打包工具在打包时对没有用到API进行剔除,最小化bundle体积。
-
Composition API:组合API
Options API就是将同一类型的东西放在同一个选项中,但是随着数据增多,我们维护的功能点会涉及到多个data和method,导致了组件难以理解和阅读。
Composition API做的就是把同一功能的代码放到一起维护,这样我们需要维护一个功能点的时候,不用去关心其他的逻辑,只关注当前的功能
// context:{attrs,slots,emit}
export default { setup(props, context) {} };
-
Fragment,Teleport,Suspense:新增的组件
-
Better TypeScript support:更好的TypeScript支持
-
Custom Renderer API:自定义渲染器
-
非兼容的功能
- 移除filter
- v-model改动:modelValue,update:modelValue
reactive
export function reactive(raw) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key)
// TODO 依赖收集
// track(target, key)
return res
},
set(target, key, value) {
const res = Reflect.set(target, key, value)
// TODO 触发依赖
// trigger(target, key, value)
return res
}
})
}
依赖收集 & 触发依赖
依赖收集我们将它封装为一个 track 函数,在触发代理对象的 get 拦截器的时候 去收集依赖
触发依赖我们将它封装为一个 trigger 函数,在触发代理对象的 set 拦截器的时候去 触发依赖
这里首先要依赖一个 副作用函数产生的 activeEffect
依赖收集:
我们想要收集依赖,得知道是哪个对象(target) 的哪个 key 吧?还得知道对应这个 key 有哪些依赖。
这里我们采用的方法是:
- 利用 全局 WeakMap 来保存所有对象
- 利用 Map 来保存对象中所有 key
- 利用 Set 来保存 key 中的依赖
依赖执行:
依赖的执行就比较简单了,就是根据传入的 target 和 key 去取出所有的 ReactiveEffect 实例并执行方法
// 最外层用来保存每一个 target 的 weakMap
const targetMap = new WeakMap()
/**
* get 依赖收集函数
* @param target 传入的对象
* @param key 对应属性的 key
*/
export function track(target, key) {
// 拿到当前 target 对应的 map (每个对应的 target 底下应该保存着自己的 key 的 map)
let depsMap = targetMap.get(target)
// 拿不到,说明需要新建一个 map 并存入 weakMap
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
// 拿到当前 key 的对应的 set (每个对应 key 底下应该保存着自己的 set, set里边是所有的依赖 ReactiveEffect)
let dep = depsMap.get(key)
// 拿不到,说明需要创建一个新的 set 并存入对应的 map
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
// 已经在 dep 中就不用 add 了
if (dep.has(activeEffect)) return
dep.add(activeEffect) // 把对应的 ReactiveEffect 实例加入 set 里
}
/**
* set 触发依赖
* @param target 传入的对象
* @param key 对应属性的 key
*/
export function trigger(target, key) {
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
// 找到对应的 ReactiveEffect 实例,执行他们的 run 方法
for (const effect of dep) {
effect.run()
}
}
Vue3 自定义渲染器
抽离浏览器特有的API
将操作DOM的API作为配置项,作为createRenderer函数的参数传递。这样就可以在函数内部通过配置项来获取操作DOM的API。
const render = createRenderer({
// 用于创建元素
createElement(tag) {
return document.createElement(tag);
},
// 用于设置元素的文本节点
setElementText(el, text) {
el.textContent = text;
},
// 用于在给定parent下添加指定元素
insert(el, parent, anchor = null) {
parent.insertBefore(el, anchor);
},
});
function createRenderer(options) {
const { createElement, setElementText, insert } = options
function mountElement(vnode, container) {}
// 更新打补丁
function patch(){}
function render(){}
return {
render
}
}
参考:
inject/provide
Vue中, Provide/Inject实现了跨组件的通信。但可惜数据并不是响应式的(设计如此)。
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的
可能之所以这样设计,是为了避免数据的混乱。就如同props不能被子组件直接修改一样。
vue组件通信方式
- props / $emit 适用 父子组件通信
- ref 与parent/children 适用 父子组件通信
- EventBus (emit/on) 适用于 父子、隔代、兄弟组件通信
- attrs/listeners 适用于 隔代组件通信
- provide / inject 适用于 隔代组件通信
- Vuex 适用于 父子、隔代、兄弟组件通信
Vue-router
hash模式
hash模式是vue-router的默认路由模式,它的标志是在域名之后带有一个#
通过window.location.hash获取到当前url的hash;hash模式下通过hashchange方法可以监听url中hash的变化
history模式
基于HTML5的history对象。通过pushState和replaceState方法可以修改url地址,结合popstate事件监听url中路由的变化。History API可以的pushState和replaceState方法可以在改变页面 url 的情况下不刷新页面
history模式需要服务端进行支持,将路由都重定向到根路由
实现原理
-
Router install
- mixin: 在组件实例化前beforeCreate阶段,并执行初始化
- 注册全局组件router-view,用来渲染路由组件
-
Router类收集数据,确定路由类型,包括路由模式mode、路由表routes。
-
监听路由变化
- hash:hashchange,获取到当前的path
- history: 添加popstate监听,可以监听到浏览器前进、后退。但不能监听到pushState、replaceState,所以在执行方法时需要修改path。
-
router-view渲染。拿到path,对所有路由信息筛选,找到对应的组件,作为内容。
- 创建vue实例监听实例path的响应式变化后进行渲染
参考:
Vuex
为什么需要在actions中做异步操作, mutation只能做同步操作
- devtool是跟着mutation走的,mutation走一步devtool记录一步,如果在mutation中加入异步,devtool记录到异步函数的时候异步函数还没调用,devtool不能跟踪数据的变化
- actions里可以返回promise判断异步状态,从而解决状态依赖问题
- 事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,只要最后触发 mutation 就行,异步竞态怎么处理那是用户自己的事情。
参考: mp.weixin.qq.com/s?__biz=Mzg…
SolidJS
- 直接使用浏览器的 DOM, 没有 虚拟DOM, DOM diff 一整套算法 ,可以发现它编译出来的代码,他的 DOM ,是原生 DOM ,其他的语法是直接调用的,也没有那一整套复杂的虚拟 DOM。
- 提前编译,按需打包 ,无论是 react 还是 vue ,不管怎么编译,都需要引入框架本身。也就是 runtime 。而且这个体积还比较大。这些框架都采用的是用运行时方案,运行时方案相比于编译时方案,最大的优势是可以「几乎没有任何语法约束」的去完成代码编写。而 Solid 则预编译,将 jsx 部分的代码,转换成原生的语法。
- 响应式原理,精准更新对应的值 ,如果了解 React 的原理,就会知道,只要是 props 或者 state 改变,React 组件就会重新渲染,而每一次判断是否会重新改变,值是否不一样,也是一整套算法…… 而 Solid 不一样,他另辟蹊径,每一个组件都是一个独立的线程,每个组件里的 createMemo 或 createEffect 里面去收集对应的依赖, 在 set 改变值后,都会重新执行这些方法。看起来就像是实时更新了一样。
React SSR
zhuanlan.zhihu.com/p/157214413…
同构
同构 就是同一套代码多端运行,服务端运行一遍,客户端运行一遍,服务端完成html元素的渲染,客户端完成元素事件的绑定。 在同构过程中,我们使用的方法是ReactDom.hydrate,而不是使用的ReactDom.render,这是由于在服务端渲染过程中,会给根元素加上data-reactroot这个属性,在客户端渲染过程中会使用hydrate方法复用渲染好的结构,然后加上事件即可,详情可见hydrate
ReactDom.hydrate(<App />, document.getElementById('root'))
渲染流程:
- 服务端运行react返回html字符串
- 客户端接收html并显示
- 客户端加载js
- js执行并接管页面操作
Taro
Taro原理
模拟运行时
Taro 3 则可以大致理解为解释型架构(相对于 Taro 1/2 而言),主要通过在小程序端模拟实现 DOM、BOM API 来让前端框架直接运行在小程序环境中,从而达到小程序和 H5 统一的目的,而对于生命周期、组件库、API、路由等差异,依然可以通过定义统一标准,各端负责各自实现的方式来进行抹平。而正因为 Taro 3 的原理,在 Taro 3 中同时支持 React、Vue 等框架,甚至还支持了 jQuery,还能支持让开发者自定义地去拓展其他框架的支持。
适配器
Taro 3 之后 ⼩程序端的整体架构。⾸先是⽤户的 React 或 Vue 的代码会通过 CLI 进⾏ Webpack 打包,其次在运⾏时会提供 React 和 Vue 对应的适配器进⾏适配,然后调⽤Taro提供的 DOM 和 BOM API, 最后把整个程序渲染到所有的⼩程序端上⾯。
Taro实现了taro-react 包,用来连接 react-reconciler 和 taro-runtime 的 BOM/DOM API。是基于 react-reconciler 的小程序专用 React 渲染器,连接 @tarojs/runtime的DOM 实例,相当于小程序版的react-dom,暴露的 API 也和react-dom 保持一致。
自定义渲染器
创建一个自定义渲染器只需两步:具体的实现主要分为两步:
第一步: **实现宿主配置( 实现**react-reconciler 的 hostConfig**配置)这是react-reconciler要求宿主提供的一些适配器方法和配置项。这些配置项定义了如何创建节点实例、构建节点树、提交和更新等操作。即在 hostConfig 的方法中调用对应的 Taro BOM/DOM 的 API。
第二步:实现渲染函数,类似于ReactDOM.render() 方法。可以看成是创建 Taro DOM Tree 容器的方法。
Taro DOM Tree
渲染器会生成 Taro DOM Tree。那 Taro DOM Tree 怎样更新到页面呢?
因为⼩程序并没有提供动态创建节点的能⼒,需要考虑如何使⽤相对静态的 wxml 来渲染相对动态的 Taro DOM 树。Taro使⽤了模板拼接的⽅式,根据运⾏时提供的 DOM 树数据结构,各 templates 递归地 相互引⽤,最终可以渲染出对应的动态 DOM 树。
首先,将小程序的所有组件挨个进行模版化处理,从而得到小程序组件对应的模版。如下图就是小程序的 view 组件模版经过模版化处理后的样子。⾸先需要在 template ⾥⾯写⼀个 view,把它所有的属性全部列出来(把所有的属性都列出来是因为⼩程序⾥⾯不能去动态地添加属性)。
接下来是遍历渲染所有⼦节点,基于组件的 template,动态 “递归” 渲染整棵树。
具体流程为先去遍历 Taro DOM Tree 根节点的子元素,再根据每个子元素的类型选择对应的模板来渲染子元素,然后在每个模板中我们又会去遍历当前元素的子元素,以此把整个节点树递归遍历出来。
Taro H5
Taro 这边遵循的是以微信小程序为主,其他小程序为辅的组件与 API 规范。但浏览器并没有小程序规范的组件与 API 可供使用,我们不能在浏览器上使用小程序的 view 组件和 getSystemInfo API。因此Taro在 H5 端实现一套基于小程序规范的组件库和 API 库。
再来看⼀下 H5 端的架构,同样的也是需要把⽤户的 React 或者 Vue 代码通过 Webpack 进⾏打包。然后在运⾏时做了三件事情:第⼀件事情是实现了⼀个组件库,组件库需要同时给到 React 、Vue 甚⾄更加多的⼀些框架去使⽤,Taro使⽤了 Stencil 去实现了⼀个基于 WebComponents 且遵循微信⼩程序规范的组件库,第⼆、三件事是实现了⼀个⼩程序规范的 API 和路由机制,最终就可以把整个程序给运⾏在浏览器上⾯。
性能优化
对小程序的性能影响较大的有两个因素,分别是 setData 的数据量和单位时间 setData 函数的调用次数。在 Taro 中,会对 setData 做 batch 捆绑更新操作,因此更多时候只需要考虑 setData 的数据量大小问题。
- batch update,为了提高更新效率,Taro可以通过batch update的方式将多次更新合并在一起
- 删除楼层节点需要谨慎处理,Taro3 目前对节点的删除处理是有缺陷的,会影响兄弟元素的更新,需要隔离删除(Taro在修复)
- 基础组件的属性尽量保持引用。每次渲染时,React 会对基础组件的属性做浅对比,如果发现属性的引用不同,就会去更新组件属性。最后导致
setData次数增多、setData数据量增大。可以通过state、闭包等手段保持对象的引用。 - 小程序基础组件尽量不要挂载额外属性,目前这些额外属性会被一并进行
setData(Taro v3.1 将会自动过滤这些额外属性) - 开发者可以使用
Taro.preload()方法实现跳转预加载,再通过getCurrentInstance().preloadData获取预加载的数据 - 建议Taro.getCurrentInstance() 的结果保存下来,但频繁调用它可能会导致问题
- 全局配置项 baseLevel。 DOM 结构超过 N 层后,会使用原生自定义组件进行渲染。
- Taro3 使用小程序的
template进行渲染,一般情况下并不会使用原生自定义组件。这会导致一个问题,所有的setData更新都是由页面对象调用,如果我们的页面结构比较复杂,更新的性能就会下降。
- Taro3 使用小程序的
- CustomWrapper 组件。创建一个原生自定义组件,对后代节点的
setData将由此自定义组件进行调用,达到局部更新的效果。
虚拟列表
因为 DOM 元素的创建和渲染需要的时间成本很高,在大数据的情况下,完整渲染列表所需要的时间不可接收。其中一个解决思路就是在任何情况下只对「可见区域」进行渲染,可以达到极高的初次渲染性能。
实现虚拟列表思路:
- 实现虚拟列表的 HTML、CSS 结构
- 容器元素使用相对定位,定高,overflow: auto;
- 使用一个不可见元素(.list-view-phantom)撑起这个列表,让列表的滚动条出现
- 列表的可见元素(.list-view-content)使用绝对定位,以便通过transform定位到可视区
- 数据渲染
- 计算当前可见区域起始数据的 startIndex
- 计算当前可见区域结束数据的 endIndex
- 计算当前可见区域的数据data.slice(start, end),并渲染到页面中
- 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上
其他处理:
- 动态计算列表项高度
- 增加高度计算函数
- 缓存计算结果
代码:
<template>
<div
class="list-view"
style="height:500px;overflow:auto;"
@scroll="handleScroll">
<div
class="list-view-phantom"
:style="{
height: contentHeight
}">
</div>
<div
ref="content"
class="list-view-content">
<div
class="list-view-item"
:style="{
height: itemHeight + 'px'
}"
v-for="item in visibleData">
{{ item.value }}
</div>
</div>
</div>
</template>
export default {
name: 'ListView',
props: {
data: {
type: Array,
required: true
},
itemHeight: {
type: Number,
default: 30
}
},
computed: {
contentHeight() {
return this.data.length * this.itemHeight + 'px';
}
},
mounted() {
this.updateVisibleData();
},
data() {
return {
visibleData: []
};
},
methods: {
updateVisibleData(scrollTop) {
scrollTop = scrollTop || 0;
const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight);
const start = Math.floor(scrollTop / this.itemHeight);
const end = start + visibleCount;
this.visibleData = this.data.slice(start, end);
this.$refs.content.style.webkitTransform = `translate3d(0, ${ start * this.itemHeight }px, 0)`;
},
handleScroll() {
const scrollTop = this.$el.scrollTop;
this.updateVisibleData(scrollTop);
}
}
}
zhuanlan.zhihu.com/p/34585166?…
预渲染
Prerender 是由 Taro CLI 提供的在小程序端提高页面初始化渲染速度的一种技术,它的实现原理和服务端渲染(Server-side Rendering)一样:将页面初始化的状态直接渲染为无状态(dataless)的 wxml,在框架和业务逻辑运行之前执行渲染流程。经过 Prerender 的页面初始渲染速度通常会和原生小程序一致甚至更快。
和所有技术一样,Prerender 并不是银弹,使用 Prerender 之后将会有以下的 trade-offs 或限制:
- 页面打包的体积会增加。Prerender 本质是一种以空间换时间的技术,体积增加的多寡取决于预渲染 wxml 的数量。
- 在 Taro 运行时把真实 DOM 和事件挂载之前(这个过程在服务端渲染被称之为
hydrate),预渲染的页面不会相应任何操作。 - Prerender 不会执行例如
componentDidMount()(React)/ready()(Vue) 这样的生命周期,这点和服务端渲染一致。如果有处理数据的需求,可以把生命周期提前到static getDerivedStateFromProps()(React) 或created()(Vue)。
参考: taro-docs.jd.com/taro/docs/p…
编译优化
缓存与并行是进行性能优化的两个重要切入角度
借助 cache-loader、thread-loader 开发的 taro-plugin-compiler-optimization插件可以让 Taro 项目的编译时长减少为原来的三分之一。
解决包体积过大无法进行预览的问题
通过在开发环境下配置压缩指定文件,解决了小程序端包体积过大无法进行预览的问题
参考: taro-docs.jd.com/taro/docs/c…
微信小程序原理
微信小程序的框架包含两部分View视图层、App Service逻辑层,View层用来渲染页面结构,AppService层用来逻辑处理、数据请求、接口调用,它们在两个进程(两个Webview)里运行。 视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理。
所有的小程序基本都最后都被打成上面的结构
1、WAService.js 框架JS库,提供逻辑层基础的API能力\
2、WAWebview.js 框架JS库,提供视图层基础的API能力
3、WAConsole.js 框架JS库,控制台
4、app-config.js 小程序完整的配置,包含我们通过app.json里的所有配置,综合了默认配置型
5、app-service.js 我们自己的JS代码,全部打包到这个文件
6、page-frame.html 小程序视图的模板文件,所有的页面都使用此加载渲染,且所有的WXML都拆解为JS实现打包到这里
7、pages 所有的页面,这个不是我们之前的wxml文件了,主要是处理WXSS转换,使用js插入到header区域。
小程序技术实现
小程序的UI视图和逻辑处理是用多个webview实现的,逻辑处理的JS代码全部加载到一个Webview里面,称之为AppService,整个小程序只有一个,并且整个生命周期常驻内存,而所有的视图(wxml和wxss)都是单独的Webview来承载,称之为AppView。所以一个小程序打开至少就会有2个webview进程,正式因为每个视图都是一个独立的webview进程,考虑到性能消耗,小程序不允许打开超过5个层级的页面,当然同是也是为了体验更好。
AppService
可以理解AppService即一个简单的页面,主要功能是负责逻辑处理部分的执行,底层提供一个WAService.js的文件来提供各种api接口,主要是以下几个部分:
- 消息通信封装为WeixinJSBridge
- 日志组件Reporter封装
- wx对象下面的api方法
- 全局的App,Page,getApp,getCurrentPages等全局方法
- 对AMD模块规范的实现
然后整个页面就是加载一堆JS文件,包括小程序配置config,上面的WAService.js(调试模式下有asdebug.js),剩下就是我们自己写的全部的js文件,一次性都加载。
AppView
可以理解为h5的页面,提供UI渲染,底层提供一个WAWebview.js来提供底层的功能,具体如下:
- 消息通信封装为WeixinJSBridge
- 日志组件Reporter封装
- wx对象下的api
- 小程序组件实现和注册
- VirtualDOM,Diff和Render UI实现
- 页面事件触发
在此基础上,AppView有一个html模板文件,通过这个模板文件加载具体的页面,这个模板主要就一个方法,$gwx,主要是返回指定page的VirtualDOM,而在打包的时候,会事先把所有页面的WXML转换为ViirtualDOM放到模板文件里,而微信自己写了2个工具wcc(把WXML转换为VirtualDOM)和wcsc(把WXSS转换为一个JS字符串的形式通过style标签append到header里)。
Service和View通信
使用消息publish和subscribe机制实现两个Webview之间的通信,实现方式就是统一封装一个WeixinJSBridge对象,而不同的环境封装的接口不一样(开发环境为window.postMessage, IOS下为WKWebview的window.webkit.messageHandlers.invokeHandler.postMessage,android下用WeixinJSCore.invokeHandler)
参考: