笔记:vue原理篇

648 阅读9分钟

目录框架

  • vue原理
    • mvvm框架
    • vue中的MVVM
  • vue-router原理
  • vuex原理
  • vue-ssr

vue原理

MVC和MVVM
  • M:model模型 只是负责处理数据的,它根本不知道数据到时候会拿去干啥
  • V:view 视图 简单来说,就是我们在界面上看见的一切。
  • C: Controller: 控制器 是应用程序中处理用户交互的部分 Controller里面把Model的数据赋值给View来显示, Controller不需要关心Model是如何拿到数据的,只管调用就行了
  • VM:viewModel: 视图模型

mvc: M到V的更新是通过C, M的数据通过C赋值给V显示,C接受用户行为,通过向C发射action将数据传给M

这就像一个公司,C相当于boss,在创业初期,boss可以处理任何闲杂的琐事,但当公司发展到一定阶段,boss有钱了,开始宗拜结果为导向,因此他请了一个助理,让助理直接向他汇报结果。这就类似MVVM诞生的背景,M中的数据,在V中不一定直接能用,因此需要一个数据解析的过程,将这个M中的数据通过VM转化为V可直接使用的。

mvvm: 是vue数据双向绑定的实现原理,在MVVM模式中,VM的出现使得Controller的存在感被完全的降低了,C直接持有VM,VM作为V和M的中间桥梁。

数据绑定和数据劫持

手动实现一个MVVM

大体流程图

创建Vue基类
// 基类
class Vue {
  constructor(options) {
    // 在当前实例中绑定$el,$data,对options的各种处理
    this.$el = options.el 
    this.$data = options.data
    ...
  }
}
数据绑定
  • 主要就是解析指令,解析插值,将数据替换
  • 过程:

在解析过程中,为了减少浏览器的回流与重绘,我们将处理dom放在内存中,经过解析操做后,统一在插入到容器中.

遍历存在内存中的dom,判断各个节点,判断如果是文本,进行变量替换,如果是标签,则进行属性解析的相关操作,如果该元素中还有子节点,继续遍历。

// 通过nodeType
class Compiler {
  /*
  @el元素
  @vm当前实例子
  */
 constructor(el, vm) {
    // el是不是一个元素,如果不是则获取
    this.el = this.isElementNode(el) === 1 ? el : document.querySelector(el)
    this.vm = vm
    // 将节点内容全部放在内存中,  appendChild() 方法移除元素到另外一个元素。
    const domFragment = this.createNodeFragment(this.el) // 为了减少浏览器的回流与重绘
    // 编译内存节点
    this.compile(domFragment)
    // 将编译后的碎片放在el中
    this.el.appendChild(domFragment)

  }
  // 编译内存节点
  compile(node) {
    if (!node) {
      return
    }
    /** 判断类型
     * 文本, {{}}
     *  标签, v-
     */
    // 拿到内存节点的子节点(只取一层) childNodes
    const childNodes = node.childNodes // 类数组
    Array.from(childNodes).forEach(child => {
      if(this.isElementNode(child)) {
        // 是标签
        this.compileElement(child)
        // 递归编译
        this.compile(child)
      } else {
        // 是文本
        this.compileText(child)
      }
    });
  }
  // 编译文本
  compileText(text) {
    const content = text.textContent
    const metchResult = /\{\{(.+)\}\}/.exec(content)
    if (/\{\{(.+)\}\}/.test(content)) {
      CompilerUtil['text'](text, content, this.vm)
    }

  }
  // 编译标签
  compileElement(node) {
    // v-, 获取元素属性,,
    const attributes = node.attributes
    // 遍历属性,是否有指令, 有指令的才需要双向绑定
    Array.from(attributes).forEach(attr => {
      const {name, value} = attr
      // name是否为指令
      if (this.isDirective(name)) { // 指令很多,需要区分 //v-mdel
        const [,directive]= name.split('-')  //v-on:click
        // 根据指令左对应的处理,将节点树数据
        CompilerUtil[directive](node, value, this.vm)
      }
    })

  }
  isDirective(name) {
    return name.startsWith('v-')
  }
  // 判断节点类型
  isElementNode(node) {
    return node.nodeType === 1
  }
  // 将节点内容移动到内存中
  createNodeFragment(el) {
    let firstChild;
    let fragment = document.createDocumentFragment()
    while(firstChild = el.firstChild) {
      fragment.appendChild(firstChild)
    }
    return fragment
  }
}
// 基类
class Vue {
  constructor(options) {
    ...
    new Compiler(this.$el, this)
    
  }
}

