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


Posted in Javascript onMarch 09, 2017

分享前???br />

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

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

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

本文分享什么

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

100行代码理解和分析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上的两个属性nameage,但是当我改变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,taghtml 标签名,data 是包含诸如classstyle这些标签上的属性,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)

我们看一下运行结果

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

好我们解释一下 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组件学习教程进行学习。

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

Javascript 相关文章推荐
在浏览器中获取当前执行的脚本文件名的代码
Jul 19 Javascript
js新闻滚动 js如何实现新闻滚动效果
Jan 07 Javascript
JavaScript使用focus()设置焦点失败的解决方法
Sep 03 Javascript
jquery带有索引按钮且自动轮播切换特效代码分享
Sep 15 Javascript
jquery+正则实现统一的表单验证
Sep 20 Javascript
jQuery简单实现点击文本框复制内容到剪贴板上的方法
Aug 01 Javascript
angular分页指令操作
Jan 09 Javascript
jQuery实现select模糊查询(反射机制)
Jan 14 Javascript
React简单介绍
May 24 Javascript
JavaScript键盘事件常见用法实例分析
Jan 03 Javascript
layui 弹出层回调获取弹出层数据的例子
Sep 02 Javascript
JavaScript工具库MyTools详解
Jan 01 Javascript
js实现悬浮窗效果(支持拖动)
Mar 09 #Javascript
详解vuelidate 对于vueJs2.0的验证解决方案
Mar 09 #Javascript
Bootstrap禁用响应式布局的实现方法
Mar 09 #Javascript
微信小程序 template模板详解及实例代码
Mar 09 #Javascript
jQuery插件HighCharts绘制2D柱状图、折线图的组合双轴图效果示例【附demo源码下载】
Mar 09 #Javascript
canvas实现爱心和彩虹雨效果
Mar 09 #Javascript
vue如何实现observer和watcher源码解析
Mar 09 #Javascript
You might like
php 搜索框提示(自动完成)实例代码
2012/02/05 PHP
Codeigniter生成Excel文档的简单方法
2014/06/12 PHP
关于Yii2框架跑脚本时内存泄漏问题的分析与解决
2019/12/01 PHP
php中yii框架实例用法
2020/12/22 PHP
JScript中的&quot;this&quot;关键字使用方式补充材料
2007/03/08 Javascript
JavaScript中变量提升 Hoisting
2012/07/03 Javascript
struts2+jquery+json实现异步加载数据(自写)
2013/06/24 Javascript
javascript学习笔记(七)Ajax和Http状态码
2014/10/08 Javascript
详谈LABJS按需动态加载js文件
2015/05/07 Javascript
使用jQuery+EasyUI实现CheckBoxTree的级联选中特效
2015/12/06 Javascript
js移动端事件基础及常用事件库详解
2017/08/15 Javascript
在vue项目创建的后初始化首次使用stylus安装方法分享
2018/01/25 Javascript
JS/HTML5游戏常用算法之碰撞检测 包围盒检测算法详解【圆形情况】
2018/12/13 Javascript
jQuery实现的点击显示隐藏下拉菜单功能完整示例
2019/05/17 jQuery
python实现系统状态监测和故障转移实例方法
2013/11/18 Python
Python中的if、else、elif语句用法简明讲解
2016/03/11 Python
python编写分类决策树的代码
2017/12/21 Python
解决PySide+Python子线程更新UI线程的问题
2019/01/11 Python
Python异常继承关系和自定义异常实现代码实例
2020/02/20 Python
Python编程快速上手——强口令检测算法案例分析
2020/02/29 Python
基于PyTorch中view的用法说明
2021/03/03 Python
10个最常见的HTML5面试题 附答案
2016/06/06 HTML / CSS
Space NK美国站:英国高端美妆护肤商城
2017/05/22 全球购物
大都会艺术博物馆商店:The Met Store
2018/06/22 全球购物
优瑞自动咖啡机官网:Jura
2018/09/29 全球购物
美国领先的个性化礼品商城:Personalization Mall
2019/07/27 全球购物
安全生产责任书范本
2014/04/15 职场文书
带病坚持工作事迹
2014/05/03 职场文书
入党综合考察材料
2014/06/02 职场文书
缓刑人员思想汇报500字
2014/09/12 职场文书
大学生在校表现评语
2014/12/31 职场文书
小学少先队辅导员述职报告
2015/01/10 职场文书
光荣之路观后感
2015/06/12 职场文书
党员干部学习三严三实心得体会
2016/01/05 职场文书
浅谈什么是SpringBoot异常处理自动配置的原理
2021/06/21 Java/Android
Java实战之课程信息管理系统的实现
2022/04/01 Java/Android