前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

文章学习导航
面试总览

30 秒速记

  • 响应式关联数据与工作,模板表达界面意图
  • 渲染负责把结果应用到宿主
  • 依赖收集期间记录读过的字段
  • 数据变化后去重调度,避免无差别全量刷新
  • 简化实现逐步补齐新增属性、数组与清理

实现一个小型数据驱动框架,可以把责任分成监听变化、理解模板和更新界面。 模板里的表达式读取数据时建立依赖,数据写入后通知相应更新函数,再把结果反映到节点。这样不用每次改值都手动找页面位置。入门实现先把最小链路跑通,再验证数组、嵌套、条件依赖和卸载,否则很容易出现演示可用、稍复杂场景就漏更新的问题。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

# 理解Vue的设计思想

  • MVVM框架的三要素:数据响应式、模板引擎及其渲染

  • 数据响应式:监听数据变化并在视图中更新

    • Object.defineProperty()
    • Proxy
  • 模版引擎:提供描述视图的模版语法

    • 插值:{{}}
    • 指令:v-bind,v-on,v-model,v-for,v-if
  • 渲染:如何将模板转换为html

    • 模板 => vdom => dom

# 数据响应式原理

数据变更能够响应在视图中,就是数据响应式。vue2中利用 Object.defineProperty() 实现变更检 测。

简单实现

// 数据响应式:
// Object.defineProperty()


function defineReactive(obj, key, val) {

  // val可能还是对象,此时我们需要递归
  observe(val)

  // 参数3是描述对象
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key);
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set', key);
        // 防止newVal是对象,提前做一次observe
        observe(newVal)
        val = newVal
      }
    }
  })
}

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }

  // 遍历
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}

// 对于新加入属性,需要单独处理他的响应式
function set(obj, key, val) {
  defineReactive(obj, key, val)
}

const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
observe(obj)
// defineReactive(obj, 'foo', 'foo')
// obj.foo
// obj.foo = 'fooooooooo'
// obj.bar
// obj.bar = 'barrrrrrrr'

// obj.baz.a = '10'

// obj.baz = {a: 10}
// obj.baz.a = 100

// 新添加一些属性
// obj.dong = 'dong' // no ok
set(obj, 'dong', 'dong')
obj.dong

// 前面的方法对于数组是不支持
// 思路:拦截数组7个变更方法push、pop。。。,扩展他们,使他们在变更数据的同时
// 额外的执行一个通知更新的任务
@前端进阶之旅: 代码已经复制到剪贴板

defineProperty() 不支持数组

解决数组数据的响应化

# Vue中的数据响应化

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <h3>KVue</h3>
    <p>{{counter}}</p>
    <p k-text="counter"></p>
    <p k-html="desc"></p>

  </div>

  <script src="kvue.js"></script>

  <script>
    const app = new KVue({
      el:'#app',
      data: {
        counter: 1,
        desc: '<span style="color:red">kvue可还行</span>'
      }
    })
    setInterval(() => {
      app.counter++
    }, 1000);
    
  </script>
</body>
</html>
@前端进阶之旅: 代码已经复制到剪贴板

# 原理分析

  • new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
  • 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在 Compile中
  • 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  • 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
  • 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

# 涉及类型介绍

  • KVue:框架构造函数
  • Observer:执行数据响应化(分辨数据是对象还是数组)
  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
  • Watcher:执行更新函数(更新dom)
  • Dep:管理多个Watcher,批量更新

# 实现Vue

# 框架构造函数:执行初始化

执行初始化,对data执行响应化处理,kvue.js

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }

  // 响应式
  new Observer(obj)

}
@前端进阶之旅: 代码已经复制到剪贴板
function defineReactive(obj, key, val) {}

class KVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    observe(this.$data)
  }
}
class Observer {
  constructor(value) {
    this.value = value this.walk(value);
  }
  walk(obj) {
    Object.keys(obj).forEach(key = >{
      defineReactive(obj, key, obj[key])
    })
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

为$data做代理

class KVue {
  constructor(options) {
    // 。。。
    proxy(this, '$data')
  }
}
function proxy(vm) {
  Object.keys(vm.$data).forEach(key = >{
    Object.defineProperty(vm, key, {
      get() {
        return vm.$data[key];
      },
      set(newVal) {
        vm.$data[key] = newVal;
      }
    });
  })
}
@前端进阶之旅: 代码已经复制到剪贴板

# 编译 Compile

编译模板中vue模板特殊语法,初始化视图、更新视图

1. 初始化视图

根据节点类型编译

class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      this.compile(this.$el);
    }
  }
  console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
  console.log("编译插值文本" + node.textContent);
}
if (node.childNodes && node.childNodes.length > 0) {
  this.compile(node);
}
});
}
isElement(node) {
  return node.nodeType == 1;

}
isInterpolation(node) {
  return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
@前端进阶之旅: 代码已经复制到剪贴板

编译插值

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • Vue

    • 从源码解读Vue生命周期
    • 组件的本质
    • 有状态组件的设计
    • 设计 VNode
    • 辅助创建 VNode 的 h 函数
    • 自定义渲染器和异步渲染
    • 渲染器之挂载
    • 渲染器的核心 Diff 算法
    • 渲染器之patch
    • 图解 Vue 响应式原理
    • 图解 Vue 异步更新
    • 剖析 Vue 内部运行机制
    • vue响应式原理模拟
    • vue状态管理之vuex
    • 理解Vue的设计思想及实现Vue
      • 理解Vue的设计思想
      • 数据响应式原理
      • Vue中的数据响应化
        • 原理分析
        • 涉及类型介绍
      • 实现Vue
    • diff算法深入
    • vue router vuex原理分析
    • Vue3初探响应式原理.
    • vue2源码分析
    • vue组件化实践
  • React

  • Webpack

  • Node

  • Javascript

  • 综合

完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

理解Vue的设计思想及实现Vue|原理篇

文章学习导航
面试总览

30 秒速记

  • 响应式关联数据与工作,模板表达界面意图
  • 渲染负责把结果应用到宿主
  • 依赖收集期间记录读过的字段
  • 数据变化后去重调度,避免无差别全量刷新
  • 简化实现逐步补齐新增属性、数组与清理

实现一个小型数据驱动框架,可以把责任分成监听变化、理解模板和更新界面。 模板里的表达式读取数据时建立依赖,数据写入后通知相应更新函数,再把结果反映到节点。这样不用每次改值都手动找页面位置。入门实现先把最小链路跑通,再验证数组、嵌套、条件依赖和卸载,否则很容易出现演示可用、稍复杂场景就漏更新的问题。

版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。

# 理解Vue的设计思想

  • MVVM框架的三要素:数据响应式、模板引擎及其渲染

  • 数据响应式:监听数据变化并在视图中更新

    • Object.defineProperty()
    • Proxy
  • 模版引擎:提供描述视图的模版语法

    • 插值:{{}}
    • 指令:v-bind,v-on,v-model,v-for,v-if
  • 渲染:如何将模板转换为html

    • 模板 => vdom => dom

# 数据响应式原理

数据变更能够响应在视图中,就是数据响应式。vue2中利用 Object.defineProperty() 实现变更检 测。

简单实现

// 数据响应式:
// Object.defineProperty()


function defineReactive(obj, key, val) {

  // val可能还是对象,此时我们需要递归
  observe(val)

  // 参数3是描述对象
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', key);
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set', key);
        // 防止newVal是对象,提前做一次observe
        observe(newVal)
        val = newVal
      }
    }
  })
}

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }

  // 遍历
  Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}

// 对于新加入属性,需要单独处理他的响应式
function set(obj, key, val) {
  defineReactive(obj, key, val)
}

const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } }
observe(obj)
// defineReactive(obj, 'foo', 'foo')
// obj.foo
// obj.foo = 'fooooooooo'
// obj.bar
// obj.bar = 'barrrrrrrr'

// obj.baz.a = '10'

// obj.baz = {a: 10}
// obj.baz.a = 100

// 新添加一些属性
// obj.dong = 'dong' // no ok
set(obj, 'dong', 'dong')
obj.dong

