100行代码理解和分析vue2.0响应式架构

分享前啰嗦

我之前介绍过vue1.0如何实现observer和watcher。本想继续写下去,可是vue2.0横空出世..所以直接看vue2.0吧。这篇文章在公司分享过,终于写出来了。我们采用用最精简的代码,还原vue2.0响应式架构实现。

以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考。

不过不看也没关系,但是最好了解下Object.defineProperty

本文分享什么

理解vue2.0的响应式架构,就是下面这张图

顺带介绍他比react快的其中一个原因

本分实现什么

const demo = new Vue({
 data: {
 text: "before",
 },
 //对应的template 为 <div><span>{{text}}</span></div>
 render(h){
 return h('div', {}, [
 h('span', {}, [this.__toString__(this.text)])
 ])
 }
})
 setTimeout(function(){
 demo.text = "after"
 }, 3000)

对应的虚拟dom会从

<div><span>before</span></div> 变为 <div><span>after</span></div>

好,开始吧!!!

第一步,讲data 下面所有属性变为observable

来来来先看代码吧

 class Vue {
 constructor(options) {
 this.$options = options
 this._data = options.data
 observer(options.data, this._update)
 this._update()
 }
 _update(){
 this.$options.render()
 }
 }

 function observer(value, cb){
 Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
 }

 function defineReactive(obj, key, val, cb) {
 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: ()=>{},
 set:newVal=> {
 cb()
 }
 })
 }

 var demo = new Vue({
 el: '#demo',
 data: {
 text: 123,
 },
 render(){
 console.log("我要render了")
 }
 })

 setTimeout(function(){
 demo._data.text = 444
 }, 3000)

为了好演示我们只考虑最简单的情况,如果看了vue 源码分析之如何实现observer和watcher可能就会很好理解,不过没关系,我们三言两语再说说,这段代码要实现的功能就是将

 var demo = new Vue({
 el: '#demo',
 data: {
 text: 123,
 },
 render(){
 console.log("我要render了")
 }
 })

中data 里面所有的属性置于 observer,然后data里面的属性,比如 text 以改变,就引起_update()函数调用进而重新渲染,是怎样做到的呢,我们知道其实就是赋值的时候就要改变对吧,当我给data下面的text 赋值的时候 set 函数就会触发,这个时候 调用 _update 就ok了,但是

 setTimeout(function(){
 demo._data.text = 444
 }, 3000)

demo._data.text没有demo.text用着爽,没关系,我们加一个代理

 _proxy(key) {
 const self = this
 Object.defineProperty(self, key, {
 configurable: true,
 enumerable: true,
 get: function proxyGetter () {
 return self._data[key]
 },
 set: function proxySetter (val) {
 self._data[key] = val
 }
 })
 }

然后在Vue的constructor加上下面这句

Object.keys(options.data).forEach(key => this._proxy(key))

第一步先说到这里,我们会发现一个问题,data中任何一个属性的值改变,都会引起
_update的触发进而重新渲染,属性这显然不够精准啊

第二步,详细阐述第一步为什么不够精准

比如考虑下面代码

 new Vue({
 template: `
 <div>
 <section>
 <span>name:</span> {{name}}
 </section>
 <section>
 <span>age:</span> {{age}}
 </section>
 <div>`,
 data: {
 name: 'js',
 age: 24,
 height: 180
 }
 })

 setTimeout(function(){
 demo.height = 181
 }, 3000)

template里面只用到了data上的两个属性name和age,但是当我改变height的时候,用第一步的代码,会不会触发重新渲染?会!,但其实不需要触发重新渲染,这就是问题所在!!

第三步,上述问题怎么解决
简单说说虚拟 DOM
首先,template最后都是编译成render函数的(具体怎么做,就不展开说了,以后我会说的),然后render 函数执行完就会得到一个虚拟DOM,为了好理解我们写写最简单的虚拟DOM

 function VNode(tag, data, children, text) {
 return {
 tag: tag,
 data: data,
 children: children,
 text: text
 }
 }

 class Vue {
 constructor(options) {
 this.$options = options
 const vdom = this._update()
 console.log(vdom)
 }
 _update() {
 return this._render.call(this)
 }
 _render() {
 const vnode = this.$options.render.call(this)
 return vnode
 }
 __h__(tag, attr, children) {
 return VNode(tag, attr, children.map((child)=>{
 if(typeof child === 'string'){
 return VNode(undefined, undefined, undefined, child)
 }else{
 return child
 }
 }))
 }
 __toString__(val) {
 return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
 }
 }

 var demo = new Vue({
 el: '#demo',
 data: {
 text: "before",
 },
 render(){
 return this.__h__('div', {}, [
 this.__h__('span', {}, [this.__toString__(this.text)])
 ])
 }
 })

