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。
作者:杨川宝
来源:51CTO

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
如何防止rogue server破坏数据中心
管理员需要防止未经授权的变更对虚拟化环境带来负面影响,及时找到rogue server并决定是否将其从现有环境中移除,避免其对当前环境造成破坏。 理想情况下,应用和服务器能够一直不间断正常运行,不会出现资源和冲突等问题,数据中心的所有一切保持着十分和谐的关系。不幸的是,我们并非生活在这样的理想环境当中。虽然针对虚拟化环境编写文档和制定控制流程能够起到帮助作用,但是尽了所有努力之后,我们依然可能遇到rogue server问题,并且这种问题会产生严重影响。而这种问题通常是管理员工作计划过于紧张的结果。如果没有事先制定清晰的变更流程,那么后期可能需要更多的人参与到其中才能够完成原有目标。 尽管某些操作的最终目的是合理的,但是本质上其仍然属于对系统进行未经授权的变更。仅仅一个未经授权的变更也许不足以摧毁整个虚拟化基础架构,但是如果大量变更累加在一起,就有可能对系统造成严重破坏 。对于那些拥有关键系统控制权的管理员来说,得知变更是在超越其知识或者权限的情况下进行的是一件非常令人沮丧的事情,但是需要记住的是,这种变更从来都不是出于恶意的。作为虚拟化管理员,能够暂时后退一步,不将个人感受带到工作当...
- 下一篇
Spring Cloud与Docker微服务架构实战
作者简介 周立 Spring Cloud中国社区联合发起人之一,近7年的软件系统开发经验,多年系统架构经验;参与开发多个大型项目,有电信某电信网管项目、某O2O电商平台、某征信系统等;对Spring Cloud、微服务、持续集成、持续交付有一定见地。 热爱技术交流,曾代表公司参加全球微服务架构高峰论坛、QCon等技术沙龙。拥抱开源,多个项目开源在Github与Git@OSC上,并获得开源中国的推荐,例如电子书《使用Spring Cloud与Docker实战微服务》等。 目前,笔者的研究重心是使用Spring Cloud、Docker、微服务,著有《Spring Cloud与Docker微服务架构实战》,已在京东上架。 笔者博客:http://itmuch.com,定期分享Spring Cloud相关博客。 一、为什么要统一管理微服务配置 对于传
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
-
Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS8编译安装MySQL8.0.19
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
推荐阅读
最新文章
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- MySQL8.0.19开启GTID主从同步CentOS8
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS关闭SELinux安全模块
- CentOS7设置SWAP分区,小内存服务器的救世主
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题