为了便于管理,统一将解析的过程放在一起,为下步的数据劫持做一些准备

// 解析工具
...省略,先把完整代码放这
CompilerUtil = {
  getValue(name, vm) {
    return name.split('.').reduce((acc, current) => {
      return acc[current]
    }, vm.$data)
  },
  setValue(expr, value) {
    return expr.split('.').reduce((acc, current, index, arr) => {
      console.log(acc, current, index, arr)
      // 在最后一项赋值
      if (index === arr.length - 1) {
        return acc[current] = value
      }
      return acc[current]
    }, vm.$data)

  },
  model(node, name, vm) {
    // 双向绑定 node.value = value
    node.addEventListener('input', (e) => {
      const newValue = e.target.value
      this.setValue(name, newValue)
    })
    const value = this.getValue(name, vm)
    // 生成watcher,在cb中改变双向绑定的值
    new Watcher(vm, name, (newValue) => {
      fn(node, newValue)
    })
    const fn = this.updater.modelUpdate
    fn(node, value)
  },
  on(node, name, vm, eventname) {
    node.addEventListener(eventname, (e) => {
      vm[name].call(vm, e)
    })
  },
  html(node, name, vm) {
    const value = this.getValue(name, vm)
    // 生成watcher,在cb中改变双向绑定的值
    new Watcher(vm, name, (newValue) => {
      fn(node, newValue)
    })
    const fn = this.updater.htmlUpdate
    fn(node, value)

  },
  getContentValue(vm, content) {
    return content.replace(/\{\{(.+?)\}\}/g, (...arg) => {
      return this.getValue(arg[1], vm)
    })
  },
  text(node, content, vm){
    const value = content.replace(/\{\{(.+?)\}\}/g, (...arg) => {
      new Watcher(vm,arg[1], (newValue) => {
        // 不能拿a的值替换 {{a}}, {{b}}
        fn(node, this.getContentValue(vm, content))
      })
      return this.getValue(arg[1], vm)
    })
    const fn = this.updater.textUpdate
    fn(node, value)
  },
  updater: {
    modelUpdate(node, value) {
      // 双向绑定 
      node.value = value
  
    },
    htmlUpdate(node, value) {
      node.innerHTML = value
    },
    textUpdate(node, value){
      node.textContent = value
    },
  }
}

数据劫持
  • 基于发布订阅模式(观察者模式)其实就是一个一个的watcher
  • 通过Observe监视data, 这个过程其实就是将传入的data变成Object.definedproperty的形式,然后对它的get和set进行对象的处理
  • Watcher: 存储老值,拿到新值,当老值与新值不一致时,执行回调
  • 通过Dep收集watcher, 与分发依次执行watcher.update()

当最开始取值时,我们会触发Object.definedProperty中的get方法,在数据绑定模块说过,最终会走到CompilerUtildmodel或者text方法,因此我们可以在这里new Watcher(),监视数据是否有变化,同时将实例的watcher挂在Dep类的target属性上,在我们new Watcher()的同时,因为会获取一次老值,因此又会出发一次get方法,此时我们实例化Dep,根据Dep是否有target属性,动态的收集watcher,当用户修改数据时,会出发,set()方法,我们在此时发,触发notify,让收集的watcher依次执行他们的update方法