我们运行一下,他会输出

 {
 tag: 'div',
 data: {},
 children:[
 {
 tag: 'span',
 data: {},
 children: [
 {
 children: undefined,
 data: undefined,
 tag: undefined,
 text: '' // 正常情况为 字符串 before,因为我们为了演示就不写代理的代码,所以这里为空
 }
 ]
 }
 ]
 }

这就是 虚拟最简单虚拟DOM,tag是html 标签名,data 是包含诸如class和style这些标签上的属性,childen就是子节点,关于虚拟DOM就不展开说了。

回到开始的问题,也就是说,我得知道,render 函数里面依赖了vue实例里面哪些变量(只考虑render 就可以,因为template 也会是帮你编译成render)。叙述有点拗口,还是看代码吧

 var demo = new Vue({
 el: '#demo',
 data: {
 text: "before",
 name: "123",
 age: 23
 },
 render(){
 return this.__h__('div', {}, [
 this.__h__('span', {}, [this.__toString__(this.text)])
 ])
 }
 })

就像这段代码,render 函数里其实只依赖text,并没有依赖name和age,所以,我们只要text改变的时候,我们自动触发render 函数 让它生成一个虚拟DOM就ok了(剩下的就是这个虚拟DOM和上个虚拟DOM做比对,然后操作真实DOM,只能以后再说了),那么我们正式考虑一下怎么做

第三步,'touch' 拿到依赖

回到最上面那张图,我们知道data上的属性设置defineReactive后,修改data 上的值会触发set。
那么我们取data上值是会触发get了。
对,我们可以在上面做做手脚,我们先执行一下render,我们看看data上哪些属性触发了get,我们岂不是就可以知道 render 会依赖data 上哪些变量了。
然后我么把这些变量做些手脚,每次这些变量变的时候,我们就触发render。
上面这些步骤简单用四个子概括就是 计算依赖。
(其实不仅是render,任何一个变量的改别,是因为别的变量改变引起,都可以用上述方法,也就是computed 和 watch 的原理,也是mobx的核心)

第一步:
我们写一个依赖收集的类,每一个data 上的对象都有可能被render函数依赖,所以每个属性在defineReactive时候就初始化它,简单来说就是这个样子的。

 class Dep {
 constructor() {
 this.subs = []
 }
 add(cb) {
 this.subs.push(cb)
 }
 notify() {
 console.log(this.subs);
 this.subs.forEach((cb) => cb())
 }
 }
 function defineReactive(obj, key, val, cb) {
 const dep = new Dep()
 Object.defineProperty(obj, key, {
 // 省略
 })
 }

然后,当执行render 函数去'touch'依赖的时候,依赖到的变量get就会被执行,然后我们就可以把这个 render 函数加到 subs 里面去了。
当我们,set 的时候 我们就执行 notify 将所有的subs数组里的函数执行,其中就包含render 的执行。
至此就完成了整个图,好我们将所有的代码展示出来

 function VNode(tag, data, children, text) {
 return {
 tag: tag,
 data: data,
 children: children,
 text: text
 }
 }

 class Vue {
 constructor(options) {
 this.$options = options
 this._data = options.data
 Object.keys(options.data).forEach(key => this._proxy(key))
 observer(options.data)
 const vdom = watch(this, this._render.bind(this), this._update.bind(this))
 console.log(vdom)
 }
 _proxy(key) {
 const self = this
 Object.defineProperty(self, key, {
 configurable: true,
 enumerable: true,
 get: function proxyGetter () {
 return self._data[key]
 },
 set: function proxySetter (val) {
 self._data.text = val
 }
 })
 }
 _update() {
 console.log("我需要更新");
 const vdom = this._render.call(this)
 console.log(vdom);
 }
 _render() {
 return this.$options.render.call(this)
 }
 __h__(tag, attr, children) {
 return VNode(tag, attr, children.map((child)=>{
 if(typeof child === 'string'){
 return VNode(undefined, undefined, undefined, child)
 }else{
 return child
 }
 }))
 }
 __toString__(val) {
 return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
 }
 }

 function observer(value, cb){
 Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
 }

 function defineReactive(obj, key, val, cb) {
 const dep = new Dep()
 Object.defineProperty(obj, key, {
 enumerable: true,
 configurable: true,
 get: ()=>{
 if(Dep.target){
 dep.add(Dep.target)
 }
 return val
 },
 set: newVal => {
 if(newVal === val)
 return
 val = newVal
 dep.notify()
 }
 })
 }
 function watch(vm, exp, cb){
 Dep.target = cb
 return exp()
 }

 class Dep {
 constructor() {
 this.subs = []
 }
 add(cb) {
 this.subs.push(cb)
 }
 notify() {
 this.subs.forEach((cb) => cb())
 }
 }
 Dep.target = null

 var demo = new Vue({
 el: '#demo',
 data: {
 text: "before",
 },
 render(){
 return this.__h__('div', {}, [
 this.__h__('span', {}, [this.__toString__(this.text)])
 ])
 }
 })

 setTimeout(function(){
 demo.text = "after"
 }, 3000)

