Vue源码——响应式原理、模板编译、vuex和vue-router简单实现等

145 阅读4分钟
vue响应式原理
  1. Observer类通过Object.defineProperty处理所有属性(包括子属性)object数据,实现object数据可观测。
  2. 封装了依赖管理器Dep,用于存储收集到的依赖。在getter中收集依赖,在setter中通知依赖更新
  3. 每一个依赖都创建了一个Watcher实例,当外界通过Watcher读取数据时,会触发getter 通过dep.depend()从而将Watcher添加到依赖Dep中;数据发生了变化时,会触发setter通过dep.notify()通知Watcher实例,由Watcher实例去做真实的更新操作。
  4. Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
export default class Dep {
  constructor () {
    this.subs = [] // watcher
  }
  // 添加一个依赖
  depend () {
    if (window.target) {
      this.addSub(window.target)
    }
  }
  // 通知所有依赖更新
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
​
function defineReactive (obj,key,val) {
  if (arguments.length === 2) {
    val = obj[key]
  }
  if(typeof val === 'object'){
    new Observer(val)
  }
  const dep = new Dep()  //实例化一个依赖管理器,生成一个依赖管理数组dep
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get(){
      dep.depend()    // 在getter中收集依赖
      return val;
    },
    set(newVal){
      if(val === newVal){
          return
      }
      val = newVal;
      dep.notify()   // 在setter中通知依赖更新
    }
  })
}
​
​
export default class Watcher {
  constructor (vm,expOrFn,cb) {
    this.vm = vm;
    this.cb = cb;
    this.getter = parsePath(expOrFn)
    this.value = this.get()
  }
  get () {
    window.target = this;
    const vm = this.vm
    let value = this.getter.call(vm, vm)
    window.target = undefined;
    return value
  }
  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}
模板编译
  1. 一堆字符串模板解析成抽象语法树AST后,我们就可以对其进行各种操作处理了,处理完后用处理后的AST来生成render函数

  2. compileToFunctions函数是模板编译入口函数,包含parsegenerate的执行,返回值是一个render函数

    1. 模板解析阶段:将一堆模板字符串用正则等方式解析成抽象语法树AST;解析器(parser)模块
    2. 优化阶段:遍历AST,找出其中的静态节点,并打上标记;
    3. 代码生成阶段:将AST转换成渲染函数;

img

生命周期

父子组件生命周期调用顺序

beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted。 img

  • 初始化阶段:为Vue实例上初始化一些属性,事件以及响应式数据;
    • new Vue
    • initLifecycle
    • initEvents
    • initInjections
    • initState
      • 该初始化函数内部总共初始化了5个选项,分别是:propsmethodsdatacomputedwatch
      • 这5个选项的初始化顺序不是任意的,而是经过精心安排的。只有按照这种顺序初始化我们才能在开发中在data中可以使用props,在watch中可以观察dataprops
  • 模板编译阶段:将模板编译成渲染函数;
  • 挂载阶段:将实例挂载到指定的DOM上,即将模板渲染到真实DOM中;vm.$mounted
    • 第一部分是将模板渲染到视图上,第二部分是开启对模板中数据(状态)的监控。
  • 销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器;vm.$destory
    • 将当前的Vue实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器。