...
class Dep {
  constructor(vm, watcher){
    this.subs = []
  }
  add(watcher) {
    this.subs.push(watcher)
  }
  notify() {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}
// 发布订阅模式,是一个一个的watcher
class Watcher {
  constructor(vm, expr, cb) {
    // 观察有某个属性,当该属性变化时执行cb方法
    this.vm = vm
    this.expr = expr
    this.cb = cb
    Dep.target = this
    // 先存放一个老值,好对比有没有变
    this.oldValue = this.get()
    Dep.target = null
  }
  get() {
    return CompilerUtil.getValue(this.expr, this.vm)
  }
  update() {
    const newValue = CompilerUtil.getValue(this.expr, this.vm)
    // 当新值和老值不一样时,执行回调
    if (newValue !== this.oldValue) {
      this.cb(newValue)
    }
  }
}
// 数据劫持
class Observe {
  // 将数据变成Object.definedproperty类型的
  constructor(data) {
    this.observe(data) 
  }
  isObject(data) {
    return typeof data === 'object' && data !== null
  }
  defineReactive(data, key, value) {
    this.observe(value)
    const dep = new Dep()
    const _this = this
    Object.defineProperty(data, key, {
      get() {
        Dep.target && dep.add(Dep.target)
        return value
      },
      set(newValue) {
        if (newValue !== value) {
          _this.observe(newValue)
          value = newValue
          dep.notify()
        }
      }
    })
  }
  observe(data) {
    if (this.isObject(data)) {
      // 是对象,将对象转为Object.definedProperty
      for(let key in data) {
        this.defineReactive(data, key, data[key])
      }
    }
  }
}
// 基类
class Vue {
  constructor(options) {
    // 在当前实例中绑定$el,$data
    this.$el = options.el
    this.$data = options.data
    let computed = options.computed // 有依赖关系
    let methods = options.methods
    if (this.$el) {
      new Observe(this.$data)
      ...
      this.proxyVm(this.$data)
      // 将vm.$data放在vm上
      new Compiler(this.$el, this)
    }
  }
  proxyVm(data) {
    for (let key in data) {
      Object.defineProperty(this, key, {
        get() {
          return data[key] //进行代理操作
        },
        set(newValue) {
          data[key] = newValue
        }
      })
    }
  }
}

computed以及methods实现
class Vue {
  constructor(options) {
    // 在当前实例中绑定$el,$data
    this.$el = options.el
    this.$data = options.data
    let computed = options.computed // 有依赖关系
    let methods = options.methods
    if (this.$el) {
      new Observe(this.$data)
      for(let key in computed) {
        Object.defineProperty(this.$data, key, {
          get: () => {
            return computed[key].call(this)
          }
        })
      }
      for(let eventName in methods) {
        Object.defineProperty(this, eventName, {
          get() {
            return methods[eventName]
          }
        })
      }
      this.proxyVm(this.$data)
      // 将vm.$data放在vm上
      new Compiler(this.$el, this)
    }
  }
  proxyVm(data) {
    for (let key in data) {
      Object.defineProperty(this, key, {
        get() {
          return data[key] //进行代理操作
        },
        set(newValue) {
          data[key] = newValue
        }
      })
    }
  }
}
...省略

vue-router原理

vue-router作为vue的插件,利用 Vue.js 提供的插件机制使用.use(plugin) 来安装 VueRouter,而这个插件机制则会调用该 plugin 对象的 install 方法

手动实现vue-router的hash模式
install阶段
+ 在install方法中,我们使用'Vue.mixin',在根组件中定义_router属性,如果是子组件,通过$parent使得每个子组件都可以访问根组件的_router。
+ 为vue的原型注入$router$route属性使得每个组件都可以通过this.$route和this.$router
+ 将_route属性指向current(当前路由信息),并将它转为响应式,以后如果我们更新app的_route属性,就会重新渲染视图
+ 将router-view注册为全局组件
+ 定义全局router的init方法
// install
export function install (Vue) {
  if (install.installed) return
  install.installed = true

  // 赋值私有 Vue 引用
  _Vue = Vue

  // 注入 $router $route
  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this.$root._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this.$root._route }
  })
  // beforeCreate mixin
  Vue.mixin({
    beforeCreate () {
      // 判断是否有 router,判断根实例,通过vue.mixin使每个组建都可以拿到根是例子
      if (this.$options.router) {
        this._rooter
        // 赋值 _router
        this._router = this.$options.router
        // 初始化 init
        this._router.init(this)
        // 定义响应式的 _route 对象
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
          this._router = this.$parent && this.$parent.$options.router
      }
    }
  })

  // 注册组件
  Vue.component('router-view', View)
  Vue.component('router-link', Link)