我们看一下运行结果

好我们解释一下 Dep.target 因为我们得区分是,普通的get,还是在查找依赖的时候的get,所有我们在查找依赖时候,我们将

 function watch(vm, exp, cb){
 Dep.target = cb
 return exp()
 }

Dep.target 赋值,相当于 flag 一下,然后 get 的时候

 get: () => {
 if (Dep.target) {
 dep.add(Dep.target)
 }
 return val
 },

判断一下,就好了。到现在为止,我们再看那张图是不是就清楚很多了?

总结

我非常喜欢,vue2.0 以上代码为了好展示,都采用最简单的方式呈现。

不过整个代码执行过程,甚至是命名方式都和vue2.0一样。

对比react,vue2.0 自动帮你监测依赖,自动帮你重新渲染,而react 要实现性能最大化,要做大量工作,比如我以前分享的:

react如何性能达到最大化(前传),暨react为啥非得使用immutable.js
react 实现pure render的时候,bind(this)隐患。

而vue2.0 天然帮你做到了最优,而且对于像万年不变的 如标签上静态的class属性,vue2.0 在重新渲染后做diff 的时候是不比较的,vue2.0比达到性能最大化的react 还要快的一个原因。
然后源码在此,喜欢的记得给个star 哦
后续,我会简单聊聊,vue2.0的diff。

本文已被整理到了《Vue.js前端组件学习教程》,欢迎大家学习阅读。

