框架通识

115 阅读4分钟

MVVM

View 视图 Model 数据模型 ViewModel 桥梁、实例
在MVVM中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue2中的数据劫持,Vue3中的数据代理。

MVC

View 视图 Model 数据模型 Controller 控制器
React

脏数据检测

当触发了指定事件后会进入脏数据检测,这时会调用 $digest 循环遍历所有的数据观察者,判断当前值是否和先前的值有区别,如果检测到变化的话,会调用 $watch 函数,然后再次调用 $digest 循环直到发现没有变化循环至少为二次 ,至多为十次
脏数据检测虽然存在低效的问题,但是不关心数据是通过什么方式改变的,这在 Vue 中的双向绑定是存在问题的并且脏数据检测可以实现批量检测出更新的值,再去统一更新 UI,减少了操作 DOM 的次数所以低效也是相对的

数据劫持

# Vue2.0内部使用Object.defineProperty()来实现双向绑定,监听数据的getset事件,搭配发布订阅模式。

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 将 Watcher 添加到订阅
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 执行 watcher 的 update 方法
      dp.notify()
    }
  })
}


class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 实例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局属性,通过该属性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向自己
    // 然后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 获得新值
    this.value = this.obj[this.key]
    // 调用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy'

Proxy 与 Object.defineProperty 对比

Object.defineProperty缺陷:
    只能对属性进行数据劫持,需要深度遍历整个对象
    对数组不可用(相当于重写数组的几个api然后做了监听处理)

路由原理

本质就是监听URL的变化然后匹配路由规则,显示相应的页面,无需刷新。
目前单页面使用的路由两种实现方式:
    hash模式
        http://test.com/#/yck   通过window.hashchange监听URL的变化
    history模式
        切换
            history.go(-2);     //后退两次
            history.go(2);      //前进两次
            history.back();     //后退
            hsitory.forward();  //前进 
        修改
            // 通过pushstate把页面的状态保存在state对象中,当页面的url再变回这个url时,可以通过event.state取到这个state对象,从而可以对页面状态进行还原
            // 参数stateObj, title, url(url与当前的origin必须一样不然会报错,可以绝对也可以相对路径)
            window.history.pushState({color:'red'}, 'red', 'red')
            // 与pushState基本相同,不过他是修改当前历史记录,pushState是创建一个新的历史记录
            window.history.replaceState(state, title, url)
            window.onpopstate = function(event){
                console.log(event.state)    // 获取stateObj数据
            }

        缺点
            害怕刷新,因为hash模式是修改#中的信息,浏览器请求不带,但是history可以自由修改path,如果服务器没有相应的资源会404

Virtual Dom

因为操作DOM很费性能,可以通过JS对象模拟DOM,判断旧的对象和新的对象之间的差异,渲染差异。

判断差异的算法分为了两步:
    首先从上至下,从左往右遍历对象,也就是树的深度遍历,这一步中会给每个节点添加索引,便于最后渲染差异
    一旦节点有子元素,就去判断子元素是否有不同

两个节点对比会出现的几种情况:
    新的节点的tagName或者key和旧的不同,这种情况代表需要替换旧的节点,并且也不再需要遍历新旧节点的子元素了,因为整个旧节点都被删掉了
    新的节点的 tagName 和 key(可能都没有)和旧的相同,开始遍历子树
    没有新的节点,那么什么都不用做

判断属性的更改也分三个步骤:
    遍历旧的属性列表,查看每个属性是否还存在于新的属性列表中
    遍历新的属性列表,判断两个列表中都存在的属性的值是否有变化
    在第二步中同时查看是否有属性不存在旧的属性列列表中

判断列表差异也分三个步骤(该算法只对有key的节点做处理):
    遍历旧的节点列表,查看每个节点是否还存在于新的节点列表中
    遍历新的节点列表,判断是否有新的节点
    在第二步中同时判断节点是否有移动

渲染差异
    深度遍历树,将需要做变更操作的取出来
    局部更新DOM