VueJs源码分析-如何实现 observer 和 watcher
Posted VueJs技术分享
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了VueJs源码分析-如何实现 observer 和 watcher相关的知识,希望对你有一定的参考价值。
本文分享什么
理解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>
ok,let begin!!!
第一步, 讲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)
为了好演示我们只考虑最简单的情况
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] }, et: 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 要实现性能最大化,要做大量工作,
而 vue2.0 天然帮你做到了最优,而且对于像万年不变的 如标签上静态的class
属性,
vue2.0 在重新渲染后做diff 的时候是不比较的,vue2.0比 达到性能最大化的react 还要快的一个原因
后续,我会简单聊聊,vue2.0的diff。
以上是关于VueJs源码分析-如何实现 observer 和 watcher的主要内容,如果未能解决你的问题,请参考以下文章
如何在 vuejs 中获取价值 [__ob__: Observer]