// ...
}
init阶段
+ init阶段,根据扁平化用户传入的参数
+ 根据参数创建router实例
+ 根据当前的路径,渲染并设置监听
import install from './install'
import createMatcher from './createMatcher'
import HashHistory from './history/HashHistory'
export default class VueRouter {
  constructor(options) {
    // 根据不同的路径,渲染不同的组件
    // 将传进来的数据扁平化,创建映射表,动态添加routes
    this.matcher = createMatcher(options.routes || [])
    this.mode = options.mode || 'hash' //切换模式
    /*
      //这两个类都继承与History基类
      hash: HashHistory
      history:HTML5History
    */
   this.history = new HashHistory(this)
  }
  match(path) {
    return this.matcher.match(path)
  }
  init(app) { // app是根实例
    // 根据路由,显示到制定的组件,并且设置监听
    const setUpListener = () => { // 跳转成功的回调
      this.history.setUpListener()
    }
    this.history.transitionTo(
      this.history.getCurrentLocation(),
      setUpListener
    )
    this.history.listen((route) => {
      app._route = route // 响应式的原理
    })
    // transitionTo公共的,getCurrentLocation属于hash,对于history,直接找path.name
  }
}
VueRouter.install = install
createMatcher
这个方法主要有两个方法: match, 以及addRoutes
+ 将传入的数据扁平化,生成路由映射表
+ addRoutes在做权限管理时,我们经常会临时添加路由配置
+ Match根据当前的路径在路由映射表中找到匹配项
import createRouterMap from './createRouterMap'
import {createRoute} from './history/BaseHistory'
export default function createMatcher(routes) {
  /* 主要是扁平化用户传入的数据,
    路径集合[/...]
    创建路由映射表 {/: []...}
  */
  const { oldPathMap: pathMap } = createRouterMap(routes) // 初始化数据
  function match(path) {
    // 如果有记录,则生成一个{path, match:[]}的对应关系
    const record = pathMap[path]
    if (record) {
      return createRoute(record, { 
        path
      })
    }
    // 拿到path取pathMap中找/,找到对应的记录,并且根据记录产生一个匹配数组
    return createRoute(null, {path})

  }
  // 动态添加的方法
  function addRoutes() {
    // 做权限管理时用
    const { pathList, pathMap } = createRouterMap(routes, pathList, pathMap) // 添加新的数据
  }
  return {
    match,
    addRoutes
  }
}

// createRouterMap.js
export default function createRouterMap(routes = [], oldPathList = [], oldPathMap = Object.create(null)) {
  // 扁平化数组
  routes.forEach(route => {
    addRouteRecord(route, oldPathList, oldPathMap)
  });
  return {
    oldPathList,
    oldPathMap
  }
}

function addRouteRecord(route, oldPathList, oldPathMap, parent) {
  // path, name, component
  let path = parent ? `${parent.path}/${route.path}` : route.path
  let record = {
    path,
    component: route.component,
    parent
  }
  if (!oldPathMap[path]) {
    oldPathList.push(path)
    oldPathMap[path] = record
  }
  if (route.children) {
    route.children.forEach(child => {
      addRouteRecord(child, oldPathList, oldPathMap, route)
    })
  }
}
History类
路由分为hashhistory模式
+ hash模式的原理是监听hashChange事件,由于锚点变化也会在历史记录栈添加新的记录,所以history.length也会在锚点变化之后改变。
+ histroy模式的原理:
    + 在HTML5,history对象提出了 pushState() 方法和 replaceState()方法,这两个方法可以用来向历史栈中添加数据,然后监听popState事件,
    + 当活动历史记录条目更改时,将触发popstate事件。如果被激活的历史记录条目是通过对history.pushState()的调用创建的,或者受到对history.replaceState()的调用的影响,popstate事件的state属性包含历史条目的状态对象的副本。
    + 需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back())
    
