怎么通过id渲染页面_完全理解Vue的渲染watcher、computed和user watcher

f5ec121acd92e304f779c2c2e095eeab.png

作者:Naice

https://segmentfault.com/a/1190000023196603

这篇文章将带大家全面理解vuewatchercomputeduser watcher,其实computeduser watcher都是基于Watcher来实现的,我们通过一个一个功能点去敲代码,让大家全面理解其中的实现原理和核心思想。所以这篇文章将实现以下这些功能点:

  • 实现数据响应式
  • 基于渲染wather实现首次数据渲染到界面上
  • 数据依赖收集和更新
  • 实现数据更新触发渲染watcher执行,从而更新ui界面
  • 基于watcher实现computed
  • 基于watcher实现user watcher

废话不要多说,先看下面的最终例子。

75b6d483a686f8dc0651db9b086fd135.gif

例子看完之后我们就直接开工了。

准备工作

首先我们准备了一个index.html文件和一个vue.js文件,先看看index.html的代码

html>
"en">

  "UTF-8">
  全面理解vue的渲染watcher、computed和user atcher


  
"root">

  
  


index.html里面分别有一个id是root的div节点,这是跟节点,然后在script标签里面,引入了vue.js,里面提供了Vue构造函数,然后就是实例化Vue,参数是一个对象,对象里面分别有data 和 render 函数。然后我们看看vue.js的代码:

function Vue (options) {

vue.js代码里面就是执行this._init()this.$mount()this._init的方法就是对我们的传进来的配置进行各种初始化,包括数据初始化initState(vm)、计算属性初始化initComputed(vm)、自定义watch初始化initWatch(vm)this.$mount方法把render函数渲染到页面中去、这些方法我们后面都写到,先让让大家了解整个代码结构。下面我们正式去填满我们上面写的这些方法。

实现数据响应式

要实现这些watcher首先去实现数据响应式,也就是要实现上面的initState(vm)这个函数。相信大家都很熟悉响应式这些代码,下面我直接贴上来。

function initState(vm) {

重要的点都在注释里面,主要核心就是给递归给data里面的数据设置getset,然后设置数据代理,让 this.name 等同于 this._data.name。设置完数据观察,我们就可以看到如下图的数据了。

d46ca6c7399cde97560b19eda4ef3c69.png
console.log(vue.name) // 张三
console.log(vue.age) // 10

ps: 数组的数据观察大家自行去完善哈,这里重点讲的是watcher的实现。

首次渲染

数据观察搞定了之后,我们就可以把render函数渲染到我们的界面上了。在Vue里面我们有一个this.$mount()函数,所以要实现Vue.prototype.$mount函数:

// 挂载方法
Vue.prototype.$mount = function () {
  const vm = this
  new Watcher(vm, vm.$options.render, () => {}, true)
}

以上的代码终于牵扯到我们Watcher这个主角了,这里其实就是我们的渲染wather,这里的目的是通过Watcher来实现执行render函数,从而把数据插入到root节点里面去。下面看最简单的Watcher实现

let wid = 0

通过上面的一顿操作,终于在render中终于可以通过this.name 读取到data的数据了,也可以插入到root.innerHTML中去。阶段性的工作我们完成了。如下图,完成的首次渲染✌️

69fad5f1303dfdd18b5160c9ab3f31bd.png

数据依赖收集和更新

首先数据收集,我们要有一个收集的地方,就是我们的Dep类,下面呢看看我们去怎么实现这个Dep

// 依赖收集
let dId = 0
class Dep{
  constructor() {
    this.id = dId++ // 每次实例化都生成一个id
    this.subs = [] // 让这个dep实例收集watcher
  }
  depend() {
    // Dep.target 就是当前的watcher
    if (Dep.target) {
      Dep.target.addDep(this) // 让watcher,去存放dep,然后里面dep存放对应的watcher,两个是多对多的关系
    }
  }
  notify() {
    // 触发更新
    this.subs.forEach(watcher => watcher.update())
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
}

let stack = []
// push当前watcher到stack 中,并记录当前watcer
function pushTarget(watcher) {
  Dep.target = watcher
  stack.push(watcher)
}
// 运行完之后清空当前的watcher
function popTarget() {
  stack.pop()
  Dep.target = stack[stack.length - 1]
}

Dep收集的类是实现了,但是我们怎么去收集了,就是我们数据观察的get里面实例化Dep然后让Dep收集当前的watcher。下面我们一步步来:

  • 1、在上面this.$mount()的代码中,我们运行了new Watcher(vm, vm.$options.render, () => {}, true),这时候我们就可以在Watcher里面执行this.get(),然后执行pushTarget(this),就可以执行这句话Dep.target = watcher,把当前的watcher挂载Dep.target上。下面看看我们怎么实现。
class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
    this.cb = cb
    this.options = options
    this.id = wid++
    this.id = wId++
+    this.deps = []
+    this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了
    this.value = this.get()
  }
  get() {
    const vm = this.vm
+   pushTarget(this)
    let value = this.getter.call(vm, vm) // 执行函数
+   popTarget()
    return value
  }
+  addDep(dep) {
+    let id = dep.id
+    if (!this.depsId.has(id)) {
+      this.depsId.add(id)
+      this.deps.push(dep)
+      dep.addSub(this);
+    }
+  }
+  update(){
+    this.get()
+  }
}
  • 2、知道Dep.target是怎么来之后,然后上面代码运行了this.get(),相当于运行了vm.$options.render,在render里面回执行this.name,这时候会触发Object.defineProperty·get方法,我们在里面就可以做些依赖收集(dep.depend)了,如下代码
function defineReactive(data, key, value) {
  • 3、调用的dep.depend() 实际上是调用了 Dep.target.addDep(this), 此时Dep.target等于当前的watcher,然后就会执行
addDep(dep) {
  let id = dep.id
  if (!this.depsId.has(id)) {
    this.depsId.add(id)
    this.deps.push(dep) // 当前的watcher收集dep
    dep.addSub(this); // 当前的dep收集当前的watcer
  }
}

这里双向保存有点绕,大家可以好好去理解一下。下面我们看看收集后的des是怎么样子的。

ca1207df229315104f1013d5682a1b18.png
  • 4、数据更新,调用this.name = '李四'的时候回触发Object.defineProperty.set方法,里面直接调用dep.notify(),然后循环调用所有的watcer.update方法更新所有watcher,例如:这里也就是重新执行vm.$options.render方法。

有了依赖收集个数据更新,我们也在index.html增加修改data属性的定时方法:

// index.html
"changeData()">改变name和age
// -----
// .....省略代码
function changeData() {
  vue.name = '李四'
  vue.age = 20
}

运行效果如下图

38c1fec8d4f8a4bfd75f9f54068086af.gif

到这里我们渲染watcher就全部实现了。

实现computed

首先我们在index.html里面配置一个computed,script标签的代码就如下:

'#root')

上面的代码,注意computed是在render里面使用了。

在vue.js中,之前写了下面这行代码。

if (options.computed) {

我们现在就实现这个initComputed,代码如下

// 初始化computed
function initComputed(vm) {
  const computed = vm.$options.computed // 拿到computed配置
  const watchers = vm._computedWatchers = Object.create(null) // 给当前的vm挂载_computedWatchers属性,后面会用到
  // 循环computed每个属性
  for (const key in computed) {
    const userDef = computed[key]
    // 判断是函数还是对象
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 给每一个computed创建一个computed watcher 注意{ lazy: true }
    // 然后挂载到vm._computedWatchers对象上
    watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true })
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

大家都知道computed是有缓存的,所以创建watcher的时候,会传一个配置{ lazy: true },同时也可以区分这是computed watcher,然后到watcer里面接收到这个对象

class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
+    if (options) {
+      this.lazy = !!options.lazy // 为computed 设计的
+    } else {
+      this.lazy = false
+    }
+    this.dirty = this.lazy
    this.cb = cb
    this.options = options
    this.id = wId++
    this.deps = []
    this.depsId = new Set()
+    this.value = this.lazy ? undefined : this.get()
  }
  // 省略很多代码
}

