Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue 依赖收集(为什么要依赖收集?如何依赖收集) #7

Open
AnnVoV opened this issue May 3, 2018 · 1 comment
Open

Comments

@AnnVoV
Copy link
Owner

AnnVoV commented May 3, 2018

Vue2中Dep, Observer 与Watcher 之间的关系(不含patch部分)

IMAGE

IMAGE

最简单的理解

按照我原先最简单的想法,我们既然给data 进行了defineProperty设置了getter和setter,为什么我们还需要依赖收集呢?为什么要引入Dep与Watcher?

为什么要引入依赖收集

  • 例子1:
new Vue({
    template: 
        `<div>
            <span>text1:</span> {{text1}}
            <span>text2:</span> {{text2}}
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});

  text3在实际模板中并没有被用到,然而当text3的数据被修改的时候(this.text3 = 'test')的时候,同样会触发text3的setter,按照原先想法,这会导致重新执行渲染,这显然不正确。

  • 例子2:
var globalData = {
  a: 1,
  b: 2
};
new Vue({
  template:`<div>{{a+b}}</div>`,
  data: (){
    return globalData;
  }
})

​ 当globalData.a 发生变化或者当globalData.b发生变化时,我们的视图都需要更新,所以我们要收集这个视图依赖于数据a 和 数据b。因此我们需要依赖收集。

前置知识

vue官网在线模板编译

https://cn.vuejs.org/v2/guide/render-function.html#%E6%A8%A1%E6%9D%BF%E7%BC%96%E8%AF%91

ASTNode 类型(Abstract Syntax Tree)

  • ASTElement type:1
  • ASTText type:2
  • ASTExpression type:3

render 函数一些函数定义

  • _c createElement
  • _m renderStatic(渲染静态结点)
  • _v createTextNode(创建文本dom)
  • _s toString(转换为字符串)

VNode 结构

VNode: {
	tag: string | void; // 标签名
	data: VNodeData | void;  // 结点相关属性数据
	children: ?Array<VNode>; // 子节点
	text: string | void;  // 文本
	elm: Node | void;  // dom元素 
	ns: string | void;  // 命名空间
	context: Component | void; // VNode所处Vue对象
	functionalContext: Component | void; // only for functional component root nodes
	key: string | number | void; 
	componentOptions: VNodeComponentOptions | void; // VNode对象如果对应的是一个自定义组件,componentOptions保存组件相关事件、props数据等
	componentInstance: Component | void; // VNode对象如果对应的是一个自定义组件,componentInstance保存相对应的vue实例
	parent: VNode | void; // 当前自定义组件在父组件中的vnode
	raw: boolean; // contains raw HTML? (server only)
	isStatic: boolean; // 是否是静态内容
	isRootInsert: boolean; // necessary for enter transition check
	isComment: boolean; // empty comment placeholder?
	isCloned: boolean; // 是否是clone的VNode对象
	isOnce: boolean; // 是否是v-once元素的VNode对象
}

真实DOM有什么问题,为什么要去使用虚拟DOM

  每个DOM上的属性多达 228 个,而这些属性有 90% 多对我们来说都是无用的。VNode 就是简化版的真实 DOM 元素,保留了我们要的属性,并新增了一些在 diff 过程中需要使用的属性,例如 isStatic。
【总结】Virtual DOM 就是一个js对象,用它来更轻量地描述DOM

入口文件查找

  • 从package.json我们看到
"scripts": {
  "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"
}
  • 从scripts/config.js我们看到
// Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },
  • 从web/entry-runtime-with-compiler.js我们看到
import Vue from './runtime/index'

然后--> /src/core/index.js --> /src/core/instance/index.js
最终在instance/index.js里面找到Vue的构造函数

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

设置情景分析

​ 假设我们的实际场景为下面的脚本, 我们下面来分析一下

var data = {
    a:1,
    b:2
  };
  new Vue({
    el:'#app',
    template: `\
        <section>\
		        <div>{{a+b}}</div>\
		        <p>静态文本<a href="www.koala.com">考拉地址</a></p>\
		    </section>\
		        `,
    data(){
      return data;
    }
  });

_init 入口函数

Vue.prototype._init = function (options) {
  initLifecycle(vm);
  initEvents(vm);
  initRender(vm);
  callHook(vm, 'beforeCreate');
  // 注意:beforeCreate阶段完成后,我们的options被merge到了vm.$options属性上,此时是获取不到this.xxx数据的,如果我们要获取data数据需要this.$options.data()来获取  
  initInjections(vm);
  // 对data进行了一些Observe 执行了defineReactive 标记1
  initState();
  initProvide(vm); // resolve provide after data/props
  callHook(vm, 'created');
  /* istanbul ignore if */
  if ("development" !== 'production' && config.performance && mark) {
    vm._name = formatComponentName(vm, false);
    mark(endTag);
    measure(("vue " + (vm._name) + " init"), startTag, endTag);
  }
  if (vm.$options.el) {
  // 对模板进行了compile 生成了render function, 调用render function 生成了vmdom
    vm.$mount(vm.$options.el);
  }
}

阶段一 initState()

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

​ initState方法里面有调用initData,在initData方法里面最后调用了observe(data, true)。那我们来看下observe 方法

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
    "development" !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    );
  }
  // proxy data on instance
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    {
      if (methods && hasOwn(methods, key)) {
        warn(
          ("Method \"" + key + "\" has already been defined as a data property."),
          vm
        );
      }
    }
    if (props && hasOwn(props, key)) {
      "development" !== 'production' && warn(
        "The data property \"" + key + "\" is already declared as a prop. " +
        "Use prop default value instead.",
        vm
      );
    } else if (!isReserved(key)) {
      proxy(vm, "_data", key);
    }
  }
  // observe data 我们重点看这里
  observe(data, true /* asRootData */);
}

function observe (value, asRootData) {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 重点看这里
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

// 然后我们再看Observer 类
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
	// 会进入walk方法,相当于在遍历我们的value
    this.walk(value);
  }
};

Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
	// 这边开始遍历data的key, 每一次遍历都会new Dep()      
    defineReactive(obj, keys[i], obj[keys[i]]);
  }
};

// defineReactive 里面就进入了我们所熟悉的defineProperty方法
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  
  Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
		// 这里有一点要注意,这里的getter是个闭包,所以我们每个key生成的dep都存了下来
        const value = getter ? getter.call(obj) : val
        // Dep.target 是什么呢?后面会讲
        if (Dep.target) {
          dep.depend()
          if (childOb) {
            childOb.dep.depend()
            if (Array.isArray(value)) {
              dependArray(value)
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        const value = getter ? getter.call(obj) : val //先算出原先的值
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
        }
        if (setter) {
          setter.call(obj, newVal)
        } else {
          val = newVal
        }
        childOb = !shallow && observe(newVal) //observe(newvalue) 但是如果只是value会直接return
        dep.notify()
        // 触发dep.notify()方法 subs[i].update()
        // subs 存储的是watcher实例
      }
  })
}

阶段二 进入$mount() 这个方法里面比较重要

​ initState() 阶段完成以后,后面会执行到mount() 方法,这个方法比较关键我们一起来看下。

Vue.prototype._init = function (options) {
	initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); // resolve injections before data/props
    initState(vm);
    initProvide(vm); // resolve provide after data/props
    callHook(vm, 'created');

    /* istanbul ignore if */
    if ("development" !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false);
      mark(endTag);
      measure(("vue " + (vm._name) + " init"), startTag, endTag);
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
}

​ 我们先来看一下$mount方法,我们发现一开始有一段赋值,其实就是先存下来Vue上的公共mount方法,然后又重写了公共的mount方法。

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
    ...
}
<!-- 源码位置:src/platforms/web/entry-runtime-with-compiler.js -->
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  const options = this.$options
  if (!options.render) {
	// 我们会进入这个地方,因为我们没有写render函数,写的是template模板
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          ...
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        ...
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
  
    if (template) {
       //走到这里, 进入了compileToFunctions方法 重点注意开始编译环节
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        delimiters: options.delimiters
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  return mount.call(this, el, hydrating)
}

function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

  compileToFunctions方法做了什么事情呢,我先大体的介绍一下。它的最终目的是让template字符串模板——>render function 函数。compile这个编译过程在Vue2会经历3个阶段:

  • 把html生成ast语法树 (Vue 源码中借鉴 jQuery 作者 John ResigHTML Parser 对模板进行解析)
  • 对ast语法树进行静态优化optimize() (找到静态结点,做标记就是在ast上添加了static属性优化diff)
  • 根据优化过的ast generate生成render function 字符串
var createCompiler = createCompilerCreator(function baseCompile (
  template,
  options
) {
  var ast = parse(template.trim(), options);
  if (options.optimize !== false) {
    optimize(ast, options);
  }
  var code = generate(ast, options);
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
});
var ref$1 = createCompiler(baseOptions);
var compileToFunctions = ref$1.compileToFunctions;

function createCompileToFunctionFn (compile) {
  var cache = Object.create(null);

  return function compileToFunctions (
    template,
    options,
    vm
  ) {
    options = extend({}, options);
    var warn$$1 = options.warn || warn;
    delete options.warn;
	....
    
    // 走到了这里
    var key = options.delimiters
      ? String(options.delimiters) + template
      : template;
    if (cache[key]) {
      return cache[key]
    }

    // 进入这里开始compile, 我们先来分析这个compile
    var compiled = compile(template, options);
	...
    var res = {};
    var fnGenErrors = [];
    res.render = createFunction(compiled.render, fnGenErrors);
    res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
      return createFunction(code, fnGenErrors)
    });

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn$$1(
          "Failed to generate render function:\n\n" +
          fnGenErrors.map(function (ref) {
            var err = ref.err;
            var code = ref.code;

            return ((err.toString()) + " in\n\n" + code + "\n");
        }).join('\n'),
          vm
        );
      }
    }

    return (cache[key] = res)
  }
}

compile(template, options);会进入baseCompile方法, 我们来看下baseCompile方法里面的细节

var createCompiler = createCompilerCreator(function baseCompile (
  template,
  options
) {
  //可以看到我们大致的流程 ast->optimize->generate  
  var ast = parse(template.trim(), options);
  if (options.optimize !== false) {
    optimize(ast, options);
  }
  var code = generate(ast, options);
  return {
    ast: ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
});

阶段二(一) 生成ast

<section id="test">
	<div> {{a+b}} </div>
	<p>静态文本<a href="www.koala.com">考拉地址</a></p>
</section>

大致理解一下html-parser吧, html-parser 会按照下面几步进行html的解析

  1. 首先定义基本的ast结构
const element1 = {
    type: 1,
    tag: "section",
    attrsList: [{name: "id", value: "test"}],
    attrsMap: {id: "app"},
    parent: undefined,
    children: []
  }

2.对ast进行预处理(preTransforms)
对ast的预处理在weex中才会有,我们直接跳过。
3、 解析v-pre、v-if、v-for、v-once、slot、key、ref等指令。
4、 对ast的class 和 style中的属性进行处理
5、 解析v-bind、v-on以及普通属性
6、 根节点或v-else块等处理
7、 模板元素父子关系的建立
8、 对ast后处理(postTransforms)

最终生成的ast长下面这样:

{
    	type: 1, //element类型
		tag: "section",
        attrsList:[{
			name: "id",
			value: "test"
        }],
		attrsMap: {id: "test"},
        children:[
            {
                type: 1,
                tag: "div",
                attrsList:[],
                attrsMap:{},
                children:[{
                    type: 2,
                    expression: "_s(a+b)",
                    tokens:[{@binding: "a+b"}]
                }]},
                parent: {...},
	            plain: true
            },
            {
            	type: 3,
            	text: ""
            },
            {
            	type: 1,
            	tag: "p",
            	attrsList: [],
            	attrsMap: {},
                parent: {...},
                children:[
                    {
                        type:3,
                        text: "静态文本"
                    },
                    {
                        ... 
                    }
               	]                    
            }
		]            
}

阶段二 optimize() 静态结点标记

源码位置: src/compiler/optimizer.js

function markStatic (node: ASTNode) {
  // 通过isStatic 方法来判断node结点是否为静态结点 
  node.static = isStatic(node)
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
  }
}

看一下isStatic方法

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false // 表达式肯定不是静态结点
  }
  if (node.type === 3) { // text
    return true // 文本肯定是静态结点
  }
  return !!(node.pre // v-pre 指令,此时子节点是不做编译的
    || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in 内置标签包括slot 和 component
    isPlatformReservedTag(node.tag) && // 是平台保留标签html和svg标签
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey) // 不是template标签的直接子元素且没有包含在for循环中
  ))
}

然后我们的ast 会变成下面这样,标记了是否为静态结点和是否为静态根结点, 增加了一个static属性

{
    	type: 1, //element类型
		tag: "section",
        attrsList:[{
			name: "id",
			value: "test"
        }],
		attrsMap: {id: "test"},
        children:[
            {
                type: 1,
                tag: "div",
                attrsList:[],
                attrsMap:{},
                children:[{
                    type: 2,
                    expression: "_s(a+b)",
                    tokens:[{@binding: "a+b"}]
                }]},
                parent: {...},
	            plain: true,
	            static: false
            },
            {
            	type: 3,
            	text: "",
	            static: true
            },
            {
            	type: 1,
            	tag: "p",
            	attrsList: [],
            	attrsMap: {},
                parent: {...},
                children:[
                    {
                        type:3,
                        text: "静态文本"
                    },
                    {
                        ... 
                    }
               	],
				static: true
            }
		],
		plain: false,
		static: false
}

阶段二(三) generate() 生成render function

源码位置:src/compiler/codegen/index.js
拿到ast结构以后,进入generate函数 var code = generate(ast, options);

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
) {
  // save previous staticRenderFns so generate calls can be nested
  ....	
  const code = ast ? genElement(ast) : '_c("div")'
  staticRenderFns = prevStaticRenderFns
  onceCount = prevOnceCount
  // 返回的整体结构
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: currentStaticRenderFns
  }
}

重点看下genElement 方法

function genElement (el: ASTElement): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el)
  } else if (el.once && !el.onceProcessed) {
  	...
  } else {
    // component or element
    let code
    if (el.component) {
      ...
    } else {
      // 大部分进入这里,plain 代表元素上是否没有属性
      // 后面先看下genData 方法
      const data = el.plain ? undefined : genData(el)
      // 后面看下genChildren方法
      const children = el.inlineTemplate ? null : genChildren(el, true);
      
      code = `_c('${el.tag}' ${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < transforms.length; i++) {
      code = transforms[i](el, code)
    }
    return code
  }
}
function genData (el: ASTElement): string {
  let data = '{'
  ...
  if (el.attrs) {
    data += `attrs:{${genProps(el.attrs)}},`
  }
  ...
  data = data.replace(/,$/, '') + '}'
  ...
  return data // data 的结构是 attrs: {id: 'app'}
}

function genChildren (el: ASTElement, checkSkip?: boolean): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    // optimize single v-for
    if (children.length === 1 &&
        el.for &&
        el.tag !== 'template' &&
        el.tag !== 'slot') {
      return genElement(el)
    }
    const normalizationType = checkSkip ? getNormalizationType(children) : 0
    return `[${children.map(genNode).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

最后生成的就是下面这个对象

{  
  render: "with(this){return _c('section',{attrs:{\"id\":\"test\"}},[_c('div',[_v(_s(a+b))]),_v(\" \"),_m(0)])}",
  staticRenderFns: [
    "with(this){return _c('p',[_v(\"静态文本\"),_c('a',{attrs:{\"href\":\"www.koala.com\"}},[_v(\"考拉地址\")])])}"
  ]
}

// render 是render function 字符串
// staticRenderFns 是静态渲染函数数组

之后调用了 createFunction 方法其实就是new Function('string')。所以其实最终我们得到的是 render function

res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
      return createFunction(code, fnGenErrors)
    });
options.render = render;
options.staticRenderFns = staticRenderFns;

function createFunction (code, errors) {
    try {
      return new Function(code)
    } catch (err) {
      errors.push({ err: err, code: code });
      return noop
    }
  }

然后compile结束后,我们得到render function 之后,开始执行$mount的公用方法(源码位置src/platforms/web/runtime/index.js) 其实就是调用了mountComponent方法

// 接下来进入这个方法
return mount.call(this, el, hydrating)
// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  // 本质是进入这个方法  
  return mountComponent(this, el, hydrating)
}

阶段四 执行renderFunction 得到VNode

  export function mountComponent (
  vm: Component,
    el: ?Element,
    hydrating?: boolean
  ): Component {
    ...
    
  callHook(vm, 'beforeMount')
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ...
  } else {
     
    updateComponent = () => {
      // 重点看这里执行了render()方法生成了vnode
      vm._update(vm._render(), hydrating)
    }
  }
	
  // 入口:下面执行了new Watcher, 这是我们的重点入口[1]
  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
  }

阶段五 进入Watcher 类

var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher
) {
 ...
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = function () {};
      "development" !== 'production' && warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  // 重点看下这里,调用了Watcher的get方法,我们看下get方法
  this.value = this.lazy
    ? undefined
    : this.get();
};

Watcher.prototype.get = function get () {
  // 这里有个关键点
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

  watcher 构造函数最后调用了this.get() , 首先调用了pushTarget(this)方法。这个方法把Dep.target设为this(即当前watcher实例)。然后执行了this.getter.call(vm, vm);这个this.getter 就是 this.getter = expOrFn; 即我们传入new Watcher 里面的第二个参数,updateComponent。updateComponent做了什么呢?它先执行了vm.render,然后执行了 vm. update

Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
      //getter对应new Watcher时我们传入的第二个参数 new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)即updateComponent
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

function pushTarget (_target) {
  if (Dep.target) { targetStack.push(Dep.target); }
  Dep.target = _target;
}

updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };

阶段六 调用render()生成VDom

​ 下面进入了updateComponent方法,会先执行vm._render(), 我们来看下vm._render()调用后得到了什么。

Vue.prototype._render = function () {
    var vm = this;
    var ref = vm.$options;
    var render = ref.render;
    var _parentVnode = ref._parentVnode;

 	...
    vm.$vnode = _parentVnode;
    // render self
    var vnode;
    try {
	 // 重点看这里,其实就是执行了我们的render function
      vnode = render.call(vm._renderProxy, vm.$createElement);
    } catch (e) {
      ...
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if ("development" !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        );
      }
      vnode = createEmptyVNode();
    }
    // set parent
    vnode.parent = _parentVnode;
    return vnode
  };
}

​ vnode = render.call(vm._renderProxy, vm.$createElement);这个方法的调用,相当于执行了我们之前得到的render function。并且我们知道render function 里面有with(this) 此时我们把this指向了vm, 所以按照我们的例子, s(a+b) 在执行的时候会读取到vm.a 和 vm.b 就分别进入了a 和 b 的getter。再回头看下getter函数。

​ 所以注意这里就是Watcher 与 Dep 关联的地方,可以认为compile 是Watcher 与 Dep之间的桥梁。

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      // 此时Dep.target 指向this, 因为watch实例生成时调用了pushTarget(this)  
      if (Dep.target) {
          // 看下dep.depend 方法
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function() {...}
}

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    // 调用的是watcher的addDep方法
    Dep.target.addDep(this);
  }
};
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      // 这里调用了dep的addSub方法   
      dep.addSub(this);
    }
  }
};
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};

​ vm.render()方法最终执行完会生成VNode, 即完成了从render function —> VNode 的过程,我们看下此时vnode的样子, 大概像下面这样

{
	    	asyncFactory: undefined,
			asyncMeta: undefined,
        	children:[
    		{tag: 'div', data: undefined, children:[VNode], text: undefined, elm: undefind ...},
			{tag: undefined, data: undefined, children:undefined, text: "", elm: undefind ...},
            {tag: 'p', data: undefined, children:[VNode, VNode], text: undefined, elm: undefind ...},
        ],
            context: vm,
            data: {
                attrs: {id: "test"}
            },
            ....
            isStatic: false,
			tag: 'section'
    	...
}

​ 得到VNode 之后,调用vm.update 方法从VNode 生成DOM。update方法内部重点调用了patch方法,看下面。因为patch内容也比较复杂,所以此次并不讲解内部相关的具体流程,会大致看下dom创建的过程。patch方法内部大致会涉及下面3部分的处理:

  • 根据vnode 创建dom
  • diff 算法
  • 自定义组件处理
Vue.prototype._update = function (vnode, hydrating) {
    ...
    vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      );
}
// patch方法内部通过调用createElm 来生成node
createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm$1,
          nodeOps.nextSibling(oldElm)
        );
// createElm 中主要通过下面这个方法来创建dom
 vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)

//nodeOps是什么,是一些创建dom相关的方法
import { namespaceMap } from 'web/util/index'
export function createElement (tagName: string, vnode: VNode): Element {
  const elm = document.createElement(tagName)
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}

export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}

...

​ 最后我们的结点生成并挂载了vnode.elm 上,打出来看下

> vnode.elm
<section>
	<div>3</div>
	<p>
		静态文本
		<a href="www.kaola.com"></a>
	</p>		
</section>

​ 接下来就会触发 insert (parent, elm, ref$$1) parent 是body, elm是上面得到的dom, 第三个参数没研究,此时elm就插入到了parent中。接下来又会回到mountComponent 接下去的方法

function mountComponent () {
    ...
    new Watcher(...)
	hydrating = false;
    if(vm.$vnode == null){
    	vm._isMounted = true;
        callHook(vm, 'mounted'); //触发了mounted钩子
    }
    return vm;
}

当data更新

当我们data的值发生了变化的时候,会进入setter函数。

set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if ("development" !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      // 进入dep.notify 通知watcher
      dep.notify();
}
Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    // 进入这里 这个方法最终会进入watcher.prototype.run 方法  
    queueWatcher(this);
  }
};

Watcher.prototype.run = function run () {
  if (this.active) {
    // 这里会调用this.get, 之前有讲过this.get里面会调用updateComponent,所以又会走到
    //  vm._update(vm._render(), hydrating);这个方法重新更新视图了
    var value = this.get();
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      var oldValue = this.value;
      this.value = value;
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
        }
      } else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};
Watcher.prototype.get = function get () {
  // 这里有个关键点
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
	// 注意this.getter = expFunction      
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

所以我们此时就理解了整个Observer,Dep 与Watcher之间的关系

参考文献

1.Vue2.0 源码阅读:模板渲染
2.compile—优化静态内容
3.深入vue2.0底层思想——模板渲染
4.Vue2 源码漫游(二)
5.Vitual DOM 的内部工作原理

@KFandroid
Copy link

写的非常好,比我目前看到的讲vue源码的文章(包括付费的)都要好,作者是真的吃透了vue源码,赶紧出书,我一定买!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants