我写了一个青铜版vue

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

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

实现原理图:

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

数据响应式 Observer 原理:

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 渲染页面

<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算法核心 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异步更新队列原理:

微任务: 微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏 览器会清空微任务之后再重新渲染。微任务的例子有 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
  }
}
原文链接:juejin.im

上一篇:JS系列之 Promise
下一篇:WebPack基础入门(一):万物皆可webpack

相关推荐

  • 🚩Vue源码——订阅者的响应

    前言 在上篇专栏中介绍了发布者是如何收集订阅者(Watcher),本专栏来详细介绍发布者发生变化后,如何通知订阅者,而订阅者是如何响应。 一、如何通知订阅者 在 Vue 中发布者一般是数据,当数据发生...

    25 天前
  • 🚩Vue源码——组件是如何注册的

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 在上一篇 🚩Vue源码——组件...

    2 个月前
  • 🚩Vue源码——组件如何渲染成最终的DOM

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 Vue有两个核心思想,一个是数据...

    2 个月前
  • 🚩Vue源码——如何监听数据变化

    最近参加了很多场面试,几乎每场面试中都会问到Vue源码方面的问题。在此开一个系列的专栏,来总结一下这方面的经验,如果觉得对您有帮助的,不妨点个赞支持一下呗。 前言 Vue 是用数据来驱动来生成视图...

    1 个月前
  • 🚩Vue源码——如何深度收集渲染订阅者

    前言 本专栏是由一个问题引起,如果你已经知道答案了,可以忽略本专栏。 &lt;!DOCTYPE html&gt; &lt;html&gt; &lt;head&gt; &...

    13 天前
  • 🚩Vue源码——nextTick实现原理

    前言 在上一篇专栏讲到订阅者的响应是先把订阅者添加到一个队列,然后再 nextTick 函数中去遍历这个队列,对每个订阅者进行响应处理。大家所熟悉的 Vue API Vue.nextTick 全局方法...

    21 天前
  • 🔥基于vue3.0.1 beta搭建仿京东淘宝的电商商城项目!

    前言 就在前段时间,vue官方发布了3.0.0-beta.1 版本,趁着五一假期有时间,就把之前的一个电商商城的项目,用最新的Composition API拿来改造一下! 👉GitHub地址请访问�...

    7 个月前
  • 🏆 掘金技术征文|双节特别篇 vue3——composition API

    vue3刚出测试版的时候尝过一次,后来学了react,才尝出点味道来,现在再尝一遍,先从重要的compositon api入手! composition api 主要是把之前vue的核心api暴露出来...

    2 个月前
  • 🎉🎉🎉 一个基于vue3+vite+ts的完整项目

    VUE VBEN ADMIN2.0 介绍 vue-vben-admin-2.0 是一个全新的开源系统,基于ant-design-vue2.x,typescript4,vue3,vite实现的 ...

    2 个月前
  • 🍊仿Element自定义Vue组件库

    前言 🍊 市面上目前已有各种各样的UI组件库,他们的强大毋庸置疑。但是有时候我们有必要开发一套属于自己团队的定制化组件库。还有时候原有的组件不能满足我们的各种需求,就需要在原有的组件上进行改造...

    4 个月前

官方社区

扫码加入 JavaScript 社区