Vue响应式原理最佳解答

看过vue官方文档的同学,对这张图应该已然相当熟悉了。

vue的响应式是如何实现的?

听过太多回答,通过Object.defineProperty,可是再详细的问时,对方浑然不知。

先撸为敬

const Observer = function(data) {
  // 循环修改为每个属性添加get set
  for (let key in data) {
    defineReactive(data, key);
  }
}

const defineReactive = function(obj, key) {
  // 局部变量dep,用于get set内部调用
  const dep = new Dep();
  // 获取当前值
  let val = obj[key];
  Object.defineProperty(obj, key, {
    // 设置当前描述属性为可被循环
    enumerable: true,
    // 设置当前描述属性可被修改
    configurable: true,
    get() {
      console.log('in get');
      // 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
      dep.depend();
      return val;
    },
    set(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      // 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher,
      // 这里每个需要更新通过什么断定?dep.subs
      dep.notify();
    }
  });
}

const observe = function(data) {
  return new Observer(data);
}

const Vue = function(options) {
  const self = this;
  // 将data赋值给this._data,源码这部分用的Proxy所以我们用最简单的方式临时实现
  if (options && typeof options.data === 'function') {
    this._data = options.data.apply(this);
  }
  // 挂载函数
  this.mount = function() {
    new Watcher(self, self.render);
  }
  // 渲染函数
  this.render = function() {
    with(self) {
      _data.text;
    }
  }
  // 监听this._data
  observe(this._data);  
}

const Watcher = function(vm, fn) {
  const self = this;
  this.vm = vm;
  // 将当前Dep.target指向自己
  Dep.target = this;
  // 向Dep方法添加当前Wathcer
  this.addDep = function(dep) {
    dep.addSub(self);
  }
  // 更新方法,用于触发vm._render
  this.update = function() {
    console.log('in watcher update');
    fn();
  }
  // 这里会首次调用vm._render,从而触发text的get
  // 从而将当前的Wathcer与Dep关联起来
  this.value = fn();
  // 这里清空了Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,
  // 造成代码死循环
  Dep.target = null;
}

const Dep = function() {
  const self = this;
  // 收集目标
  this.target = null;
  // 存储收集器中需要通知的Watcher
  this.subs = [];
  // 当有目标时,绑定Dep与Wathcer的关系
  this.depend = function() {
    if (Dep.target) {
      // 这里其实可以直接写self.addSub(Dep.target),
      // 没有这么写因为想还原源码的过程。
      Dep.target.addDep(self);
    }
  }
  // 为当前收集器添加Watcher
  this.addSub = function(watcher) {
    self.subs.push(watcher);
  }
  // 通知收集器中所的所有Wathcer,调用其update方法
  this.notify = function() {
    for (let i = 0; i < self.subs.length; i += 1) {
      self.subs[i].update();
    }
  }
}

const vue = new Vue({
  data() {
    return {
      text: 'hello world'
    };
  }
})

vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get 

这里我们用不到100行的代码,实现了一个简易的vue响应式。当然,这里如果不考虑期间的过程,我相信,40行代码之内可以搞定。但是我这里不想省略,为什么呢?我怕你把其中的过程自动忽略掉,怕别人问你相关东西的时候,明明自己看过了,却被怼的哑口无言。总之,我是为了你好,多喝热水。

Dep的作用是什么?

依赖收集器,这不是官方的名字蛤,我自己起的,为了好记。

用两个例子来看看依赖收集器的作用吧。

  • 例子1,毫无意义的渲染是不是没必要?

    const vm = new Vue({
        data() {
            return {
                text: 'hello world',
                text2: 'hey',
            }
        }
    }) 
    

    vm.text2的值发生变化时,会再次调用render,而template中却没有使用text2,所以这里处理render是不是毫无意义?

    针对这个例子还记得我们上面模拟实现的没,在Vuerender函数中,我们调用了本次渲染相关的值,所以,与渲染无关的值,并不会触发get,也就不会在依赖收集器中添加到监听(addSub方法不会触发),即使调用set赋值,notify中的subs也是空的。OK,继续回归demo,来一小波测试去印证下我说的吧。

    const vue = new Vue({
      data() {
        return {
          text: 'hello world',
          text2: 'hey'
        };
      }
    })
    
    vue.mount(); // in get
    vue._data.text = '456'; // in watcher update /n in get
    vue._data.text2 = '123'; // nothing 
    
  • 例子2,多个Vue实例引用同一个data时,通知谁?是不是应该俩都通知?

     let commonData = {
      text: 'hello world'
    };
    
    const vm1 = new Vue({
      data() {
        return commonData;
      }
    })
    
    const vm2 = new Vue({
      data() {
        return commonData;
      }
    })
    
    vm1.mount(); // in get
    vm2.mount(); // in get
    commonData.text = 'hey' // 输出了两次 in watcher update /n in get 
    

