前端知识体系-框架

1,260 阅读23分钟

框架

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打上代表增/删/更新的标记。 整个SchedulerReconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer

Renderer(渲染器)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。

React 技术揭秘:react.iamkasong.com/preparation…

函数式组件、calss组件

juejin.cn/post/698275…

Hooks

"hooks" 直译是 “钩子”,指:

系统运行到某一时期时,会调用被注册到该时机的回调函数。

以 react 为例,hooks 是:

一系列以 “use” 作为开头的方法,它们提供了让你可以完全避开 class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。

而在 vue 中, hooks 的定义可能更模糊,姑且总结一下:

在 vue 组合式API里,以 “use” 作为开头的,一系列提供了组件复用、状态管理等开发能力的方法。

mixins弊端

  • 难以追溯的方法与属性
  • 属性、方法的覆盖、同名
  • mixins复用的复杂性高,可读性低。

Hooks的优点

  • 解决了mixins的追溯、覆盖、复用弊端
  • 组织方式的混乱程度变低,做到高度聚合
    • 一个页面中,N件事情的代码在一个组件内互相纠缠确实是在 Hooks 出现之前非常常见的一种状态。Hooks 写法都能做到,将“分散在各种声明周期里的代码块”,通过 Hooks 的方式将相关的内容聚合到一起。
  • 代码可阅读性提升,易理解
  • 友好的渐进式 -你依然可以在项目里一边写 class 组件,一边写 Hooks 组件,在项目的演进和开发过程中,这是一件没有痛感,却悄无声息改变着一切的事情。

vue 和 react 自定义 Hook 的异同

  • 相似点: 总体思路是一致的 都遵照着 "定义状态数据","操作状态数据","隐藏细节" 作为核心思路。

  • 差异点: 组合式APIReact函数组件 有着本质差异
    vue3 的组件里, setup 是作为一个早于 “created” 的生命周期存在的,无论如何,在一个组件的渲染过程中只会进入一次。
    React函数组件 则完全不同,如果没有被 memorized,它们可能会被不停地触发,不停地进入并执行方法,因此需要开销的心智相比于vue其实是更多的。

juejin.cn/post/706695…

异步setState

React会将多个setState的调用合并成一个来执行,这意味着当调用setState时,state并不会立即更新。

setState需要实现的两个功能

  1. 异步更新state,将短时间内的多个setState合并成一个
  2. 为了解决异步更新导致的问题,增加另一种形式的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
       }
   } );
}

参考:github.com/hujiulong/b…

参考: 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源码分析

vue3js.cn/start/

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
  • 判断newVnodeoldVnode是否指向同一个对象,如果是,那么直接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]

参考 juejin.cn/post/699495…

列表组件中 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,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该keythis.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

  1. 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之后才能触发下一级的深度响应式。

参考 juejin.cn/post/685889…

  1. Tree-shaking support:支持摇树优化

    • 在2.x版本中,很多函数都挂载在全局Vue对象上,比如nextTick、nextTick、nextTick、set等函数,因此虽然我们可能用不到,但打包时只要引入了vue这些全局函数仍然会打包进bundle中。而在Vue3中,所有的API都通过ES6模块化的方式引入,这样就能让webpack或rollup等打包工具在打包时对没有用到API进行剔除,最小化bundle体积。
  2. Composition API:组合API Options API就是将同一类型的东西放在同一个选项中,但是随着数据增多,我们维护的功能点会涉及到多个data和method,导致了组件难以理解和阅读。

Composition API做的就是把同一功能的代码放到一起维护,这样我们需要维护一个功能点的时候,不用去关心其他的逻辑,只关注当前的功能

// context:{attrs,slots,emit}
export default { setup(props, context) {} };
  1. Fragment,Teleport,Suspense:新增的组件

  2. Better TypeScript support:更好的TypeScript支持

  3. Custom Renderer API:自定义渲染器

  4. 非兼容的功能

    • 移除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 有哪些依赖。