从上面这句this.value = this.lazy ? undefined : this.get()代码可以看到,computed创建watcher的时候是不会指向this.get的。只有在render函数里面有才执行。

现在在render函数通过this.info还不能读取到值,因为我们还没有挂载到vm上面,上面defineComputed(vm, key, userDef)这个函数功能就是让computed挂载到vm上面。下面我们实现一下。

set个

上面代码有看到在watcher中调用了watcher.evaluate()watcher.depend(),然后去watcher里面实现这两个方法,下面直接看watcher的完整代码。

class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
    if (options) {
      this.lazy = !!options.lazy // 为computed 设计的
    } else {
      this.lazy = false
    }
    this.dirty = this.lazy
    this.cb = cb
    this.options = options
    this.id = wId++
    this.deps = []
    this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了
    this.value = this.lazy ? undefined : this.get()
  }
  get() {
    const vm = this.vm
    pushTarget(this)
    // 执行函数
    let value = this.getter.call(vm, vm)
    popTarget()
    return value
  }
  addDep(dep) {
    let id = dep.id
    if (!this.depsId.has(id)) {
      this.depsId.add(id)
      this.deps.push(dep)
      dep.addSub(this);
    }
  }
  update(){
    if (this.lazy) {
      this.dirty = true
    } else {
      this.get()
    }
  }
  // 执行get,并且 this.dirty = false
+  evaluate() {
+    this.value = this.get()
+    this.dirty = false
+  }
  // 所有的属性收集当前的watcer