希望通过这两个例子,你已经大概清楚了Dep的作用,有没有原来就那么回事的感觉?有就对了。总结一下吧(以下依赖收集器实为Dep):

  • vuedata初始化为一个Observer并对对象中的每个值,重写了其中的getsetdata中的每个key,都有一个独立的依赖收集器。
  • get中,向依赖收集器添加了监听
  • 在mount时,实例了一个Watcher,将收集器的目标指向了当前Watcher
  • data值发生变更时,触发set,触发了依赖收集器中的所有监听的更新,来触发Watcher.update

为了帮助大家更好温习重点知识、更高效的准备面试,特别整理了《Vue 面试题总结》电子稿文件。

vue-cli工程

  • 构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么?
  • vue-cli 工程常用的 npm 命令有哪些?
  • 请说出vue-cli工程中文件夹和文件的用处
  • config文件夹 下 index.js 的对于工程 开发环境 和 生产环境 的配置
  • 请你详细介绍一些 package.json 里面的配置

vue核心知识点

  • 对于Vue是一套渐进式框架的理解
  • vue.js的两个核心是什么?
  • 请问 v-if 和 v-show 有什么区别
  • vue常用的修饰符
  • v-on可以监听多个方法吗?
  • vue中 key 值的作用
  • vue-cli工程升级vue版本
  • vue事件中如何使用event对象?
  • $nextTick的使用
  • Vue 组件中 data 为什么必须是函数
  • v-for 与 v-if 的优先级
  • vue中子组件调用父组件的方法
  • vue中 keep-alive 组件的作用
  • vue中如何编写可复用的组件?
  • 什么是vue生命周期?
  • vue生命周期钩子函数有哪些?
  • vue如何监听键盘事件中的按键?
  • vue更新数组时触发视图更新的方法
  • vue中对象更改检测的注意事项
  • 解决非工程化项目初始化页面闪动问题
  • v-for产生的列表,实现active的切换
  • v-model语法糖的组件中的使用
  • vue中自定义过滤器
  • vue等单页面应用及其优缺点
  • 什么是vue的计算属性?
  • vue-cli提供的几种脚手架模板
  • vue父组件如何向子组件中传递数据?
  • vue弹窗后如何禁止滚动条滚动?
  • 计算属性的缓存和方法调用的区别
  • vue-cli中自定义指令的使用

vue-router

  • vue-router如何响应 路由参数 的变化?
  • 完整的 vue-router 导航解析流程
  • vue-router有哪几种导航钩子( 导航守卫 )?
  • vue-router传递参数的几种方式
  • vue-router的动态路由匹配
  • vue-router如何定义嵌套路由?
  • 组件及其属性
  • vue-router实现路由懒加载( 动态加载路由 )
  • vue-router路由的两种模式
  • history路由模式配置及后台配置

vuex

  • 什么是vuex?
  • 使用vuex的核心概念
  • vuex在vue-cli中的应用
  • 在vue中使用vuex,修改state的值
  • vuex actions异步修改状态

http请求

  • Promise对象是什么?
  • axios、fetch与ajax有什么区别?
  • 什么是JS的同源策略和跨域问题?
  • 如何解决跨域问题?
  • axios有什么特点?

UI样式

  • .vue组件的scoped属性的作用
  • 如何让CSS只在当前组件中起作用?
  • vue-cli中常用的UI组件库
  • 如何适配移动端?【 经典 】
  • 移动端媒体查询
  • vue内容垂直和水平居中
  • vue-cli引入图片的方法
  • 移动端常见样式问题
  • 文本超出隐藏

常用功能

  • vue中如何实现tab切换功能?
  • vue中keep-alive 实现标签页组件缓存
  • vue中实现页面从右往左侧滑入效果
  • vue中父子组件如何相互调用方法?
  • vue中央事件总线的使用

MVVM设计模式

  • MVC、MVP与MVVM模式
  • MVC、MVP与MVVM的区别
  • MVVM的实现原理
  • Object.defineProperty()方法
  • ES6中定义的类和对象
  • JS中的文档碎片
  • 解构赋值
  • Array.from
  • Array.reduce
  • 递归的使用
  • Obj.keys()与Obj.defineProperty
  • 发布-订阅模式
  • vue项目优化,缩短首屏加载时间

深入拓展

  • vue开发命令 npm run dev 输入后的执行过程
  • vue的服务器端渲染
  • 从零写一个npm安装包
  • vue-cli中常用到的加载器
  • webpack的特点

篇幅有限,需要完整版PDF的朋友【点击这里】可以领取前端85道 Vue 面试题及解析。
posted @ 2021-07-14 22:14  Android程序员吴彦祖  阅读(102)  评论(0编辑  收藏  举报