// history基类
export function createRoute(record, location) {
  let res = []
  if (record) {
    while(record) {
      res.unshift(record)
      record = record.parent
    }
  }
  return {
    ...location,
    matched: res
  }
}
export default class Base {
  constructor(router) { // Vue-router
    this.router = router
    this.curret = createRoute(null, {
      path: '/'
    })
  }
  transitionTo(path, callback) {
    // 拿到路径到映射表里取匹配
    const route = this.router.match(path)
    if(this.curret.path !== path && route.matched.length !== this.curret.matched.length) {
      this.updateRoute(route)
    }
    callback && callback()
  }
  updateRoute(route) {
    this.curret = route
    this.cb && this.cb(this.curret)
  }
  listen(cb) {
    this.cb = cb
  }
}
// hash路由类
import BaseHistory from './BaseHistory'
function getHash() {
  return window.location.hash.slice(1)
}
function ensureSlash() {
  if (window.location.hash) {
    return
  }
  window.location.hash = '/'
}
// 让HashHistory继承自BaseHistory
export default class HashHistory extends BaseHistory {
  // 继承base基类
  constructor(router) {
    super(router)
    ensureSlash() // 确保路径有/
  }
  getCurrentLocation() {
    return getHash()
  }
  setUpListener() {
    // 监听hash变化
    window.addEventListener('hashchange', () => {
      ensureSlash()
      this.transitionTo(this.getCurrentLocation())
      // 路径变化之后,要将新的route属性覆盖current
    })
  }
}

函数式组件
// view.js
// 函数式组件
export default {
  functional: true,
  render(h, { parent, data }) {
    // h为渲染方法,context为上下文
    // 拿到match一次渲染, curret中的match
    // data = { attr: 1}
    let matched = parent.$route.matched
    let dep = 0
    data.routerView = true
    while(parent) {
      if (parent.$vnode && parent.$vnode.data.routerView) {
        dep++
      }
      parent = parent.$parent
    }
    let record = matched[dep]
    if (!record) {
      return h()
    }
    let component = record.component
    return h(component, data)
  }
}

vuex原理

  • vue中的action与mutation有什么区别
  • 如何解决vuex持久化的问题
  • mutation是同步操作,actions是异步操作

vuex是vue的状态管理器, 采取的flux思想,主要思路是单向数据流。 放一张最经典的图,用户通过视图层触发(dispatch)actions,actions载提交(commit)mutations更新state,从而促进视图的更新

源码分析

  • 数据:state --> data 获取数据:getters --> computed 更改数据:mutations --> methods
  • 同vue-router一样,作为vue的插件,需提供一个install方法
install
Vue.mixin({
    beforeCreate() {
        if (this.$options && this.$options.store) {
            //找到根组件 main 上面挂一个$store
            this.$store = this.$options.store

        } else {
            //非根组件指向其父组件的$store
            this.$store = this.$parent && this.$parent.$store
        }
    }
})
state
  • 我们可以看出Vuex的state状态是响应式,是借助vue的data是响应式,将state存入vue实例组件的data中;
// 对state的处理
class Store{
    constructor() {
     this._s = new Vue({ 
            data: {
                // 只有data中的数据才是响应式
                state: options.state
            }
        })
        
    }
    get state() {
        return this._s.state
    }
}
...

getters
  • Vuex的getters则是借助vue的计算属性computed实现数据实时监听。
...
let getters = options.getters || {}
this.getters = {};
Object.keys(getters).forEach((getterName) => {
// 将getterName 放到this.getters = {}中
    Object.defineProperty(this.getters, getterName, {
        // 当你要获取getterName(myAge)会自动调用get方法
        // 箭头函数中没有this               
        get: () => {
            return getters[getterName](this.state)
        }
    })
})
...
mutations与actions
...
let mutations = options.mutations || {}
this.mutations = {};
Object.keys(mutations).forEach(mutationName=>{
    this.mutations[mutationName] = (payload) =>{
        this.mutations[mutationName](this.state,payload)
    }
})
...
// actions的原理 
    const actions = options.actions
    this.actions = {}
    Object.keys(actions).forEach(actionName => {
      this.actions[actionName] = payload => {
        this.actions[actionName](this, payload) // 内部函数执行,第一个参数是状态
      }
    })