简单的Vuex实现
const createStore = ({ state, mutations }) {
    return new Vue({
        data: { state },
        methods: {
            commit (mutationType) {
                mutations[mutationType](this.state)
            }
        }
    })
}
const store = createStore({
    state: { count: 0 },
    mutations: {
        inc (state) {
            state.count++
        }
    }
})
const Counter = {
    render(h) {
        return h('div', store.state.count)
    }
}
new Vue({
    el: '#app',
    components: { Counter },
    methods: {
        inc() {
            store.commit('inc')
        }
    }
})
简单的vue-router实现
// '/user/123?foo=bar'
// path-to-regexp 匹配路由
const routerObj = {
    path: '/user/123',
    params: { username: '123' },
    query: { foo: 'bar' }
}
const Bar = { template: `<div>bar</div>`}
const Foo = { template: `<div>foo</div>`}
const NotFound = { template: `<div>notFound</div>`}
​
const routerTable = {
    'foo/:id': Foo,
    'bar': Bar
}
​
// 匹配路由参数
const compiledRouterTable = []
Object.keys(routerTable).forEach(path => {
    const dynamicSegments = []
    const regex = pathToRegex(path, dynamicSegments)
    const component = routerTable[path]
    compiledRouterTable.push({
        component,
        regex,
        dynamicSegments
    })
})
​
window.addEventListerner('hashchange', () => {
    app.url = window.location.hash.slice(1)
})
​
const app = new Vue({
    el: '#app',
    url: window.location.hash.slice(1)
    render (h) {
        const path = '/' + this.url
        let componentToRender
        let props = {}
        // 根据已经存的取组件和参数
        compiledRouters.some(router => {
            // foo/123 => match [foo/123 123]  segments {name: id} => props { id: 123 }
            const match = router.regex.exec(path)
            if(match) {
                componentToRender = router.component
                router.dynamicSegments.forEach((segment, index) => {
                    props[segment.name] = match[index + 1]
                })
            }
        })
        return h('div', [
            h(componentToRender, { props }),
            h('a', { attrs: { href: '#foo/123' }}, 'foo 123'),
            ' | ',
            h('a', { attrs: { href: '#foo/234' }}, 'foo 234'),
            ' | ',
            h('a', { attrs: { href: '#bar' }}, 'bar'),
        ])
    }
})
​
​
const validationPlugin = {
    install (Vue) {
        Vue.mixin({
            computed: {
                $v() {
                   const rules = this.$options.validations
                }
            }
        })
    }
}
diff算法

当数据改变时,会触发setter,并且通过Dep.notify去通知所有订阅者Watcher,订阅者们就会调用patch方法,给真实DOM打补丁,更新相应的视图。

  1. Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM,对比出是哪个虚拟节点更改了,找出这个虚拟节点,并只更新这个虚拟节点所对应的真实节点,而不用更新其他数据没发生改变的节点,实现精准地更新真实DOM,进而提高效率虚拟DOM算法操作真实DOM,性能高于直接操作真实DOM

  2. Diff算法比较只会在同层级进行,深度优先算法。 时间复杂度:O(n)

  3. 流程

    patch 对比当前同层的虚拟节点是否为同一种类型的标签

    都有子节点执行updateChildren函数比较子节点

    截屏2021-08-08 下午3.03.31.png

    1、oldS 和 newS使用sameVnode方法进行比较,sameVnode(oldS, newS)

    2、oldS 和 newE使用sameVnode方法进行比较,sameVnode(oldS, newE)

    3、oldE 和 newS使用sameVnode方法进行比较,sameVnode(oldE, newS)

    4、oldE 和 newE使用sameVnode方法进行比较,sameVnode(oldE, newE)

    5、如果以上逻辑都匹配不到,再把所有旧子节点的 key 做一个映射到旧节点下标的 key -> index 表,然后用新 vnodekey 去找出在旧节点中可以复用的位置。

nextTick源码
let callbacks = []; //回调函数
let pending = false;
function flushCallbacks() {
  pending = false; //把标志还原为false
  // 依次执行回调
  for (let i = 0; i < callbacks.length; i++) {
    callbacks[i]();
  }
}
let timerFunc; //先采用微任务并按照优先级优雅降级的方式实现异步刷新
if (typeof Promise !== "undefined") {
  // 如果支持promise
  const p = Promise.resolve();
  timerFunc = () => {
    p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== "undefined") {
  // MutationObserver 主要是监听dom变化 也是一个异步方法
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = () => {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
} else if (typeof setImmediate !== "undefined") {
  // 如果前面都不支持 判断setImmediate
  timerFunc = () => {
    setImmediate(flushCallbacks);
  };
} else {
  // 最后降级采用setTimeout
  timerFunc = () => {
    setTimeout(flushCallbacks, 0);
  };
}
​
export function nextTick(cb) {
  callbacks.push(cb);
  if (!pending) {
    pending = true;
    timerFunc();
  }
}
​

引用

术语

搜索引擎优化 SEO Search Engine Optimization

单页面 SPA

服务器渲染 SSR

函数式编程

"函数式编程"是一种"编程范式"(programming paradigm)函数式编程是把运算过程尽量写成一系列嵌套的函数调用

  1. 函数是"第一等公民"
  2. 只用"表达式",不用"语句"
  3. 没有"副作用"
  4. 不修改状态
  5. 引用透明

关系型数据库复杂模型比如a.b.c这种关联可以通过id来维护关系来实现数据的扁平化