关于vue.js组件的教程,请大家点击专题vue.js组件学习教程进行学习。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Vue.js每天必学之内部响应式原理探究

    深入响应式原理 大部分的基础内容我们已经讲到了,现在讲点底层内容.Vue.js 最显著的一个功能是响应系统 -- 模型只是普通对象,修改它则更新视图.这让状态管理非常简单且直观,不过理解它的原理也很重要,可以避免一些常见问题.下面我们开始深挖 Vue.js 响应系统的底层细节. 如何追踪变化 把一个普通对象传给 Vue 实例作为它的 data 选项,Vue.js 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter.这是 ES5 特性,不能打补丁

  • 谈谈对vue响应式数据更新的误解

    对于刚接触vue的同学会经常遇到数据更新了但是模板没有更新的问题,下面将结合vue的响应式特性以及异步更新机制分析常见的错误: 异步更新带来的数据响应式误解 异步数据的处理基本是一定会遇到的,处理不好就会遇到数据不更新的问题,但有一种情况是在未正确处理的情况下也能正常更新,这就会造成一种误解,详情如下所示: 模板 <div id="app"> <h2>{{dataObj.text}}</h2> </div> js new Vue({ el

  • Vue响应式原理详解

    Vue 嘴显著的特性之一便是响应式系统(reactivity system),模型层(model)只是普通JavaScript对象,修改它则更新视图(view). Vue 响应式系统的底层细节 如何追踪变化 把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象的所有属性,并使用Object.defineProperty 把这些属性全部转为 getter/setter.Object.defineProperty是仅ES5支持,并无法shim的特性,这也就是为什么Vu

  • Vue响应式添加、修改数组和对象的值

    有些时候,不得不想添加.修改数组和对象的值,但是直接添加.修改后又失去了getter.setter. 由于 JavaScript 的限制, Vue 不能检测以下变动的数组: 1. 利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue 2. 修改数组的长度时,例如: vm.items.length = newLength 为了避免第一种情况,以下两种方式将达到像 vm.items[indexOfItem] = newValue 的效果, 同时也将触发状

  • 100行代码理解和分析vue2.0响应式架构

    分享前啰嗦 我之前介绍过vue1.0如何实现observer和watcher.本想继续写下去,可是vue2.0横空出世..所以直接看vue2.0吧.这篇文章在公司分享过,终于写出来了.我们采用用最精简的代码,还原vue2.0响应式架构实现. 以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考. 不过不看也没关系,但是最好了解下Object.defineProperty 本文分享什么 理解vue2.0的响应式架构,就是下面这张图 顺带介绍他比rea

  • 浅谈实现vue2.0响应式的基本思路

    最近看了vue2.0源码关于响应式的实现,以下博文将通过简单的代码还原vue2.0关于响应式的实现思路. 注意,这里只是实现思路的还原,对于里面各种细节的实现,比如说数组里面数据的操作的监听,以及对象嵌套这些细节本实例都不会涉及到,如果想了解更加细节的实现,可以通过阅读源码 observer文件夹以及instance文件夹里面的state文件具体了解. 首先,我们先定义好实现vue对象的结构 class Vue { constructor(options) { this.$options = o

  • 100行代码实现一个vue分页组功能

    今天用vue来实现一个分页组件,总体来说,vue实现比较简单,样式部分模仿了elementUI.所有代码的源码可以再github上下载的到:下载地址 先来看一下实现效果: 点击查看效果 整体思路 我们先看一下使用到的文件的目录: 我们在 pageComponentsTest.vue 页面引入了 pageComponent.vue 分页组件.整体思路是通过 props 来达到组件的灵活通用的效果,整体语法是使用vue的VM语法. pageComponent.vue实现 首先实现一个分页,需要知道数

  • 100行代码实现vue表单校验功能(小白自编)

    两个文件,一个写逻辑,一个写校验规则: 特点:逻辑简单,代码量少,够用: 不想看代码直接新建这两个文件复制代码,看最下面的使用方法: 示例图片 //validator.js //引入校验规则 var valitatorRules = require('./valitator-rules.js'); export const Validator=function(formName,rules,errors){ // rules:{ // name:'required|regexp_hanzi',

  • 100 行代码实现Flutter自定义TabBar的示例代码

    Flutter 的确很强大,但美中不足的是生态还有待完善,没有出现像前端的 Antd 或 Element 那样全能的基础 UI 库. 由此带来的直接影响是开发效率提不上去,需要耗费大量的时间精力在基础组件的封装上. 官方的 TabBar 不满足需求,又没有合适的轮子,只好自己造轮子啦.接下来带你一步步实现自定义 TabBar-- 一.目标和效果 需求目标是: 这个页面不要 material 左侧统一的返回键和 Title 在右侧有取消按钮,点取消即返回 点击 Tab 可以实现 content 切

  • python使用dabl几行代码实现数据处理分析及ML自动化

    目录 dabl 1.数据预处理 2.探索性数据分析 3.建模 结论 数据科学模型开发涉及各种组件,包括数据收集.数据处理.探索性数据分析.建模和部署.在训练机器学习或深度学习模型之前,必须清洗数据集并使其适合训练.通常这些过程是重复的,且占用了大部时间. 为了克服这个问题,今天我分享一个名为 dabl 的开源 Python 工具包,它可以自动化机器学习模型开发,包括数据预处理.特征可视化和分析.建模.欢迎收藏学习,喜欢点赞支持. dabl dabl 是一个数据分析基线库,可以让机器学习建模更容易

  • Vue2.x响应式简单讲解及示例

    一.回顾Vue响应式用法 ​ vue响应式,我们都很熟悉了.当我们修改vue中data对象中的属性时,页面中引用该属性的地方就会发生相应的改变.避免了我们再去操作dom,进行数据绑定. 二.Vue响应式实现分析 对于vue的响应式原理,官网上给了出文字描述 https://cn.vuejs.org/v2/guide/reactivity.html . vue内部主要是通过数据劫持和观察者模式实现的 数据劫持: vue2.x内部使用Object.defineProperty https://dev

  • vue2之响应式双向绑定,在对象或数组新增属性页面无响应的情况

    目录 vue2响应式双向绑定,在对象或数组新增属性页面无响应 问题描述 解决方法 vue2实现响应式数据 JS中的对象属性 利用Object.defineProperty()进行数据劫持 与标签联动 v-model的实现 总结 vue2响应式双向绑定,在对象或数组新增属性页面无响应 问题描述 vue2 中可以将数据与视图进行绑定,修改 data 对象的属性值将引起对应视图的改变. Vue2的数据视图绑定是通过JS特性这一语法实现,其使用中存在数据属性丢失的这 一 bug,主要针对 对象或数组 属

  • 一文带你了解vue3.0响应式

    目录 使用案例 reactive API相关的流程 reactive createReactiveObject 创建响应式对象 mutableHandlers 处理函数 get函数 get函数的的调用时机 track 收集依赖 set函数 trigger 分发依赖 get和副作用渲染函数关联 副作用渲染函数的执行过滤 结尾 我们知道Vue 2.0是利用Ojbect.defineProperty对对象的已有属性值的读取和修改进行劫持,但是这个API不能监听对象属性的新增和删除,此外为了深度劫持对象

  • vue3.0响应式函数原理详细

    目录 1.reactive 2.ref 3.toRefs 4.computed 前言: Vue3重写了响应式系统,和Vue2相比底层采用Proxy对象实现,在初始化的时候不需要遍历所有的属性再把属性通过defineProperty转换成get和set.另外如果有多层属性嵌套的话只有访问某个属性的时候才会递归处理下一级的属性所以Vue3中响应式系统的性能要比Vue2好. 接下来我们自己实现Vue3响应式系统的核心函数(reactive/ref/toRefs/computed/effect/trac

随机推荐