commit dispatch的实现
// commit
commit = (type,payload){ // 保证this指向
    this.mutations[type](payload)
}
// dispatch
dispatch = (type,payload)=>{
    this.actions[type](payload)
}
modules (分模块)
  • 将传入的参数组合成树结构,然后遍历+发布订阅 如图所示:

class ModuleCollections {
  constructor(options) {
    this.regiester([], options) // 将模块注册成树结构
  }
  regiester(path, rootModule) {
    let module = {
      _rowModule: rootModule,
      _children:  {},
      state: rootModule.state
    }
    if (path.length === 0) {
      this.root = module // 根数据
    } else {
      // reduce [a, b, c] = { a: {b: {c{}}}}
     path.reduce((root, item, index, arr) => {
       if (arr.length -1 === index) {
         return root._children[item] = module
       } else {
         return root = root._children[item]
       }

      }, this.root)
    }

    if (rootModule.modules) {
      Object.keys(rootModule.modules).forEach((moduleName) => {
        this.regiester(path.concat(moduleName), rootModule.modules[moduleName])
      })
    }

  }
}
Store类
  • 所有的getters都会定义在this.getters中,即使是模块中的,actions以及mutations同理
class Store {
  constructor(options) {
    this.s = new Vue({
      data() {
        return  {
          state: options.state
        }
      }
    })
    this.getters = {}
    this.mutations = {}
    this.actions = {}
    this._modules= new ModuleCollections()

    installModule(this, this.state, [], this._modules.root)
  }
  commit = (name, payload) => {
    this.mutations[name].forEach(fn => fn(preload))
  }
  dispatch = (dispatchName, payload) => {
    this.actions[dispatchName].forEach(fn => fn(preload))
  }
  get state() { // 类的属性访问器 //调用this.state会走到这里
    return this._s.state
  }
}

// installModule
const installModule = (store, rootState, path = [], rootModule) => {
  // 如果path 子[a]
  if (path.length > 0) {
    let parent = path.slice(0, -1).reduce((root, item) => {
        return root = root[item]
    }, rootState)
    // 实现
    Vue.$set(parent, path[path.length -1], rootModule.state)
  }


  let  getters = rootModule._rowModule.getters
  if (getters) {
    Object.keys(getters).forEach(getterName => {
      Object.defineProperty(store.getters, getterName, {
        get() {
          return getters[getterName](rootModule.state) // 更新自己的状态
        }
      })
    })
  }
  // 整合mutaions
  let mutations = rootModule._rowModule.mutations
  if (mutations) {
    Object.keys(mutations).forEach(mutationName => {
      // 同名的事件可能是多个
      let mutationArr = store.mutations[mutationName] || []
      mutationArr.push((preload) => {
        mutations[mutationName](rootModule.state, preload)
      })
      store.mutations[mutationName] = mutationArr 
    })
  }
    // 整合actions
  let actions = rootModule._rowModule.actions
  if (actions) {
    Object.keys(actions).forEach(actionName => {
      // 同名的事件可能是多个
      let actionsArr = store.actions[actionName] || []
      actionsArr.push((preload) => {
        actions[actionName](rootModule.state, preload)
      })
      store.actions[actionName] = actionsArr 
    })
  }
  Object.keys(rootModule._children).forEach((childName) => {
    installModule(store, rootState, path.concat(childName), rootModule._children[childName])
  })
}

数据持久化
  • 在store中加插件
const persits = (store) => {
  store.subscribe(mutations, state) {
    localStorage.setItem('vuex-store', JSON.stringify(state))
  }
}
new Vuex.Store({
    plugin: [
        persits
    ]
    ...
})

参考文献

~~ 未完,持续更新