响应式的思路:
响应式的数据变化, 数据变化了我可以监控到数据的变化
数据的取值和更改值我们要监控到
Vue使用了构造函数,然后将不同的原型方法分别放到不同的mixin文件中,让相似功能的方法尽可能集中在一起存放,如果使用class的话,通常只能将所有方法都放到class里面去
Vue拦截了每个data、props中的字段,都给它们添加了get set钩子,并且访问每个data、props里面的字段时,都加了代理从而让vm能直接访问到data和props上的值
监听逻辑:
./index.js
import { initMixin } from "./init";
import { initLifeCycle } from "./lifecycle";
// 将所有的方法都耦合在一起
function Vue(options){ // options就是用户的选项
this._init(options); // 默认就调用了init
}
./init.js
Vue.prototype._init = function (options) { // 用于初始化操作
// vue vm.$options 就是获取用户的配置
// 我们使用的 vue的时候 $nextTick $data $attr.....
const vm = this;
vm.$options = options; // 将用户的选项挂载到实例上
// 初始化状态
initState(vm);
./state.js
import { observe } from "./observe/index";
export function initState(vm) {
const opts = vm.$options; // 获取所有的选项
if (opts.data) {
initData(vm);
}
}
function proxy(vm, target, key) {
Object.defineProperty(vm, key, { // vm.name
get() {
return vm[target][key]; // vm._data.name
},
set(newValue){
vm[target][key] = newValue
}
})
}
function initData(vm) {
let data = vm.$options.data; // data可能是函数和对象
data = typeof data === 'function' ? data.call(vm) : data; // data是用户返回的对象
vm._data = data; // 我将返回的对象放到了_data上
// 对数据进行劫持 vue2 里采用了一个api defineProperty
observe(data)
// 将vm._data 用vm来代理就可以了
for (let key in data) {
proxy(vm, '_data', key);
}
}
./observe/index.js
import { newArrayProto } from "./array";
class Observer{
constructor(data){
// Object.defineProperty只能劫持已经存在的属性 (vue里面会为此单独写一些api $set $delete)
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false // 将__ob__ 变成不可枚举 (循环的时候无法获取到)
});
// data.__ob__ = this; // 给数据加了一个标识 如果数据上有__ob__ 则说明这个属性被观测过了
if(Array.isArray(data)){
// 这里我们可以重写数组中的方法 7个变异方法 是可以修改数组本身的
data.__proto__ = newArrayProto // 需要保留数组原有的特性,并且可以重写部分方法
this.observeArray(data); // 如果数组中放的是对象 可以监控到对象的变化
}else{
this.walk(data);
}
}
walk(data){ // 循环对象 对属性依次劫持
// "重新定义"属性 性能差
Object.keys(data).forEach(key=> defineReactive(data,key,data[key]))
}
observeArray(data){ // 观测数组
data.forEach(item=> observe(item))
}
}
export function defineReactive(target,key,value){ // 闭包 属性劫持
observe(value); // 对所有的对象都进行属性劫持
Object.defineProperty(target,key,{
get(){ // 取值的时候 会执行get
console.log('key',key)
return value
},
set(newValue){ // 修改的时候 会执行set
if(newValue === value) return
observe(newValue)
value = newValue
}
})
}
export function observe(data){
// 对这个对象进行劫持
if(typeof data !== 'object' || data == null){
return; // 只对对象进行劫持
}
if(data.__ob__ instanceof Observer){ // 说明这个对象被代理过了
return data.__ob__;
}
// 如果一个对象被劫持过了,那就不需要再被劫持了 (要判断一个对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持过)
return new Observer(data);
}
./observe/array.js
// 我们希望重写数组中的部分方法
let oldArrayProto = Array.prototype; // 获取数组的原型
// newArrayProto.__proto__ = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto);
let methods = [ // 找到所有的变异方法
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
] // concat slice 都不会改变原数组
methods.forEach(method => {
// arr.push(1,2,3)
newArrayProto[method] = function (...args) { // 这里重写了数组的方法
// push.call(arr)
// todo...
const result = oldArrayProto[method].call(this, ...args); // 内部调用原来的方法 , 函数的劫持 切片编程
// 我们需要对新增的 数据再次进行劫持
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift': // arr.unshift(1,2,3)
inserted = args;
break;
case 'splice': // arr.splice(0,1,{a:1},{a:1})
inserted = args.slice(2);
default:
break;
}
// console.log(inserted); // 新增的内容
if(inserted) {
// 对新增的内容再次进行观测
ob.observeArray(inserted);
}
return result
}
})
在./observe/array.js这个文件中,对于数组新加进来的内容args,我们希望继续递归地进行响应式处理
args是数组类型,对数组类型进行响应式处理的方法是Observer.prototype.observeArray
因此,我们希望通过observeArray这个方法来处理args
而在newArrayProto[method]调用push、unshift、splice等这些方法时,能拿到的只有this,这里的this指向调用push、unshift、splice这些方法的数组,因此我们需要将this和Observer对象建立关联关系,这个关联关系的代码体现在./observe/index.js文件的第6行,又因为我们不能给__ob__本身做响应式处理,否则就会陷入死循环,所以将它定义为了非枚举的属性
渲染逻辑:
import { compileToFunction } from "./compiler";
import { mountComponent } from "./lifecycle";
import { initState } from "./state";
export function initMixin(Vue) { // 就是给Vue增加init方法的
Vue.prototype._init = function (options) { // 用于初始化操作
// vue vm.$options 就是获取用户的配置
// 我们使用的 vue的时候 $nextTick $data $attr.....
const vm = this;
vm.$options = options; // 将用户的选项挂载到实例上
// 初始化状态
initState(vm);
if (options.el) {
vm.$mount(options.el); // 实现数据的挂载
}
}
Vue.prototype.$mount = function (el) {
const vm = this;
el = document.querySelector(el);
let ops = vm.$options
if (!ops.render) { // 先进行查找有没有render函数
let template; // 没有render看一下是否写了tempate, 没写template采用外部的template
if (!ops.template && el) { // 没有写模板 但是写了el
template = el.outerHTML
}else{
if(el){
template = ops.template // 如果有el 则采用模板的内容
}
}
// 写了temlate 就用 写了的template
if(template && el){
// 这里需要对模板进行编译
const render = compileToFunction(template);
ops.render = render; // jsx 最终会被编译成h('xxx')
}
}
mountComponent(vm,el); // 组件的挂载
// 最终就可以获取render方法
// script 标签引用的vue.global.js 这个编译过程是在浏览器运行的
// runtime是不包含模板编译的, 整个编译是打包的时候通过loader来转义.vue文件的, 用runtime的时候不能使用template
}
}
Dep和Watcher关联关系分析:
Dep中存放用到的Watcher,比较容易理解,就是在dep更新的时候执行哪些Watcher
Watcher中存放关联的Dep,使用场合如下:
计算属性,组件卸载时,会用到
Dep.js:
let id = 0;
class Dep{
constructor(){
this.id = id++; // 属性的dep要收集watcher
this.subs = [];// 这里存放着当前属性对应的watcher有哪些
}
depend(){
// 这里我们不希望放重复的watcher,而且刚才只是一个单向的关系 dep -> watcher
// watcher 记录dep
// this.subs.push(Dep.target);
Dep.target.addDep(this); // 让watcher记住dep
// dep 和 watcher是一个多对多的关系 (一个属性可以在多个组件中使用 dep -> 多个watcher)
// 一个组件中由多个属性组成 (一个watcher 对应多个dep)
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher=>watcher.update()); // 告诉watcher要更新了
}
}
Dep.target = null;
export default Dep;
调用dep.depend时就是要添加watcher,由于避免重复添加,Vue采取了调用当前watcher对象的addDep方法,借由watcher和dep建立关联关系时将watcher加到属性的dep中,即在watcher的addDep方法里面实现了watcher和dep双向关联,如下面代码的17行addDep方法所示
import Dep from "./dep";
let id = 0;
// 1) 当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target上
// 2) 调用_render() 会取值 走到get上
// 每个属性有一个dep (属性就是被观察者) , watcher就是观察者(属性变化了会通知观察者来更新) -》 观察者模式
class Watcher { // 不同组件有不同的watcher 目前只有一个 渲染根实例的
constructor(vm, fn, options) {
this.id = id++;
this.renderWatcher = options; // 是一个渲染watcher
this.getter = fn; // getter意味着调用这个函数可以发生取值操作
this.deps = []; // 后续我们实现计算属性,和一些清理工作需要用到
this.depsId = new Set();
this.get();
}
addDep(dep) { // 一个组件 对应着多个属性 重复的属性也不用记录
let id = dep.id;
if (!this.depsId.has(id)) {
this.deps.push(dep);
this.depsId.add(id);
dep.addSub(this); // watcher已经记住了dep了而且去重了,此时让dep也记住watcher
}
}
get() {
Dep.target = this; // 静态属性就是只有一份
this.getter(); // 会去vm上取值 vm._update(vm._render) 取name 和age
Dep.target = null; // 渲染完毕后就清空
}
update() {
this.get(); // 重新渲染
}
异步更新:
修改属性时会触发对应属性的notify方法,进而执行watcher的update方法,但如果更改的属性都映射到了一个watcher的话,就会导致重复更新,为解决该问题,watcher的update方法应该是将watcher做一下去重
目前我们遇到的情况还只有一个watcher的更新,即组件的渲染watcher,但通常情况下,每一轮事件循环通常会触发多个watcher更新,所以所有需要更新的watcher都会被加入到一个队列中去,在一个特定周期内从队列中取出所有watcher再遍历执行,由pending变量来控制队列是否正在添加中
class Watcher {
// ...
update() {
queueWatcher(this); // 把当前的watcher 暂存起来
// this.get(); // 重新渲染
}
我们在页面中,也经常会有dom操作,这些dom操作我们要放在数据变化之后的nextTick回调和遍历watcher执行的nextTick,我们自己的$nextTick会后调用,例如:
const vm = new Vue({
data: {
name: 'zf',
age: 20,
address: {
num: 30,
content: '回龙观'
},
hobby: ['eat', 'drink', { a: 1 }]
},
});
vm.$mount('#app');
vm.name = 'jw'; // 不会立即重新渲染页面
vm.$nextTick(()=>{
console.log(app.innerHTML); // 同步获取
})
异步相关的源码:
let queue = [];
let has = {};
let pending = false; // 防抖
function flushSchedulerQueue() {
let flushQueue = queue.slice(0);
queue = [];
has = {};
pending = false;
flushQueue.forEach(q => q.run()); // 在刷新的过程中可能还有新的watcher,重新放到queue中
}
function queueWatcher(watcher) {
const id = watcher.id;
if (!has[id]) {
queue.push(watcher);
has[id] = true;
// 不管我们的update执行多少次 ,但是最终只执行一轮刷新操作
if (!pending) {
nextTick(flushSchedulerQueue, 0)
pending = true;
}
}
}
let callbacks = [];
let waiting = false;
function flushCallbacks() {
let cbs = callbacks.slice(0);
waiting = false;;
callbacks = [];
cbs.forEach(cb => cb()); // 按照顺序依次执行
}
export function nextTick(cb) { // 先内部还是先用户的?
callbacks.push(cb); // 维护nextTick中的cakllback方法
if (!waiting) {
// timerFunc()
Promise.resolve().then(flushCallbacks)
waiting = true
}
}
上述代码的nextTick中,我们可以看到,是用微任务的方式实现的(Promise.resolve),在较早的Vue版本中,为了做兼容,作者依次判断了Promise -> MutationObserver -> setImmediate -> setTimeout 依次降级
nextTick的兼容实现:
// nextTick 没有直接使用某个api 而是采用优雅降级的方式
// 内部先采用的是promise (ie不兼容) MutationObserver(h5的api) 可以考虑ie专享的 setImmediate setTimeout
let timerFunc;
if (Promise) {
timerFunc = () => {
Promise.resolve().then(flushCallbacks)
}
}else if(MutationObserver){
let observer = new MutationObserver(flushCallbacks); // 这里传入的回调是异步执行的
let textNode = document.createTextNode(1);
observer.observe(textNode,{
characterData:true
});
timerFunc = () => {
textNode.textContent = 2;
}
}else if(setImmediate){
timerFunc = () => {
setImmediate(flushCallbacks);
}
}else{
timerFunc = () => {
setTimeout(flushCallbacks);
}
}
Vue中的异步队列模型:
flushCallbacks和nextTick共同构成了Vue的异步队列模型,我们可以通过nextTick来注册希望异步执行(放到下一轮队列中执行)的方法,再通过flushCallbacks从队列中取出来执行,callbacks就是存放队列的数组
let callbacks = [];
let waiting = false;
function flushCallbacks() {
let cbs = callbacks.slice(0);
waiting = false;
callbacks = [];
cbs.forEach(cb => cb()); // 按照顺序依次执行
}
export function nextTick(cb) { // 先内部还是先用户的?
callbacks.push(cb); // 维护nextTick中的cakllback方法
if (!waiting) {
// timerFunc()
Promise.resolve().then(flushCallbacks)
waiting = true
}
}
遍历所有watcher执行notify,即flushSchedulerQueue,其实只是队列中所有方法中的一个
两个开关:
1、pending:Vue内部在渲染的过程中,逻辑是将所有的watcher放到一个queue里面,然后在flushSchedulerQueue里遍历watcher.update,将其作为一个异步任务执行,这里的pending开关是专门控制watcher的入队的
2、waiting:
而这里的waiting是用来控制Vue中所有异步任务的入队的
计算属性:
计算属性依赖于watcher来实现,取值时还要设置dirty的值,所以单独写了一个新的方法evaluate来做这两件事:
class Watcher {
// ...
evaluate(){
this.value = this.get(); // 获取到用户函数的返回值 并且还要标识为脏
this.dirty = false;
}
目前我们已经引入了两种watcher:分别是计算属性的watcher和渲染watcher,二者区别在于,前者的dirty属性是有意义的,且初始化后不会立即执行(lazy属性初始化为true),而渲染watcher在初始化的同时就会调用this.get -> this.getter方法执行一次取值
计算属性在初始化时,在data里面的属性,如果被计算属性用到了,它的dep里面会先订阅计算属性的watcher,再订阅组件的渲染watcher,具体的订阅流程的逻辑如下:
function initComputed(vm) {
const computed = vm.$options.computed;
const watchers = vm._computedWatchers = {}; // 将计算属性watcher保存到vm上
for (let key in computed) {
let userDef = computed[key];
// 我们需要监控 计算属性中get的变化
let fn = typeof userDef === 'function' ? userDef : userDef.get
// 如果直接new Watcher 默认就会执行fn, 将属性和watcher对应起来
watchers[key] = new Watcher(vm, fn, { lazy: true })
defineComputed(vm, key, userDef);
}
}
上述代码中,Watcher的初始化传入了lazy: true这一参数(标识它不会立即执行getter方法),接下来在mount的过程中,如果模板中用到了某个计算属性,则会触发它的get钩子,下面代码的createComputedGetter的返回值:
function defineComputed(target, key, userDef) {
// const getter = typeof userDef === 'function' ? userDef : userDef.get;
const setter = userDef.set || (() => { })
// 可以通过实例拿到对应的属性
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter
})
}
// 计算属性根本不会收集依赖 ,只会让自己的依赖属性去收集依赖
function createComputedGetter(key) {
// 我们需要检测是否要执行这个getter
return function () {
const watcher = this._computedWatchers[key]; // 获取到对应属性的watcher
if (watcher.dirty) {
// 如果是脏的就去执行 用户传入的函数
watcher.evaluate(); // 求值后 dirty变为了false ,下次就不求值了
}
if (Dep.target) { // 计算属性出栈后 还要渲染watcher, 我应该让计算属性watcher里面的属性 也去收集上一层watcher
watcher.depend();
}
return watcher.value; // 最后返回的是watcher上的值
}
}
而dirty的初始值是true,所以会默认计算一下初始值watcher.evaluate -> watcher.get(18-21行),在watcher.get里面,会把计算属性的watcher入栈:
class Watcher { // 不同组件有不同的watcher 目前只有一个 渲染根实例的
// ...
evaluate(){
this.value = this.get(); // 获取到用户函数的返回值 并且还要标识为脏
this.dirty = false;
}
get() {
pushTarget(this)// 静态属性就是只有一份
let value = this.getter.call(this.vm); // 会去vm上取值 vm._update(vm._render) 取name 和age
popTarget() // 渲染完毕后就清空
return value;
}
接下来要执行watcher的getter方法,就要访问到data里面属性的get钩子,此时get钩子里的Dep.target就是计算属性的watcher
export function defineReactive(target,key,value){ // 闭包 属性劫持
let childOb = observe(value); // 对所有的对象都进行属性劫持 childOb.dep 用来收集依赖的
let dep = new Dep(); // 每一个属性都有一个dep
Object.defineProperty(target,key,{
get(){ // 取值的时候 会执行get
if(Dep.target){
dep.depend(); // 让这个属性的收集器记住当前的watcher
if(childOb){
childOb.dep.depend(); // 让数组和对象本身也实现依赖收集
if(Array.isArray(value)){
dependArray(value);
}
}
}
return value
},
紧接着,在第7行,这个data属性的dep就会收集到计算属性的watcher
与此同时,计算属性的watcher也收集到了它所依赖的data属性的dep对象
从data上取完值,回到watcher.get方法中后,计算属性的Watcher出栈,之后执行createComputedGetter方法中的watcher.depend:
function createComputedGetter(key) {
// 我们需要检测是否要执行这个getter
return function () {
const watcher = this._computedWatchers[key]; // 获取到对应属性的watcher
if (watcher.dirty) {
// 如果是脏的就去执行 用户传入的函数
watcher.evaluate(); // 求值后 dirty变为了false ,下次就不求值了
}
if (Dep.target) { // 计算属性出栈后 还要渲染watcher, 我应该让计算属性watcher里面的属性 也去收集上一层watcher
watcher.depend();
}
return watcher.value; // 最后返回的是watcher上的值
}
}
此时watcher的deps包含了所有用到的data属性的dep,这时再调用计算属性的watcher的depend(上面代码10行),进而调用了所依赖的每个dep的depend方法
class Watcher {
// ...
depend(){ // watcher的depend 就是让watcher中dep去depend
let i = this.deps.length;
while(i--){
// dep.depend()
this.deps[i].depend(); // 让计算属性watcher 也收集渲染watcher
}
}
此时,下面这里的Dep.target就是渲染watcher了
class Dep {
// ...
depend(){
// 这里我们不希望放重复的watcher,而且刚才只是一个单向的关系 dep -> watcher
// watcher 记录dep
// this.subs.push(Dep.target);
Dep.target.addDep(this); // 让watcher记住dep
// dep 和 watcher是一个多对多的关系 (一个属性可以在多个组件中使用 dep -> 多个watcher)
// 一个组件中由多个属性组成 (一个watcher 对应多个dep)
}
此时,和计算属性有关联的data属性应该有2个watcher:计算属性的watcher和组件的渲染watcher
综合上述分析,在data属性的set钩子被触发时,由于收集了计算属性的watcher,因此也会执行该watcher的update:
class Watcher {
update() {
if(this.lazy){
// 如果是计算属性 依赖的值变化了 就标识计算属性是脏值了
this.dirty = true;
}else{
queueWatcher(this); // 把当前的watcher 暂存起来
// this.get(); // 重新渲染
}
}
但由于计算属性watcher被标记了lazy是true,从上述代码中也可以看到,只是将dirty值改为了true,这样在下一次用到该计算属性时,会重新进入到watcher.evaluate()里面取到新的值
而所谓的下一次用到该计算属性,其实也就是遍历下一个watcher,即渲染watcher的update过程中在模板中访问计算属性
计算属性的set仅仅是一个hook,往进订阅什么,然后执行,没有其他逻辑
watch:
无论哪种写法,最终都会走到$watch方法中去
export function initState(vm) {
const opts = vm.$options; // 获取所有的选项
if (opts.data) {
initData(vm);
}
if (opts.computed) {
initComputed(vm);
}
if (opts.watch) {
initWatch(vm);
}
}
function initWatch(vm){
let watch = vm.$options.watch;
for(let key in watch){
const handler = watch[key]; // 字符串 数组 函数
if(Array.isArray(handler)){
for(let i = 0; i < handler.length;i++){
createWatcher(vm,key,handler[i]);
}
}else{
createWatcher(vm,key,handler);
}
}
}
function createWatcher(vm,key,handler){
// 字符串 函数
if(typeof handler === 'string'){
handler = vm[handler];
}
return vm.$watch(key,handler)
}
// 最终调用的都是这个方法
Vue.prototype.$watch = function (exprOrFn, cb) {
// firstname
// ()=>vm.firstname
// firstname的值变化了 直接执行cb函数即可
new Watcher(this,exprOrFn,{user:true},cb)
}
通过配置传入Vue中的watcher,像下面这样:
const vm = new Vue({
el: '#app',
data: {
firstname: '珠',
lastname: '峰',
age: 13
},
// 直接写一个函数
// 数组写法
watch: {
firstname(newValue,oldValue){
console.log(newValue, oldValue)
}
},
要比之前讲到的渲染watcher先执行,因为它执行的时机在init阶段:_init -> initState -> initWatch -> createWatcher -> new Watcher -> this.get -> this.getter.call -> 触发属性的get钩子 -> 收集依赖
上面这个流程执行完了以后,才执行vm.$mount -> mountComponent -> new Watcher
对象和数组本身的Dep依赖:
const vm = new Vue({
el: '#app',
data: {
firstname: '珠',
lastname: '峰',
age: 13,
a: {
b: 1
}
},
上面代码中如果我们修改firstname、lastname、age,或给a属性直接赋值,都可以触发渲染watcher进而更新DOM,但如果我们只是给a对象下新加一个属性,例如vm.a.c = 2,目前我们的代码对这种操作是没有效果的,即不会更新DOM,所以我们需要给每个对象增加一个Dep依赖对象,往数组中push一项这种行为也是类似
实现方式:如下代码的6-12行:
class Observer{
constructor(data){
// 给每个对象都增加收集功能
this.dep = new Dep(); // 所有对象都要增加dep
// Object.defineProperty只能劫持已经存在的属性 (vue里面会为此单独写一些api $set $delete)
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false // 将__ob__ 变成不可枚举 (循环的时候无法获取到)
});
// data.__ob__ = this; // 给数据加了一个标识 如果数据上有__ob__ 则说明这个属性被观测过了
if(Array.isArray(data)){
// 这里我们可以重写数组中的方法 7个变异方法 是可以修改数组本身的
data.__proto__ = newArrayProto // 需要保留数组原有的特性,并且可以重写部分方法
this.observeArray(data); // 如果数组中放的是对象 可以监控到对象的变化
}else{
this.walk(data);
}
}
以及下面代码41行:
// 我们希望重写数组中的部分方法
let oldArrayProto = Array.prototype; // 获取数组的原型
// newArrayProto.__proto__ = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto);
let methods = [ // 找到所有的变异方法
'push',
'pop',
'shift',
'unshift',
'reverse',
'sort',
'splice'
] // concat slice 都不会改变原数组
methods.forEach(method => {
// arr.push(1,2,3)
newArrayProto[method] = function (...args) { // 这里重写了数组的方法
// push.call(arr)
// todo...
const result = oldArrayProto[method].call(this, ...args); // 内部调用原来的方法 , 函数的劫持 切片编程
// 我们需要对新增的 数据再次进行劫持
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift': // arr.unshift(1,2,3)
inserted = args;
break;
case 'splice': // arr.splice(0,1,{a:1},{a:1})
inserted = args.slice(2);
default:
break;
}
// console.log(inserted); // 新增的内容
if(inserted) {
// 对新增的内容再次进行观测
ob.observeArray(inserted);
}
// 走到这里
ob.dep.notify(); // 数组变化了 通知对应的watcher实现更新逻辑
以及下面的18-23行:
function dependArray(value){
for(let i = 0; i < value.length;i++){
let current = value[i]
current.__ob__ && current.__ob__.dep.depend();
if(Array.isArray(current)){
dependArray(current);
}
}
}
export function defineReactive(target,key,value){ // 闭包 属性劫持
let childOb = observe(value); // 对所有的对象都进行属性劫持 childOb.dep 用来收集依赖的
let dep = new Dep(); // 每一个属性都有一个dep
Object.defineProperty(target,key,{
get(){ // 取值的时候 会执行get
if(Dep.target){
dep.depend(); // 让这个属性的收集器记住当前的watcher
if(childOb){
childOb.dep.depend(); // 让数组和对象本身也实现依赖收集
if(Array.isArray(value)){
dependArray(value);
}
}
}
return value
},
diff算法:
export function initLifeCycle(Vue){
Vue.prototype._update = function(vnode){ // 将vnode转化成真实dom
const vm = this;
const el = vm.$el;
// patch既有初始化的功能 又有更新
vm.$el = patch(el,vnode);
}
export function patch(oldVNode, vnode) {
// 写的是初渲染流程
const isRealElement = oldVNode.nodeType;
if (isRealElement) {
const elm = oldVNode; // 获取真实元素
const parentElm = elm.parentNode; // 拿到父元素
let newElm = createElm(vnode);
parentElm.insertBefore(newElm, elm.nextSibling);
parentElm.removeChild(elm); // 删除老节点
return newElm
} else {
// 1.两个节点不是同一个节点 直接删除老的换上新的 (没有比对了)
// 2.两个节点是同一个节点 (判断节点的tag和 节点的key) 比较两个节点的属性是否有差异 (复用老的节点,将差异的属性更新)
// 3.节点比较完毕后就需要比较两人的儿子
return patchVnode(oldVNode, vnode);
}
}
function patchVnode(oldVNode, vnode) {
if (!isSameVnode(oldVNode, vnode)) { // tag == tag key === key
// 用老节点的父亲 进行替换
let el = createElm(vnode);
oldVNode.el.parentNode.replaceChild(el, oldVNode.el)
return el;
}
// 文本的情况 文本我们期望比较一下文本的内容
let el = vnode.el = oldVNode.el; // 复用老节点的元素
if (!oldVNode.tag) { // 是文本
if (oldVNode.text !== vnode.text) {
el.textContent = vnode.text; // 用新的文本覆盖掉老的
}
}
// 是标签 是标签我们需要比对标签的属性
patchProps(el, oldVNode.data, vnode.data);
// 比较儿子节点 比较的时候 一方有儿子 一方没儿子
// 两方都有儿子
let oldChildren = oldVNode.children || [];
let newChildren = vnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
// 完整的diff算法 需要比较两个人的儿子
updateChildren(el, oldChildren, newChildren);
} else if (newChildren.length > 0) { // 没有老的,有新的
mountChildren(el, newChildren);
} else if (oldChildren.length > 0) { // 新的没有 老的有 要删除
el.innerHTML = ''; // 可以循环删除
}
return el;
}
function mountChildren(el, newChildren) {
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child))
}
}
function updateChildren(el, oldChildren, newChildren) {
// 我们操作列表 经常会是有 push shift pop unshift reverse sort这些方法 (针对这些情况做一个优化)
// vue2中采用双指针的方式 比较两个节点
let oldStartIndex = 0;
let newStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newEndIndex = newChildren.length - 1;
let oldStartVnode = oldChildren[0];
let newStartVnode = newChildren[0];
let oldEndVnode = oldChildren[oldEndIndex];
let newEndVnode = newChildren[newEndIndex];
// 循环的时候为什么要+key
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // 有任何一个不满足则停止 || 有一个为true 就继续走
// 双方有一方头指针,大于尾部指针则停止循环
if (isSameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode); // 如果是相同节点 则递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
// 比较开头节点
}
if (isSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode); // 如果是相同节点 则递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
// 比较开头节点
}
}
if (newStartIndex <= newEndIndex) { // 新的多了 多余的就插入进去
for (let i = newStartIndex; i <= newEndIndex; i++) {
let childEl = createElm(newChildren[i])
// 这里可能是像后追加 ,还有可能是向前追加
let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null; // 获取下一个元素
// el.appendChild(childEl);
el.insertBefore(childEl, anchor); // anchor 为null的时候则会认为是appendChild
}
}
if (oldStartIndex <= oldEndIndex) { // 老的对了,需要删除老的
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (oldChildren[i]) {
let childEl = oldChildren[i].el
el.removeChild(childEl);
}
}
}
}
updateChildren中的遍历,主要考虑两种情况,向尾部插入或删除,还有向头部插入或删除
对于在尾部追加或删除的情况,newStartIndex oldStartIndex会向右移动,索引递增,上述代码中,updateChildren方法中命中24行的if判断,最终使得oldStartIndex或newStartIndex越界
接下来
对于尾部追加的情况,会命中37行的if判断,将新增的元素加到后面
对于尾部删除的情况,会命中47行的if判断,将多余的元素删除掉
对于在头部追加或删除的情况,newEndIndex oldEndIndex会向左移动,索引递减,上述代码中,
updateChildren方法中命中30行的if判断,最终使得oldEndIndex或newEndIndex越界
接下来在while循环的后面
对于头部追加的情况,会命中37行的if判断,将新增的元素加到前面,这里用insertBefore同时兼容了插入和追加的情况,通过判断插入索引的下一个位置是否有元素存在来判断是插入还是追加
对于头部删除的情况,和尾部删除的处理方式一样,不再赘述
为了优化队列反转,Vue又增加了交叉对比,例如如下情况:
上面一排是老的节点,下面一排是新的
在第22行while循环的过程中,24和30行的条件都不会命中,所以我们又添加了一个逻辑,就是首尾比较,即用oldEndVnode(老队列的最后一个节点)和newStartVnode(新队列的第一个节点)来进行比较
这个思路顺带优化了一种情况,就是最后一个节点移动到最前面的这种情况:
当24和30行的条件都不会命中时,我们的思路是拿两个队列编号为4的节点进行比较
发现二者相同之后,调换位置,再按如下方向移动指针
代码体现为28-34行,在移动完4之后,newStartIndex和oldStartIndex都指向了1,而newEndIndex和oldEndIndex都指向了3,所以我们必须把while中的多个if判断改为if else判断,防止进入两个if中去:
function updateChildren(el, oldChildren, newChildren) {
// 我们操作列表 经常会是有 push shift pop unshift reverse sort这些方法 (针对这些情况做一个优化)
// vue2中采用双指针的方式 比较两个节点
let oldStartIndex = 0;
let newStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newEndIndex = newChildren.length - 1;
let oldStartVnode = oldChildren[0];
let newStartVnode = newChildren[0];
let oldEndVnode = oldChildren[oldEndIndex];
let newEndVnode = newChildren[newEndIndex];
// 循环的时候为什么要+key
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // 有任何一个不满足则停止 || 有一个为true 就继续走
// 双方有一方头指针,大于尾部指针则停止循环
if (isSameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode); // 如果是相同节点 则递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
// 比较开头节点
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode); // 如果是相同节点 则递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
// 比较开头节点
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
// insertBefore 具备移动性 会将原来的元素移动走
el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 将老的尾巴移动到老的前面去
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
}
}
与此相对称的是,第一个节点移动到最后时的情况:
function updateChildren(el, oldChildren, newChildren) {
// 我们操作列表 经常会是有 push shift pop unshift reverse sort这些方法 (针对这些情况做一个优化)
// vue2中采用双指针的方式 比较两个节点
let oldStartIndex = 0;
let newStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newEndIndex = newChildren.length - 1;
let oldStartVnode = oldChildren[0];
let newStartVnode = newChildren[0];
let oldEndVnode = oldChildren[oldEndIndex];
let newEndVnode = newChildren[newEndIndex];
// 循环的时候为什么要+key
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // 有任何一个不满足则停止 || 有一个为true 就继续走
// 双方有一方头指针,大于尾部指针则停止循环
if (isSameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode); // 如果是相同节点 则递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
// 比较开头节点
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode); // 如果是相同节点 则递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
// 比较开头节点
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
// insertBefore 具备移动性 会将原来的元素移动走
el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 将老的尾巴移动到老的前面去
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
// insertBefore 具备移动性 会将原来的元素移动走
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); // 将老的尾巴移动到老的前面去
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex];
}
}
key的作用:
在如下案例中,列表使用index作为key来进行比对,如果用户勾选了第0个CheckBox,如果再点追加,向li里unshift('桃子')时,会发现选中项是新加进来的'桃子',而且通过控制台还可以看到li实际上是更新了3次,最后在末尾追加了一项,而不是做头尾比对,直接在最前面添加一项
<div id="app">
<div>
<li v-for="(a,index) in arr" :key="index">
{{a}} <input type="checkbox">
</li>
</div>
<button @click="append">追加</button>
</div>
<!-- <script src="vue.js"></script> -->
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data() {
// 自己做id是可以的 , 你自己弄一个index 也是相当于造了一个ad
return { arr: ['香蕉', '苹果', '橘子'] }
},
methods:{
append(){
this.arr.unshift('桃子');
}
}
})
</script>
其原因就是新旧队列的第0、1、2项的key都是一样的,就会命中isSameVnode(oldStartVnode, newStartVnode)的条件:
例如,在新旧列表中,对于key都是0的两项(香蕉、桃子),进入isSameVnode(oldStartVnode, newStartVnode)之后,会继续执行patchVnode(oldStartVnode, newStartVnode),将旧的文本“香蕉”替换为新的文本“桃子”,之前该节点处于checked状态,更新后这个节点并没有被移动,只是做了文本替换,所以依然是checked状态
但如果把key换成名字本身,结果就会不同,即如下代码:
<div id="app">
<div>
<li v-for="(a,index) in arr" :key="a">
{{a}} <input type="checkbox">
</li>
</div>
<button @click="append">追加</button>
</div>
<!-- <script src="vue.js"></script> -->
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script>
<script>
let vm = new Vue({
el: '#app',
data() {
// 自己做id是可以的 , 你自己弄一个index 也是相当于造了一个ad
return { arr: ['香蕉', '苹果', '橘子'] }
},
methods:{
append(){
this.arr.unshift('桃子');
}
}
})
</script>
在对比过程中,不满足isSameVnode(oldStartVnode, newStartVnode),而满足isSameVnode(oldEndVnode, newEndVnode),从而命中从尾部开始对比的逻辑,最终就会在头部插入一项,而checked的项也是香蕉
当以上条件都无法命中时,Vue认为是乱序比对,此时要尽可能复用, 首先将实现的代码初步列出:
生成映射表:
function updateChildren(el, oldChildren, newChildren) {
// 我们操作列表 经常会是有 push shift pop unshift reverse sort这些方法 (针对这些情况做一个优化)
// vue2中采用双指针的方式 比较两个节点
let oldStartIndex = 0;
let newStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newEndIndex = newChildren.length - 1;
let oldStartVnode = oldChildren[0];
let newStartVnode = newChildren[0];
let oldEndVnode = oldChildren[oldEndIndex];
let newEndVnode = newChildren[newEndIndex];
function makeIndexByKey(children) {
let map = {
}
children.forEach((child, index) => {
map[child.key] = index;
});
return map;
}
let map = makeIndexByKey(oldChildren);
乱序比对的过程(在最后一个else分支中体现):
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // 有任何一个不满足则停止 || 有一个为true 就继续走
// 双方有一方头指针,大于尾部指针则停止循环
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
} else if (isSameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode); // 如果是相同节点 则递归比较子节点
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
// 比较开头节点
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode); // 如果是相同节点 则递归比较子节点
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
// 比较开头节点
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode);
// insertBefore 具备移动性 会将原来的元素移动走
el.insertBefore(oldEndVnode.el, oldStartVnode.el); // 将老的尾巴移动到老的前面去
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex];
}
else if (isSameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode);
// insertBefore 具备移动性 会将原来的元素移动走
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); // 将老的尾巴移动到老的前面去
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex];
} else {
// 在给动态列表添加key的时候 要尽量避免用索引,因为索引前后都是从0 开始 , 可能会发生错误复用
// 乱序比对
// 根据老的列表做一个映射关系 ,用新的去找,找到则移动,找不到则添加,最后多余的就删除
let moveIndex = map[newStartVnode.key]; // 如果拿到则说明是我要移动的索引
if (moveIndex !== undefined) {
let moveVnode = oldChildren[moveIndex]; // 找到对应的虚拟节点 复用
el.insertBefore(moveVnode.el, oldStartVnode.el);
oldChildren[moveIndex] = undefined; // 表示这个节点已经移动走了
patchVnode(moveVnode, newStartVnode); // 比对属性和子节点
} else {
el.insertBefore(createElm(newStartVnode), oldStartVnode.el);
}
newStartVnode = newChildren[++newStartIndex];
思路:
根据老的列表做一个映射关系,用新的去找,找到则移动,找不到则添加,最后多余的就删除
比对过程:
旧节点key和索引的映射:
{ a: 0, b: 1, c: 2, d: 3 }
新旧列表初始状态:
这种状态下,头头对比、尾尾对比、头尾对比、尾头对比条件都无法命中,就会走到乱序对比,代码中体现为最后的else分支
newStartIndex最开始指向B,接下来就拿着B的key去上面的映射中去找,结果找到了,证明老列表里有B,而且索引是1,接下来将B移动(实际调用的是insertBefore)到oldStartIndex的前面,并且将新列表的newStartIndex向后移(在该过程中newStartIndex从B移动到M,下图新列表中newChildren一排展示的红色实现箭头是newStartIndex移动后的结果)
除此之外,oldChildren中(即老的虚拟节点列表),我们需要将虚拟节点B对应的位置置为undefined,需要注意此时oldStartIndex还没有移动,依然在A节点处:
需要注意,不可以在oldChildren中直接将B节点删掉,否则指针指向会错乱,例如oldEndIndex依然是3,但却并不指向D节点了
接下来while循环又来了一轮,这一轮依然无法命中头头对比、尾尾对比、头尾对比、尾头对比的条件,在这一轮中,新的列表遇到了M
M在映射里找不到,于是直接将它插入到oldStartIndex对应的真实节点的前面,且newStartIndex继续向后移动:
接下来while循环又来了一轮,newStartIndex和oldStartIndex都遇到了A节点,这一轮可以命中头头对比条件,A节点将会复用,且newStartIndex和oldStartIndex均向后移动:
接下来while循环又来了一轮,由于oldChildren中B节点被置为undefined,如果不做任何处理,还是会走到最后的else分支,而且会报错,因为在else中会尝试从oldStartVnode上获取el,这个是获取不到的
所以,我们需要对undefined的节点做特殊处理:
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { // 有任何一个不满足则停止 || 有一个为true 就继续走
// 双方有一方头指针,大于尾部指针则停止循环
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
根据这个条件,可以知道,我们没有对真实节点做任何操作,仅仅是移动了指针:
接下来的一轮循环,依然不命中else分支前面的任何一个条件,依然是乱序对比,将newChildren中P节点对应的真实节点插入到当前oldStartIndex对应的节点C的前面,并将newStartIndex向后移
接下来的一轮循环,又命中了isSameVnode(oldStartVnode, newStartVnode)的条件,做了节点复用之后,指针继续后移:
再往下执行由于oldStartVnode和newStartVnode重合,oldEndVnode和newEndVnode重合,while条件不满足,循环整体结束,接下来走到了循环的后面:
if (newStartIndex <= newEndIndex) { // 新的多了 多余的就插入进去
for (let i = newStartIndex; i <= newEndIndex; i++) {
let childEl = createElm(newChildren[i])
// 这里可能是像后追加 ,还有可能是向前追加
let anchor = newChildren[newEndIndex + 1] ? newChildren[newEndIndex + 1].el : null; // 获取下一个元素
// el.appendChild(childEl);
el.insertBefore(childEl, anchor); // anchor 为null的时候则会认为是appendChild
}
}
根据上述代码的逻辑,此时的情况满足newStartIndex <= newEndIndex的条件,我们将创建出虚拟节点Q对应的真实节点,然后插入anchor的前面,需要注意,此处由于newEndIndex已经到最后了,所以newChildren[newEndIndex + 1]是undefined,我们的anchor就会是null,insertBefore兼容到这种情况时,执行的行为是appendChild,于是Q对应的真实节点就会插入到最后
执行完newStartIndex <= newEndIndex条件对应的行为之后,接下来又进入到oldStartIndex <= oldEndIndex:
if (oldStartIndex <= oldEndIndex) { // 老的对了,需要删除老的
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
if (oldChildren[i]) {
let childEl = oldChildren[i].el
el.removeChild(childEl);
}
}
}
这一步会将oldChildren中节点D对应的真实DOM删除,完成最终的更新过程,可以发现,真实节点最终和newChildren是一样的排列顺序
全局组件和局部组件的继承关系:
在_init -> mergeOptions中,将构造函数上的options和_init传进来的options进行合并
export function initMixin(Vue) { // 就是给Vue增加init方法的
Vue.prototype._init = function (options) { // 用于初始化操作
// vue vm.$options 就是获取用户的配置
// 我们使用的 vue的时候 $nextTick $data $attr.....
const vm = this;
// 我们定义的全局指令和过滤器.... 都会挂载到实力上
// Sub.options,options
vm.$options = mergeOptions(this.constructor.options,options); // 将用户的选项挂载到实例上
这样,通过调用Vue.component注册到Vue.options.components中的全局组件,就会和局部组件进行合并,然后在模板中使用
function createComponentVnode(vm, tag, key, data, children, Ctor) {
if (typeof Ctor === 'object') {
Ctor = vm.$options._base.extend(Ctor)
// Ctor = Vue.extend(Ctor)
}
data.hook = {
init(vnode){ // 稍后创造真实节点的时候 如果是组件则调用此init方法
// 保存组件的实例到虚拟节点上
let instance = vnode.componentInstance = new vnode.componentOptions.Ctor;
instance.$mount(); // instance.$el
}
}
return vnode(vm, tag, key, data, children, null, { Ctor })
}
注:createComponentVnode中期望调用Vue.extend方法,上面代码的第3行,通过vm.$options._base引用拿到了Vue,进而拿到了Vue.extend
注意不可以通过vm.constructor.extend去试图获取Vue.extend方法,因为vm.constructor有可能是Vue,也有可能是Sub,Sub上是没有extend方法的,因为Sub和Vue之间的继承关系仅限于它们的实例之间,静态方法extend是没有继承的
经过测试,还有一种方案,此处可以直接将Vue引入,虽然会构成循环依赖,但并没有出现错误,也可以编译成功
为什么Vue的组件中的data不能是一个对象呢?
我们定义的每个组件,都是通过extend继承了Vue的一个方法:Sub,如果data是一个对象的话,所有组件的实例就会共享这个对象,都改这一个对象,造成错误
组件化
由于组件化涉及到父子组件render即update的流程较为繁琐,因此为表述方便,以及使流程更加清晰,我们使用如下案例来进行分析,并引入“根组件”和“mybutton组件”两个称呼来说明问题:
const vm = new Vue({
el: '#app',
data() {
return { name: 'zf' }
},
components:{ // js中的原型链 内部可能是一个继承的模型
'my-button':Vue.extend({
template:'<button>inner Button</button>'
})
},
template: '<div>子组件 <my-button></my-button> </div>',
});
根组件执行到this.render时,会调用_c方法创建虚拟DOM树结构,_c内部调用了createElementVNode方法,在该方法内部,如果遇到非原生节点(my-button),则会启动创建组件节点的流程
export function createElementVNode(vm, tag, data, ...children) {
if (data == null) {
data = {}
}
let key = data.key;
if (key) {
delete data.key
}
if (isReservedTag(tag)) {
return vnode(vm, tag, key, data, children);
} else {
// 创造一个组件的虚拟节点 (包含组件的构造函数)
let Ctor = vm.$options.components[tag]; // 组件的构造函数
// Ctor就是组件的定义 可能是一个Sub类 还有可能是组件的obj选项
return createComponentVnode(vm, tag, key, data, children, Ctor);
}
}
可以看到组件节点和原生节点的区别在于调用createComponentVnode时多增加了Ctor参数
function createComponentVnode(vm, tag, key, data, children, Ctor) {
if (typeof Ctor === 'object') {
Ctor = vm.$options._base.extend(Ctor)
// Ctor = Vue.extend(Ctor)
}
data.hook = {
init(vnode){ // 稍后创造真实节点的时候 如果是组件则调用此init方法
// 保存组件的实例到虚拟节点上
let instance = vnode.componentInstance = new vnode.componentOptions.Ctor;
instance.$mount(); // instance.$el
}
}
return vnode(vm, tag, key, data, children, null, { Ctor })
}
function vnode(vm, tag, key, data, children, text, componentOptions) {
return {
vm,
tag,
key,
data,
children,
text,
componentOptions // 组件的构造函数
// ....
}
}
此外,组件的vnode节点上data属性也是有意义的,目前我们可以看到它包含了init方法,将来在实例化组件的时候就会调用它
render执行完后,会继续执行update -> patch方法将虚拟节点树转换为真实DOM,注意此时执行的都是根组件的render和update,由于prevVnode是undefined,所以执行到了下面代码第9行
Vue.prototype._update = function(vnode){ // 将vnode转化成真实dom
const vm = this;
const el = vm.$el;
const prevVnode = vm._vnode;
vm._vnode = vnode; // 把组件第一次产生的虚拟节点保存到_vnode上
if(prevVnode){ // 之前渲染过了
vm.$el = patch(prevVnode,vnode);
}else{
vm.$el = patch(el,vnode);
根组件的options中el是必填项,所以这里的oldVNode对应的实参就是el,所以下面代码的第3行无法命中,继续走下面的流程
export function patch(oldVNode, vnode) {
// mount()
if(!oldVNode){ // 这就是组件的挂载
return createElm(vnode); // vm.$el 对应的就是组件渲染的结果了
}
// 写的是初渲染流程
const isRealElement = oldVNode.nodeType;
if (isRealElement) {
const elm = oldVNode; // 获取真实元素
const parentElm = elm.parentNode; // 拿到父元素
let newElm = createElm(vnode);
本案例中,生产的render方法如下:
(function anonymous(
) {
with(this){return _c('div',null,_v("子组件"),_c('my-button',null))}
})
生成的vnode树大概如下:
{
tag: 'div',
children: [
{
tag: undefined,
text: '子组件 '
},
{
tag: 'my-button',
componentOptions: { Ctor }
}
]
}
据此结构,我们可以分析,进入到createElm执行到createComponent时,会返回undefined,再执行到遍历children将节点插入,当遍历到第2个children,即my-button时,createComponent(vnode)就会命中
export function createElm(vnode) {
let { tag, data, children, text } = vnode;
if (typeof tag === 'string') { // 标签
// 创建真实元素 也要区分是组件还是元素
if (createComponent(vnode)) { // 组件 vnode.componentInstance.$el
return vnode.componentInstance.$el;
}
vnode.el = document.createElement(tag); // 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
patchProps(vnode.el, {}, data);
children.forEach(child => {
vnode.el.appendChild(createElm(child)); // 会将组件创建的元素插入到父元素中
});
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el
}
此时就开始了my-button组件的实例化、初始化:
function createComponent(vnode) {
let i = vnode.data;
if ((i = i.hook) && (i = i.init)) { // data.hook.init
i(vnode); // 初始化组件 , 找到init方法
}
if(vnode.componentInstance){
return true; // 说明是组件
}
}
接下来就执行到了组件的初始化,如下代码第9行
function createComponentVnode(vm, tag, key, data, children, Ctor) {
if (typeof Ctor === 'object') {
Ctor = vm.$options._base.extend(Ctor)
// Ctor = Vue.extend(Ctor)
}
data.hook = {
init(vnode){ // 稍后创造真实节点的时候 如果是组件则调用此init方法
// 保存组件的实例到虚拟节点上
let instance = vnode.componentInstance = new vnode.componentOptions.Ctor;
instance.$mount(); // instance.$el
}
}
紧接着,再沿着new Vue -> _init,在Vue.prototype._init中,由于我们并没有传options.el,所以并不会执行到vm.mount方法来触发挂载过程
由于没有el参数,因此相比较于根组件的mount里面,进而进入mountComponent -> updateComponent -> _update -> patch(el,vnode)之后el也是undefined
在patch当中,我们专门对el是undefined做了处理:
export function patch(oldVNode, vnode) {
// mount()
if(!oldVNode){ // 这就是组件的挂载
return createElm(vnode); // vm.$el 对应的就是组件渲染的结果了
}
export function createElm(vnode) {
let { tag, data, children, text } = vnode;
if (typeof tag === 'string') { // 标签
// 创建真实元素 也要区分是组件还是元素
if (createComponent(vnode)) { // 组件 vnode.componentInstance.$el
return vnode.componentInstance.$el;
}
vnode.el = document.createElement(tag); // 这里将真实节点和虚拟节点对应起来,后续如果修改属性了
patchProps(vnode.el, {}, data);
children.forEach(child => {
vnode.el.appendChild(createElm(child)); // 会将组件创建的元素插入到父元素中
});
} else {