我写了一个青铜版vue

838 阅读5分钟

我的青铜版vue代码地址: 【GitHub | 码云

GitHub | 码云——青铜版vue代码都是结核vue源码简化实现注释详细可放心品尝

实现原理图:

在这里插入图片描述

vue.js初始化流程图:对应vue源码

在这里插入图片描述

数据响应式 Observer 原理:

在这里插入图片描述 Observer 作用:通过Object.definePropertydata 内的所有层级的数据都进如下操作:

class Observer {
  constructor(data) {
    //__ob__ 一个响应式标记 作用:将当前this'继承'给需响应的对象或数组
    Object.defineProperty(data, '__ob__', {
      value: this,         //指向this
      enumerable: false,   //不可枚举
      configurable: false
    })

    //判断数组响应式
    if (Array.isArray(data)) {
      data.__proto__ = arrayMethods //替换封装的原型方法
      this.observeArray(DataCue)
    } else {
      this.walk(data)
    }
  }

  observeArray(data) {
    for (let i = 0; i < data.length; i++) {
      observe(data[i])
    }
  }

  walk(data) {
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }

  defineReactive(data, key, value) {
    observe(value) //递归 所有数据响应式
    let dep = new Dep //每个属性一个
    Object.defineProperty(data, key, {
      get() {
        if (Dep.target) { //将Dep.target赋值后再调用get方法就可以给该属性添加一个wacher
          dep.depend()    //添加watcher
        }

        return value
      },
      set(newValue) {
        if (newValue === value) return
        observe(newValue) //给新数据响应式
        value = newValue

        //视图更新
        dep.notify()
      }
    })
  }
}



export function observe(data) {
  //不是对象或=null不监控
  if (!isObject(data)) {
    return
  }

  //对象已监控 则跳出
  if (data.__ob__ instanceof Observer) {
    return
  }

  return new Observer(data)
}

observe 方法的作用是遍历对象,在内部对数据进行劫持添加 get 和 set方法,把劫持的逻辑单独抽取成 defineReactive 方法,observe 方法作用是对数据类型验证,符合需求后会调用Observer方法进行属性响应式,然后再循环对象每一个属性进行劫持,当数据为数组时,通过重写改变数组的7个方法来实现监听数组改变而触发指定更新watcher, set方法内部的 observe 作用是将新赋值的对象进行深度劫持,确保该插入数据转换成响应式。 在 defineReactive 方法中,对每一个属性创建了 Dep 的实例, 当页面上出现 {{data}}响应式数据时该属性Dep内就会新增一个 watcher,当render函数执行就会触发了 get,在 get 中就可以将这个 watcher 添加到 Dep 的 subs 数组中进行统一管理,因为在代码中获取 data 中的值操作比较多,会经常触发 get,我们又要保证 watcher 不会被重复添加,所以在 Watcher 类中,获取旧值并保存后,立即将 Dep.target 赋值为 null,并且在触发 get 时对Dep.target进行了短路操作,存在才调用 Dep 的 depend 进行添加 dep 和 watcher 是一个多对多的关系 每个组件一个diff的逻辑 也就是每个组件一个watcher 也就是组件页面内多个响应式属性指向一个watcher 每个属性对应一个dep ,而dep内存储多个watcher 也就是该dep出现在多个watcher内 说明该属性存在多个组件页面内响应式显示 详情请异步源码

模板编译器 compiler 原理:

如果你通过document.querySelector("div").outerHTML 获取一个节点的outerHTML你会发现它就是你在html文件内写的标签代码字符串

  1. 第一步我们需通过parseHTML方法把outerHTML转换成ast
  2. 第二步我们我们需把ast树转换成render函数的字符串形式
  3. 最后我们通过new Function()方法 将render函数的字符串形式转换成真正的render方法,render方法的作用就是生成vNode, 最终通过diff 算法比对新老vNode 从而完成了页面的更新渲染
export function compileToFunctions(template) {
  //1. 将outerHTML 转换成 ast树
  let ast = parseHTML(template) // { tag: 'div', attrs, parent, type, children: [...] }
  // console.log("AST:", ast)

  //2. ast树 => 拼接字符串
  let code = generate(ast) //return _c('div',{id:app,style:{color:red}}, ...children)
  code = `with(this){ \r\n return ${code} \r\n }`
  // console.log("code:", code)
  
  //3. 字符串 => 可执行方法
  let render = new Function(code)
  /**如下:
  * render(){ 
  *   with(this){
  *     return _c('div',{id:app,style:{color:red}},_c('span',undefined,_v("helloworld"+_s(msg)) ))
  *   }
  * }
  * 
  */

  return render
  /**
   * 编译原理的3个步骤:
   * 1. outerHTML    => ast树
   * 2. ast树        => render字符串
   * 3. render字符串 => render方法
   */
}

patch.js diff算法实现原理:

通过比对新旧vNode的不同而更新 dom 渲染页面 在这里插入图片描述 Vnode是什么? 下面就是一个Vnode的雏形: 在这里插入图片描述 el: 就是当前节点对应dom上的真实节点,当比对两个vNode 不同时就直接通过el操作真实dom渲染页面 它对应的HTML如下:

<div id="app">
  <h1 class="h1">标题一 姓名: {{name}} </h1>
  <h2 style="color: red;">标题二 年龄: {{age}}</h2>
  <div>
    <h3 class="h2">标题三</h3>
    <span style="color: pink;font-size: 30px;">姓名: {{name}} ,年龄: {{age}}</span>
  </div>