+  depend() {
+    let i = this.deps.length
+    while(i--) {
+      this.deps[i].depend()
+    }
+  }
}

代码都实现王完成之后,我们说下流程,

  • 1、首先在render函数里面会读取this.info,这个会触发createComputedGetter(key)中的computedGetter(key)
  • 2、然后会判断watcher.dirty,执行watcher.evaluate()
  • 3、进到watcher.evaluate(),才真想执行this.get方法,这时候会执行pushTarget(this)把当前的computed watcher push到stack里面去,并且把Dep.target 设置成当前的computed watcher`;
  • 4、然后运行this.getter.call(vm, vm) 相当于运行computedinfo: function() { return this.name + this.age },这个方法;
  • 5、info函数里面会读取到this.name,这时候就会触发数据响应式Object.defineProperty.get的方法,这里name会进行依赖收集,把watcer收集到对应的dep上面;并且返回name = '张三'的值,age收集同理;
  • 6、依赖收集完毕之后执行popTarget(),把当前的computed watcher从栈清除,返回计算后的值('张三+10'),并且this.dirty = false
  • 7、watcher.evaluate()执行完毕之后,就会判断Dep.target 是不是true,如果有就代表还有渲染watcher,就执行watcher.depend(),然后让watcher里面的deps都收集渲染watcher,这就是双向保存的优势。
  • 8、此时name都收集了computed watcher渲染watcher。那么设置name的时候都会去更新执行watcher.update()
  • 9、如果是computed watcher的话不会重新执行一遍只会把this.dirty 设置成 true,如果数据变化的时候再执行watcher.evaluate()进行info更新,没有变化的的话this.dirty 就是false,不会执行info方法。这就是computed缓存机制。

实现了之后我们看看实现效果:

6d2d09347bcb1cb52fce71a1e12ac026.gif

这里conputed的对象set配置没有实现,大家可以自己看看源码

watch实现

先在script标签配置watch配置如下代码:

'#root')

知道了computed实现之后,自定义watch实现很简单,下面直接实现initWatch

function initWatch(vm) {

然后修改一下Watcher,直接看Wacher的完整代码。

let wId = 0

最后看看效果75b6d483a686f8dc0651db9b086fd135.gif

当然很多配置没有实现,比如说options.immediate 或者options.deep等配置都没有实现。篇幅太长了。自己也懒~~~ 完结撒花

详细代码:https://github.com/naihe138/write-vue

感谢 · 转发欢迎大家留言

18db375fbf4cd35e2a2fb3217db9e393.png

好文章,我在看❤️

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/567477.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VS2015启动调试程序变慢

问题描述## 标题 vs2015编译速度很快,运行时不停显示加载xxx.dll动态库,加载很慢 解决方案## 标题 打开vs2015,依次点击工具-》选项-》调试-》符号,点击勾选去掉Microsoft符号服务器,清空符号缓存完毕 转载自VS2015启动调试程序变…

根可达算法的根_我的JVM(六):GC的基础概念以及GC算法

一、概述垃圾收集Garbage Collection通常被称为GC,但是GC一般也指Garbage Collecting(垃圾回收这个动作)或Garbage Collector(垃圾回收器),这些都是是JVM知识体系中非常重要的知识,也是程序员必须要掌握的技能,本文将详细讲述Java…

docker 删除包含关键字的镜像_30分钟带你轻松掌握Docker原理

前言Docker是什么?Docker是Go语言开发实现的容器。2013年发布至今,备受推崇。相关文档、学习资料十分详尽。近期有docker相关项目,得重新学习一下。博客以笔记为什么要使用 Docker?Docker 容器的启动在秒级Docker 对系统资源利用率…

pads中如何设置等长_如何在SQL Server中设置扩展,监控系统性能

dbForge Studio for SQL Server为有效的探索、分析SQL Server数据库中的大型数据集提供全面的解决方案,并设计各种报表以帮助作出合理的决策。dbForge Studio for SQL Server​www.evget.com扩展事件是一种有用且方便的解决方案,旨在监视您的系统性能。它…

iar stm32_STM32延时函数的四种方法

关注、星标公众号,不错过精彩内容单片机编程过程中经常用到延时函数,最常用的莫过于微秒级延时delay_us()和毫秒级delay_ms()。本文基于STM32F207介绍4种不同方式实现的延时函数。普通延时这种延时方式应该是大家在51单片机时候,接触最早的延…

使用pm2启动node文件_PM2 是什么

目录 pm2是什么特点示例说明配置文件常用命令背景 由于需要在容器云新增一个测试环境,改了代码相关的配置后,进行部署。发现服务一直启动不了。在和运维一起排查问题,他看到pm2的一些信息, 问我pm2是不是阻塞了,并不是…

CP/M世界上第一个微机操作系统

CP/M世界上第一个微机操作系统 微软宣布2014年4月8日将不再对Windows XP系统进行更新,宣告这个存活了13年的史上寿命最长的微机操作系统基本上寿终正寝。很多人都知道这是个脱胎于DOS的系统,也潜意识中认为DOS是微机操作系统的鼻祖。不过,这大…

安卓 usb音量调节_戴尔推出面向Teams和Zoom视频通话的USB-C免提适配器

COVID-19 大流行期间,许多企业将会议安排在了线上举办。为方便通过 Microsoft Teams 和 Zoom 之类云视讯平台进行沟通的笔记本电脑用户,戴尔特地推出了新款 USB-C 音频适配器。作为一款两用配件,MH3021P 不仅可以作为有线通话的免提适配器(集…

word表格美化技巧:如何统一改变表格的样式

在Word中插入的表格默认都是黑边白底,看上去非常的单调。比如这样: 很多小伙伴都是在新建表格并且填完数据之后,应用表格样式,来提升档次,如下: 但是,每次新建表格后再选择应用【表格样式】&…

banner信息是什么_我的设计成长笔记—第10篇(banner)

Banner2019年5月9日这里是我的设计成长笔记—————————————————Banner为什么单独拿出来聊因为我有差不多一年的时间都在做这个在我的成长过程中,banner是过渡从平面/美工到UI设计的一个过程。在这个过程中,我对设计的理解分成了三个部分第…

后台原理_电气控制原理动图22张,超赞!

今天从低压电器、电动机及控制线路、传感器及控制原理三部分来分享22张超赞的原理动图。低压电器部分1按钮开关2闸刀开关3行程开关4交流接触器5热继电器6时间继电器7速度继电器电动机及控制线路1异步电动机2直流电动机3步进电动机4永磁电机5正反转控制6自动往返控制7顺序控制8多…

[word技巧]把标题、图表题注编号由“一.1”改为“1.1“

一、问题描述 写作中文报告时,有时会要求一级标题用类似”第一章”的中文编号,二级标题用1.1这类编号,图表题注用“表1.1”的编号。此时,由于一级标题用的是中文数字(如“一”)作为序号,因此wor…

excel中怎样用公式获取表单控件_挑战高手:用不到 100 行代码,在前端实现 Excel 全部功能...

(点击图片获取专属你的开发工具)SpreadJS是一款基于 HTML5 的纯前端表格控件,兼容 450 种以上的 Excel 公式,具备“高性能、跨平台、与 Excel 高度兼容”的产品特性广受世界各地名企追捧。接下来让我们看看其具有哪些独特优势吧!四大优势&…

vissim免修改时间工具_App闪退怎么办?免越狱如何安装未签名的App?

由于最近苹果大规模的封签名,导致在第三方渠道下载的软件都出现闪退没法使用的现象。目前的替代方法就是使用电脑端 Cydia Impactor 来给 App 进行自签,虽然相对比较麻烦,但这也是目前针对企业签名无法使用的唯一解决方法。下面以安装 FilzaE…

word表格美化技巧:如何统一改变表格的样式2+续表制作

一、表格样式 开始-样式-新建样式-样式类型:表格。 单独设置标题行,汇总行等格式即可。表格样式会显示在-设计-样式-表格样式中。 问题:汇总行无效。 即可。 二、续表 直接复制表格及题注到下一页,然后下下面表格的题注修改为…

excel条件格式详解

1、基于公式返回的逻辑值去判断,True则执行条件格式,FALSE不执行。 2、基于区域第一个单元格的公式,依次根据单元格引用方式判断区域内公式是否成立。 函数: 使用条件格式: 函数: 条件格式: 函…

ccf a类会议_CCF推荐 | 人工智能领域顶级会议:截稿日期批量速递

以下为已公布截稿日期的CCF推荐,人工智能领域会议,按照时间排序。个别会议因为没有公布截稿信息而不在所列时间线内。录取率信息为网络上可获得的最近年份数据,不一定是上一年的,文中尽量选择同时带有投稿量的数据年份供大家参考.…

python递归调用详解_Python递归调用自己的函数

原博文 2019-11-16 10:36 − def fact(x): if x 1: return 1 else: return x * fact(x-1) ...0191 相关推荐 2019-09-28 21:13 − Python python是一种跨平台的计算机程序设计语言,是一种面向对象的动态类型语言。 最初被设计用于编写自动化脚本(shell)&#xff0c…

vue跳转到外部链接_前端实战项目:Vue.js实现外卖平台webapp,饿了么项目的翻版...

链接:https://github.com/ljianshu/mt-app适合没有经验的朋友。项目涉及到技术栈:vue全家桶:Vue、Vue-router、Vue-cli等 组件化:单Vue文件 模块化:ES6 Module 第三方模块:better-scroll axios等 基础开发环…