// 前面的方法对于数组是不支持
// 思路:拦截数组7个变更方法push、pop。。。,扩展他们,使他们在变更数据的同时
// 额外的执行一个通知更新的任务
@前端进阶之旅: 代码已经复制到剪贴板

defineProperty() 不支持数组

解决数组数据的响应化

# Vue中的数据响应化

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <h3>KVue</h3>
    <p>{{counter}}</p>
    <p k-text="counter"></p>
    <p k-html="desc"></p>

  </div>

  <script src="kvue.js"></script>

  <script>
    const app = new KVue({
      el:'#app',
      data: {
        counter: 1,
        desc: '<span style="color:red">kvue可还行</span>'
      }
    })
    setInterval(() => {
      app.counter++
    }, 1000);
    
  </script>
</body>
</html>
@前端进阶之旅: 代码已经复制到剪贴板

# 原理分析

  • new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
  • 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在 Compile中
  • 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  • 由于data的某个key在一个视图中可能出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
  • 将来data中数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

# 涉及类型介绍

  • KVue:框架构造函数
  • Observer:执行数据响应化(分辨数据是对象还是数组)
  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
  • Watcher:执行更新函数(更新dom)
  • Dep:管理多个Watcher,批量更新

# 实现Vue

# 框架构造函数:执行初始化

执行初始化,对data执行响应化处理,kvue.js

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return
  }

  // 响应式
  new Observer(obj)

}
@前端进阶之旅: 代码已经复制到剪贴板
function defineReactive(obj, key, val) {}

class KVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    observe(this.$data)
  }
}
class Observer {
  constructor(value) {
    this.value = value this.walk(value);
  }
  walk(obj) {
    Object.keys(obj).forEach(key = >{
      defineReactive(obj, key, obj[key])
    })
  }
}
@前端进阶之旅: 代码已经复制到剪贴板

为$data做代理

class KVue {
  constructor(options) {
    // 。。。
    proxy(this, '$data')
  }
}
function proxy(vm) {
  Object.keys(vm.$data).forEach(key = >{
    Object.defineProperty(vm, key, {
      get() {
        return vm.$data[key];
      },
      set(newVal) {
        vm.$data[key] = newVal;
      }
    });
  })
}
@前端进阶之旅: 代码已经复制到剪贴板

# 编译 Compile

编译模板中vue模板特殊语法,初始化视图、更新视图

1. 初始化视图

根据节点类型编译

class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      this.compile(this.$el);
    }
  }
  console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
  console.log("编译插值文本" + node.textContent);
}
if (node.childNodes && node.childNodes.length > 0) {
  this.compile(node);
}
});
}
isElement(node) {
  return node.nodeType == 1;

}
isInterpolation(node) {
  return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
@前端进阶之旅: 代码已经复制到剪贴板

编译插值

compile(el) { // ...
} else if (this.isInerpolation(node)) {
  // console.log("编译插值文本" + node.textContent); this.compileText(node);
}
});
}
compileText(node) {
  console.log(RegExp.$1);
  node.textContent = this.$vm[RegExp.$1];
}
@前端进阶之旅: 代码已经复制到剪贴板

编译元素

compile(el) { //...
  if (this.isElement(node)) {
    // console.log("编译元素" + node.nodeName); this.compileElement(node)
  }
}
compileElement(node) {
  let nodeAttrs = node.attributes;
  Array.from(nodeAttrs).forEach(attr = >{
    let attrName = attr.name;
    let exp = attr.value;
    if (this.isDirective(attrName)) {
      let dir = attrName.substring(2);
      this[dir] && this[dir](node, exp);
    }
  });
}
isDirective(attr) {
  return attr.indexOf("k-") == 0;
}
text(node, exp) {
  node.textContent = this.$vm[exp];
}
@前端进阶之旅: 代码已经复制到剪贴板

# 依赖收集

视图中会用到data中某key,这称为依赖。同一个key可能出现多次,每次都需要收集出来用一个

Watcher来维护它们,此过程称为依赖收集。

多个Watcher需要一个Dep来管理,需要更新时由Dep统一通知。 看下面案例,理出思路:

← vue状态管理之vuexdiff算法深入 →

fe