这里我们采用的方法是:

  1. 利用 全局 WeakMap 来保存所有对象
  2. 利用 Map 来保存对象中所有 key
  3. 利用 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
    }
}

参考:

blog.csdn.net/l63492818/a…

inject/provide

Vue中, Provide/Inject实现了跨组件的通信。但可惜数据并不是响应式的(设计如此)。

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的

可能之所以这样设计,是为了避免数据的混乱。就如同props不能被子组件直接修改一样。

vue组件通信方式

  1. props / $emit 适用 父子组件通信
  2. ref 与parent/children 适用 父子组件通信
  3. EventBus (emit/on) 适用于 父子、隔代、兄弟组件通信
  4. attrs/listeners 适用于 隔代组件通信
  5. provide / inject 适用于 隔代组件通信
  6. Vuex 适用于 父子、隔代、兄弟组件通信

Vue-router

hash模式

hash模式是vue-router的默认路由模式,它的标志是在域名之后带有一个#

通过window.location.hash获取到当前url的hash;hash模式下通过hashchange方法可以监听url中hash的变化

history模式

基于HTML5的history对象。通过pushStatereplaceState方法可以修改url地址,结合popstate事件监听url中路由的变化。History API可以的pushStatereplaceState方法可以在改变页面 url 的情况下不刷新页面

history模式需要服务端进行支持,将路由都重定向到根路由

实现原理

  1. Router install

    • mixin: 在组件实例化前beforeCreate阶段,并执行初始化
    • 注册全局组件router-view,用来渲染路由组件
  2. Router类收集数据,确定路由类型,包括路由模式mode、路由表routes。

  3. 监听路由变化

    • hash:hashchange,获取到当前的path
    • history: 添加popstate监听,可以监听到浏览器前进、后退。但不能监听到pushState、replaceState,所以在执行方法时需要修改path。
  4. router-view渲染。拿到path,对所有路由信息筛选,找到对应的组件,作为内容。

    • 创建vue实例监听实例path的响应式变化后进行渲染

参考:

juejin.cn/post/684490…

Vuex

为什么需要在actions中做异步操作, mutation只能做同步操作

  • devtool是跟着mutation走的,mutation走一步devtool记录一步,如果在mutation中加入异步,devtool记录到异步函数的时候异步函数还没调用,devtool不能跟踪数据的变化
  • actions里可以返回promise判断异步状态,从而解决状态依赖问题
  • 事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,只要最后触发 mutation 就行,异步竞态怎么处理那是用户自己的事情。

juejin.cn/post/684490…

参考: 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 和路由机制,最终就可以把整个程序给运⾏在浏览器上⾯。

blog.csdn.net/qiwoo_weekl…

性能优化

对小程序的性能影响较大的有两个因素,分别是 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 更新都是由页面对象调用,如果我们的页面结构比较复杂,更新的性能就会下降。
  • CustomWrapper 组件。创建一个原生自定义组件,对后代节点的 setData 将由此自定义组件进行调用,达到局部更新的效果。

虚拟列表

因为 DOM 元素的创建和渲染需要的时间成本很高,在大数据的情况下,完整渲染列表所需要的时间不可接收。其中一个解决思路就是在任何情况下只对「可见区域」进行渲染,可以达到极高的初次渲染性能。

实现虚拟列表思路:

  1. 实现虚拟列表的 HTML、CSS 结构
  • 容器元素使用相对定位,定高,overflow: auto;
  • 使用一个不可见元素(.list-view-phantom)撑起这个列表,让列表的滚动条出现
  • 列表的可见元素(.list-view-content)使用绝对定位,以便通过transform定位到可视区
  1. 数据渲染
  • 计算当前可见区域起始数据的 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);
    }
  }
}

taro-docs.jd.com/taro/docs/o…

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-loaderthread-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)

参考:

cloud.tencent.com/developer/a…