</div>

首先进行树级别比较,可能有三种情况:增删改。 new VNode不存在就删 old VNode不存在就增 在这里插入图片描述 都存在就执行diff执行更新:

//diff算法核心 vnode比较得出最终dom
//对应vue源码 src\core\vdom\patch.js  424行
oldStartIndex // 老的开始的索引
oldStartVnode // 老的开始
oldEndIndex   // 老的尾部索引
oldEndVnode   // 获取老的孩子的最后一个

newStartIndex // 老的开始的索引
newStartVnode // 老的开始
newEndIndex   // 老的尾部索引
newEndVnode   // 获取老的孩子的最后一个

while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
    //1与2解决数组塌陷时设置节点为null的问题

    //1. 旧开始节点是否存在 不存在下一个
    if (!oldStartVnode) {
      oldStartVnode = oldChildren[++oldStartIndex]

    //2. 旧结束节点是否存在 不存在前一个
    } else if (!oldEndVnode) {
      oldEndVnode = oldChildren[--oldEndIndex]

    //3. 新老 开始 节点是否相同 是递归patch比较子节点
    } else if (isSameVnode(oldStartVnode, newStartVnode)) {
      patch(oldStartVnode, newStartVnode)
      oldStartVnode = oldChildren[++oldStartIndex]
      newStartVnode = newChildren[++newStartIndex]
    
    //4. 新老 结束 节点是否相同 是递归patch比较子节点
    } else if (isSameVnode(oldEndVnode, newEndVnode)) {
      patch(oldEndVnode, newEndVnode);
      oldEndVnode = oldChildren[--oldEndIndex]; // 移动尾部指针
      newEndVnode = newChildren[--newEndIndex];

    //5. 老开始 新结束 节点是否相同 是递归patch比较子节点
    } else if (isSameVnode(oldStartVnode, newEndVnode)) { // 正序  和 倒叙  reverst sort
      //头不一样 尾不一样  头移尾  倒序操作
      patch(oldStartVnode, newEndVnode);
      parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); // 具备移动性
      oldStartVnode = oldChildren[++oldStartIndex];
      newEndVnode = newChildren[--newEndIndex];

    //6. 老结束 新开始 节点是否相同 是递归patch比较子节点
    } else if (isSameVnode(oldEndVnode, newStartVnode)) { // 老的尾 和新的头比对
      patch(oldEndVnode, newStartVnode)
      parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
      oldEndVnode = oldChildren[--oldEndIndex]
      newStartVnode = newChildren[++newStartIndex]
    }
    ...

watcher异步更新队列原理:

在这里插入图片描述 事件循环Event Loop: 浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而制定的工 作机制。 宏任务Task: 代表一个个离散的、独立的工作单元。浏览器完成一个宏任务,在下一个宏任务执 行开始前,会对页面进行重新渲染。主要包括创建文档对象、解析HTML、执行主线JS代码以及各 种事件如页面加载输入网络事件定时器等。

微任务: 微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏 览器会清空微任务之后再重新渲染。微任务的例子有 Promise回调函数DOM变化等。 体验宏微任务处理流程

异步: 只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变 更。 批量: 如果同一个 watcher 被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算 和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列执行实际工作。 异步策略: Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserversetImmediate ,如果执行环境都不支持,则会采用 setTimeout 代替。

let has = {}; // vue源码里有的时候去重用的是set 有的时候用的是对象来实现的去重
let queue = [];

// 这个队列是否正在等待更新
function flushSchedulerQueue() {
  for (let i = 0; i < queue.length; i++) {
    queue[i].run() // 执行 watcher 内部的 updateComponent 方法 更新页面
  }
  queue = []
  has = {}
}

//由于多个元素指向同一个 watcher 所以更新的时候需要把这些 watcher 集中起来 去重后一起执行
//原因:如果每匹配一个元素就执行一个 watcher 这样重复执行了许多相同的 watcher 性能大大下降
export function queueWatcher(watcher) {
  const id = watcher.id;

  if (has[id] == null) {
    has[id] = true // 如果没有注册过这个watcher,就注册这个watcher到队列中,并且标记为已经注册
    queue.push(watcher)  // watcher 存储了updateComponent方法 用来更新页面
    console.log("queuequeue---", queue);
    nextTick(flushSchedulerQueue); // flushSchedulerQueue 调用渲染watcher
  }
}

// 1. callbacks[0] 是flushSchedulerQueue函数 当监听组件data数据改变时会执行dep.notify()方法
// 2. dep.notify()方法将所有触发的 watcher 传递给 queueWatcher 方法
// 3. queueWatcher方法会对 watcher 进行去重 当所有组件data改变都监听完后 flushCallbacksQueue 开始工作
let callbacks = [];   // [flushSchedulerQueue,fn]
let pending = false
function flushCallbacksQueue() {
  callbacks.forEach(fn => fn())
  pending = false
}


//上面22行第一次进入nextTick就开启了一个定时器 执行 nextTick 进来的回调函数
//由于js定时器为宏观任务,定时器会等到所有微观任务都执行后才会执行定时器
//所以当组件内的nextTick回调都一个个添加 callbacks 内且页面完全渲染后会触发 flushCallbacksQueue 方法
export function nextTick(fn) {
  callbacks.push(fn)  // 防抖
  if (!pending) {     // true  事件环的概念 promise mutationObserver setTimeout setImmediate
    setTimeout(() => {
      flushCallbacksQueue() //清除回调队列
    }, 0)
    pending = true
  }
}